UI: Speed up alerts/rules/... pages by not rendering collapsed content

In contrast to Bootstrap, Mantine's Accordion component didn't remove its
panel contents from the DOM when collapsed, so rendering pages with lots of
collapsed Accordion items was way slower and more resource-intensive in the
new Mantine UI. While I talked to Vitaly from Mantine and he managed to add
unmounting of collapsed panel contents in Mantine 9, this will only be
available next year. So for now, I'm forking over the Accordion component
from Mantine and adding a hacky modification to it that removes contents
for collapsed panels. This fork can be removed after upgrading to Mantine 9
sometime in 2026. I removed all the unnecessary test files and so on and
just kept the core Accordion code files.

This should really help with the following issues:

https://github.com/prometheus/prometheus/issues/17254
https://github.com/prometheus/prometheus/issues/16830

The /alerts and /rules pages should be the most affected since the panels
on those are collapsed by default. The /targets and /service-discovery
pages have expanded panels by default, but I still swapped out the
Accordion implementation for consistency and in case someone collapses a
bunch of panels.

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2025-11-06 10:12:46 +01:00
parent 1b7fa3e76d
commit 2b25bbf997
17 changed files with 1113 additions and 9 deletions

5
NOTICE
View file

@ -101,6 +101,11 @@ https://github.com/microsoft/vscode-codicons
Copyright (c) Microsoft Corporation and other contributors
See https://github.com/microsoft/vscode-codicons/blob/main/LICENSE for license details.
Mantine UI
https://github.com/mantinedev/mantine
Copyright (c) 2021 Vitaly Rtishchev
See https://github.com/mantinedev/mantine/blob/master/LICENSE for license details.
We also use code from a large number of npm packages. For details, see:
- https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/package.json
- https://github.com/prometheus/prometheus/blob/main/web/ui/react-app/package-lock.json

View file

@ -16,7 +16,7 @@ const compat = new FlatCompat({
});
export default [{
ignores: ['**/dist', '**/.eslintrc.cjs'],
ignores: ['**/dist', '**/.eslintrc.cjs', 'src/components/Accordion/**'],
}, ...fixupConfigRules(compat.extends(
'eslint:recommended',
'plugin:@typescript-eslint/recommended',

View file

@ -0,0 +1,10 @@
This is a temporary fork of the Accordion component from Mantine UI v8.3.6 with modifications specific to Prometheus.
The component has been modified to unmount children of collapsed panels to reduce page rendering times and
resource usage.
According to Mantine author Vitaly, a similar feature has now been added to Mantine itself, but will only be
available in version 9.0.0 and later, quote from https://discord.com/channels/854810300876062770/1006447791498870784/threads/1428787320546525336:
> I've managed to implement it, but only in 9.0 since it requires some breaking changes. Will be available next year
So this Accordion fork can be removed once Prometheus upgrades to Mantine v9 or later.

View file

@ -0,0 +1,56 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
import { createSafeContext, GetStylesApi } from "@mantine/core";
import type { AccordionFactory } from "./Accordion";
import {
AccordionChevronPosition,
AccordionHeadingOrder,
} from "./Accordion.types";
interface AccordionContext {
loop: boolean | undefined;
transitionDuration: number | undefined;
disableChevronRotation: boolean | undefined;
chevronPosition: AccordionChevronPosition | undefined;
order: AccordionHeadingOrder | undefined;
chevron: React.ReactNode;
onChange: (value: string) => void;
isItemActive: (value: string) => boolean;
getControlId: (value: string) => string;
getRegionId: (value: string) => string;
getStyles: GetStylesApi<AccordionFactory>;
variant: string | undefined;
unstyled: boolean | undefined;
}
export const [AccordionProvider, useAccordionContext] =
createSafeContext<AccordionContext>(
"Accordion component was not found in the tree"
);

View file

@ -0,0 +1,204 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
.root {
--accordion-radius: var(--mantine-radius-default);
}
.panel {
overflow-wrap: break-word;
}
.content {
padding: var(--mantine-spacing-md);
padding-top: calc(var(--mantine-spacing-xs) / 2);
}
.itemTitle {
margin: 0;
padding: 0;
}
.control {
width: 100%;
display: flex;
align-items: center;
flex-direction: row-reverse;
padding-inline: var(--mantine-spacing-md);
opacity: 1;
cursor: pointer;
background-color: transparent;
color: var(--mantine-color-bright);
&:where([data-chevron-position="left"]) {
flex-direction: row;
padding-inline-start: 0;
}
&:where(:disabled, [data-disabled]) {
opacity: 0.4;
cursor: not-allowed;
}
}
.control--default,
.control--contained {
&:where(:not(:disabled, [data-disabled])) {
@mixin hover {
@mixin where-light {
background-color: var(--mantine-color-gray-0);
}
@mixin where-dark {
background-color: var(--mantine-color-dark-6);
}
}
}
}
.label {
color: inherit;
font-weight: 400;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
padding-top: var(--mantine-spacing-sm);
padding-bottom: var(--mantine-spacing-sm);
}
.chevron {
display: flex;
align-items: center;
justify-content: flex-start;
transition: transform var(--accordion-transition-duration, 200ms) ease;
width: var(--accordion-chevron-size, rem(15px));
min-width: var(--accordion-chevron-size, rem(15px));
transform: rotate(0deg);
&:where([data-rotate]) {
transform: rotate(180deg);
}
&:where([data-position="left"]) {
margin-inline-end: var(--mantine-spacing-md);
margin-inline-start: var(--mantine-spacing-md);
}
}
.icon {
display: flex;
align-items: center;
justify-content: center;
margin-inline-end: var(--mantine-spacing-sm);
&:where([data-chevron-position="left"]) {
margin-inline-end: 0;
margin-inline-start: var(--mantine-spacing-lg);
}
}
.item {
@mixin where-light {
--item-border-color: var(--mantine-color-gray-3);
--item-filled-color: var(--mantine-color-gray-0);
}
@mixin where-dark {
--item-border-color: var(--mantine-color-dark-4);
--item-filled-color: var(--mantine-color-dark-6);
}
}
.item--default {
border-bottom: 1px solid var(--item-border-color);
}
.item--contained {
border: 1px solid var(--item-border-color);
transition: background-color 150ms ease;
&:where([data-active]) {
background-color: var(--item-filled-color);
}
&:first-of-type {
border-start-start-radius: var(--accordion-radius);
border-start-end-radius: var(--accordion-radius);
& > [data-accordion-control] {
border-start-start-radius: var(--accordion-radius);
border-start-end-radius: var(--accordion-radius);
}
}
&:last-of-type {
border-end-start-radius: var(--accordion-radius);
border-end-end-radius: var(--accordion-radius);
& > [data-accordion-control] {
border-end-start-radius: var(--accordion-radius);
border-end-end-radius: var(--accordion-radius);
}
}
& + & {
border-top: 0;
}
}
.item--filled {
border-radius: var(--accordion-radius);
&:where([data-active]) {
background-color: var(--item-filled-color);
}
}
.item--separated {
background-color: var(--item-filled-color);
border-radius: var(--accordion-radius);
border: 1px solid transparent;
transition: background-color 150ms ease;
&[data-active] {
border-color: var(--item-border-color);
@mixin where-light {
background-color: var(--mantine-color-white);
}
@mixin where-dark {
background-color: var(--mantine-color-dark-7);
}
}
& + & {
margin-top: var(--mantine-spacing-md);
}
}

View file

@ -0,0 +1,280 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
import { useId, useUncontrolled } from "@mantine/hooks";
import {
Box,
BoxProps,
createVarsResolver,
ElementProps,
ExtendComponent,
Factory,
getRadius,
getSafeId,
getWithProps,
MantineRadius,
MantineThemeComponent,
rem,
StylesApiProps,
useProps,
useStyles,
} from "@mantine/core";
import { AccordionProvider } from "./Accordion.context";
import {
AccordionChevronPosition,
AccordionHeadingOrder,
AccordionValue,
} from "./Accordion.types";
import { AccordionChevron } from "./AccordionChevron";
import { AccordionControl } from "./AccordionControl/AccordionControl";
import { AccordionItem } from "./AccordionItem/AccordionItem";
import { AccordionPanel } from "./AccordionPanel/AccordionPanel";
import classes from "./Accordion.module.css";
export type AccordionStylesNames =
| "root"
| "content"
| "item"
| "panel"
| "icon"
| "chevron"
| "label"
| "itemTitle"
| "control";
export type AccordionVariant = "default" | "contained" | "filled" | "separated";
export type AccordionCssVariables = {
root:
| "--accordion-transition-duration"
| "--accordion-chevron-size"
| "--accordion-radius";
};
export interface AccordionProps<Multiple extends boolean = false>
extends BoxProps,
StylesApiProps<AccordionFactory>,
ElementProps<"div", "value" | "defaultValue" | "onChange"> {
/** If set, multiple items can be opened at the same time */
multiple?: Multiple;
/** Controlled component value */
value?: AccordionValue<Multiple>;
/** Uncontrolled component default value */
defaultValue?: AccordionValue<Multiple>;
/** Called when value changes, payload type depends on `multiple` prop */
onChange?: (value: AccordionValue<Multiple>) => void;
/** If set, arrow keys loop though items (first to last and last to first) @default `true` */
loop?: boolean;
/** Transition duration in ms @default `200` */
transitionDuration?: number;
/** If set, chevron rotation is disabled */
disableChevronRotation?: boolean;
/** Position of the chevron relative to the item label @default `right` */
chevronPosition?: AccordionChevronPosition;
/** Size of the chevron icon container @default `auto` */
chevronSize?: number | string;
/** Size of the default chevron icon. Ignored when `chevron` prop is set. @default `16` */
chevronIconSize?: number | string;
/** Heading order, has no effect on visuals */
order?: AccordionHeadingOrder;
/** Custom chevron icon */
chevron?: React.ReactNode;
/** Key of `theme.radius` or any valid CSS value to set border-radius. Numbers are converted to rem. @default `theme.defaultRadius` */
radius?: MantineRadius;
}
export type AccordionFactory = Factory<{
props: AccordionProps;
ref: HTMLDivElement;
stylesNames: AccordionStylesNames;
vars: AccordionCssVariables;
variant: AccordionVariant;
}>;
const defaultProps = {
multiple: false,
disableChevronRotation: false,
chevronPosition: "right",
variant: "default",
chevronSize: "auto",
chevronIconSize: 16,
} satisfies Partial<AccordionProps>;
const varsResolver = createVarsResolver<AccordionFactory>(
(_, { transitionDuration, chevronSize, radius }) => ({
root: {
"--accordion-transition-duration":
transitionDuration === undefined
? undefined
: `${transitionDuration}ms`,
"--accordion-chevron-size":
chevronSize === undefined ? undefined : rem(chevronSize),
"--accordion-radius":
radius === undefined ? undefined : getRadius(radius),
},
})
);
export function Accordion<Multiple extends boolean = false>(
_props: AccordionProps<Multiple>
) {
const props = useProps(
"Accordion",
defaultProps as AccordionProps<Multiple>,
_props
);
const {
classNames,
className,
style,
styles,
unstyled,
vars,
children,
multiple,
value,
defaultValue,
onChange,
id,
loop,
transitionDuration,
disableChevronRotation,
chevronPosition,
chevronSize,
order,
chevron,
variant,
radius,
chevronIconSize,
attributes,
...others
} = props;
const uid = useId(id);
const [_value, handleChange] = useUncontrolled({
value,
defaultValue,
finalValue: multiple ? ([] as any) : null,
onChange,
});
const isItemActive = (itemValue: string) =>
Array.isArray(_value) ? _value.includes(itemValue) : itemValue === _value;
const handleItemChange = (itemValue: string) => {
const nextValue: AccordionValue<Multiple> = Array.isArray(_value)
? _value.includes(itemValue)
? _value.filter((selectedValue) => selectedValue !== itemValue)
: [..._value, itemValue]
: itemValue === _value
? null
: (itemValue as any);
handleChange(nextValue);
};
const getStyles = useStyles<AccordionFactory>({
name: "Accordion",
classes,
props: props as AccordionProps,
className,
style,
classNames,
styles,
unstyled,
attributes,
vars,
varsResolver,
});
return (
<AccordionProvider
value={{
isItemActive,
onChange: handleItemChange,
getControlId: getSafeId(
`${uid}-control`,
"Accordion.Item component was rendered with invalid value or without value"
),
getRegionId: getSafeId(
`${uid}-panel`,
"Accordion.Item component was rendered with invalid value or without value"
),
chevron:
chevron === null
? null
: chevron || <AccordionChevron size={chevronIconSize} />,
transitionDuration,
disableChevronRotation,
chevronPosition,
order,
loop,
getStyles,
variant,
unstyled,
}}
>
<Box
{...getStyles("root")}
id={uid}
{...others}
variant={variant}
data-accordion
>
{children}
</Box>
</AccordionProvider>
);
}
const extendAccordion = (
c: ExtendComponent<AccordionFactory>
): MantineThemeComponent => c;
Accordion.extend = extendAccordion;
Accordion.withProps = getWithProps<AccordionProps, AccordionProps>(
Accordion as any
);
Accordion.classes = classes;
Accordion.displayName = "@mantine/core/Accordion";
Accordion.Item = AccordionItem;
Accordion.Panel = AccordionPanel;
Accordion.Control = AccordionControl;
Accordion.Chevron = AccordionChevron;

View file

@ -0,0 +1,35 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
export type AccordionValue<Multiple extends boolean> = Multiple extends true
? string[]
: string | null;
export type AccordionHeadingOrder = 2 | 3 | 4 | 5 | 6;
export type AccordionChevronPosition = "left" | "right";

View file

@ -0,0 +1,66 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
import { rem } from "@mantine/core";
export interface AccordionChevronProps
extends React.ComponentPropsWithoutRef<"svg"> {
/** Controls `width` and `height` of the icon, `16` by default */
size?: number | string;
}
export function AccordionChevron({
style,
size = 16,
...others
}: AccordionChevronProps) {
return (
<svg
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{
...style,
width: rem(size),
height: rem(size),
display: "block",
}}
{...others}
>
<path
d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
);
}
AccordionChevron.displayName = "@mantine/core/AccordionChevron";

View file

@ -0,0 +1,175 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
import {
Box,
BoxProps,
CompoundStylesApiProps,
createScopedKeydownHandler,
ElementProps,
factory,
Factory,
UnstyledButton,
useProps,
} from "@mantine/core";
import { useAccordionContext } from "../Accordion.context";
import { useAccordionItemContext } from "../AccordionItem.context";
import classes from "../Accordion.module.css";
export type AccordionControlStylesNames =
| "control"
| "chevron"
| "label"
| "itemTitle"
| "icon";
export interface AccordionControlProps
extends BoxProps,
CompoundStylesApiProps<AccordionControlFactory>,
ElementProps<"button"> {
/** Sets `disabled` attribute, prevents interactions */
disabled?: boolean;
/** Custom chevron icon */
chevron?: React.ReactNode;
/** Control label */
children?: React.ReactNode;
/** Icon displayed next to the label */
icon?: React.ReactNode;
}
export type AccordionControlFactory = Factory<{
props: AccordionControlProps;
ref: HTMLButtonElement;
stylesNames: AccordionControlStylesNames;
compound: true;
}>;
export const AccordionControl = factory<AccordionControlFactory>(
(props, ref) => {
const {
classNames,
className,
style,
styles,
vars,
chevron,
icon,
onClick,
onKeyDown,
children,
disabled,
mod,
...others
} = useProps("AccordionControl", null, props);
const { value } = useAccordionItemContext();
const ctx = useAccordionContext();
const isActive = ctx.isItemActive(value);
const shouldWrapWithHeading = typeof ctx.order === "number";
const Heading = `h${ctx.order!}` as const;
const content = (
<UnstyledButton<"button">
{...others}
{...ctx.getStyles("control", {
className,
classNames,
style,
styles,
variant: ctx.variant,
})}
unstyled={ctx.unstyled}
mod={[
"accordion-control",
{
active: isActive,
"chevron-position": ctx.chevronPosition,
disabled,
},
mod,
]}
ref={ref}
onClick={(event) => {
onClick?.(event);
ctx.onChange(value);
}}
type="button"
disabled={disabled}
aria-expanded={isActive}
aria-controls={ctx.getRegionId(value)}
id={ctx.getControlId(value)}
onKeyDown={createScopedKeydownHandler({
siblingSelector: "[data-accordion-control]",
parentSelector: "[data-accordion]",
activateOnFocus: false,
loop: ctx.loop,
orientation: "vertical",
onKeyDown,
})}
>
<Box
component="span"
mod={{
rotate: !ctx.disableChevronRotation && isActive,
position: ctx.chevronPosition,
}}
{...ctx.getStyles("chevron", { classNames, styles })}
>
{chevron || ctx.chevron}
</Box>
<span {...ctx.getStyles("label", { classNames, styles })}>
{children}
</span>
{icon && (
<Box
component="span"
mod={{ "chevron-position": ctx.chevronPosition }}
{...ctx.getStyles("icon", { classNames, styles })}
>
{icon}
</Box>
)}
</UnstyledButton>
);
return shouldWrapWithHeading ? (
<Heading {...ctx.getStyles("itemTitle", { classNames, styles })}>
{content}
</Heading>
) : (
content
);
}
);
AccordionControl.displayName = "@mantine/core/AccordionControl";
AccordionControl.classes = classes;

View file

@ -0,0 +1,39 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
import { createSafeContext } from "@mantine/core";
interface AccordionItemContext {
value: string;
}
export const [AccordionItemProvider, useAccordionItemContext] =
createSafeContext<AccordionItemContext>(
"Accordion.Item component was not found in the tree"
);

View file

@ -0,0 +1,84 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
import {
Box,
BoxProps,
CompoundStylesApiProps,
ElementProps,
factory,
Factory,
useProps,
} from "@mantine/core";
import { useAccordionContext } from "../Accordion.context";
import { AccordionItemProvider } from "../AccordionItem.context";
import classes from "../Accordion.module.css";
export type AccordionItemStylesNames = "item";
export interface AccordionItemProps
extends BoxProps,
CompoundStylesApiProps<AccordionItemFactory>,
ElementProps<"div"> {
/** Value that is used to manage the accordion state */
value: string;
}
export type AccordionItemFactory = Factory<{
props: AccordionItemProps;
ref: HTMLDivElement;
stylesNames: AccordionItemStylesNames;
compound: true;
}>;
export const AccordionItem = factory<AccordionItemFactory>((props, ref) => {
const { classNames, className, style, styles, vars, value, mod, ...others } =
useProps("AccordionItem", null, props);
const ctx = useAccordionContext();
return (
<AccordionItemProvider value={{ value }}>
<Box
ref={ref}
mod={[{ active: ctx.isItemActive(value) }, mod]}
{...ctx.getStyles("item", {
className,
classNames,
styles,
style,
variant: ctx.variant,
})}
{...others}
/>
</AccordionItemProvider>
);
});
AccordionItem.displayName = "@mantine/core/AccordionItem";
AccordionItem.classes = classes;

View file

@ -0,0 +1,106 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
import {
BoxProps,
Collapse,
CompoundStylesApiProps,
ElementProps,
factory,
Factory,
useProps,
} from "@mantine/core";
import { useAccordionContext } from "../Accordion.context";
import { useAccordionItemContext } from "../AccordionItem.context";
import classes from "../Accordion.module.css";
import { useEffect, useState } from "react";
export type AccordionPanelStylesNames = "panel" | "content";
export interface AccordionPanelProps
extends BoxProps,
CompoundStylesApiProps<AccordionPanelFactory>,
ElementProps<"div"> {
/** Called when the panel animation completes */
onTransitionEnd?: () => void;
}
export type AccordionPanelFactory = Factory<{
props: AccordionPanelProps;
ref: HTMLDivElement;
stylesNames: AccordionPanelStylesNames;
compound: true;
}>;
export const AccordionPanel = factory<AccordionPanelFactory>((props, ref) => {
const { classNames, className, style, styles, vars, children, ...others } =
useProps("AccordionPanel", null, props);
const { value } = useAccordionItemContext();
const ctx = useAccordionContext();
const isActive = ctx.isItemActive(value);
// Prometheus-specific Accordion modification: unmount children when panel is closed.
const [showChildren, setShowChildren] = useState(isActive);
// Hide children from DOM 200ms after collapsing the panel
// to give the animation time to finish.
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;
if (isActive) {
setShowChildren(true);
} else {
timeout = setTimeout(() => setShowChildren(false), 200);
}
return () => clearTimeout(timeout);
}, [isActive]);
return (
<Collapse
ref={ref}
{...ctx.getStyles("panel", { className, classNames, style, styles })}
{...others}
in={isActive}
transitionDuration={ctx.transitionDuration ?? 200}
role="region"
id={ctx.getRegionId(value)}
aria-labelledby={ctx.getControlId(value)}
>
<div {...ctx.getStyles("content", { classNames, styles })}>
{/* Prometheus-specific Accordion modification: unmount children when panel is closed. */}
{showChildren && children}
</div>
</Collapse>
);
});
AccordionPanel.displayName = "@mantine/core/AccordionPanel";
AccordionPanel.classes = classes;

View file

@ -0,0 +1,47 @@
/*
* Some parts of this file are derived from Mantine UI (https://github.com/mantinedev/mantine)
* which is distributed under the MIT license:
*
* MIT License
*
* Copyright (c) 2021 Vitaly Rtishchev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Modifications to this file are licensed under the Apache License, Version 2.0.
*/
export { Accordion } from "./Accordion";
export { AccordionChevron } from "./AccordionChevron";
export { AccordionItem } from "./AccordionItem/AccordionItem";
export { AccordionPanel } from "./AccordionPanel/AccordionPanel";
export { AccordionControl } from "./AccordionControl/AccordionControl";
export type {
AccordionProps,
AccordionStylesNames,
AccordionCssVariables,
AccordionFactory,
AccordionVariant,
} from "./Accordion";
export type { AccordionControlProps } from "./AccordionControl/AccordionControl";
export type { AccordionItemProps } from "./AccordionItem/AccordionItem";
export type { AccordionPanelProps } from "./AccordionPanel/AccordionPanel";
export type { AccordionChevronProps } from "./AccordionChevron";
export type { AccordionValue, AccordionHeadingOrder } from "./Accordion.types";

View file

@ -3,7 +3,6 @@ import {
Group,
Table,
Text,
Accordion,
Badge,
Tooltip,
Box,
@ -38,6 +37,7 @@ import { KVSearch } from "@nexucis/kvsearch";
import { inputIconStyle } from "../styles";
import CustomInfiniteScroll from "../components/CustomInfiniteScroll";
import classes from "./AlertsPage.module.css";
import { Accordion } from "../components/Accordion";
type AlertsPageData = {
// How many rules are in each state across all groups.

View file

@ -1,5 +1,4 @@
import {
Accordion,
Alert,
Anchor,
Badge,
@ -46,6 +45,7 @@ import classes from "./RulesPage.module.css";
import { useDebouncedValue, useLocalStorage } from "@mantine/hooks";
import { KVSearch } from "@nexucis/kvsearch";
import { StateMultiSelect } from "../components/StateMultiSelect";
import { Accordion } from "../components/Accordion";
const kvSearch = new KVSearch<Rule>({
shouldSort: true,

View file

@ -1,5 +1,4 @@
import {
Accordion,
Alert,
Anchor,
Box,
@ -33,6 +32,7 @@ import { targetPoolDisplayLimit } from "./ServiceDiscoveryPage";
import { LabelBadges } from "../../components/LabelBadges";
import ErrorBoundary from "../../components/ErrorBoundary";
import RelabelSteps from "./RelabelSteps";
import { Accordion } from "../../components/Accordion";
type TargetLabels = {
discoveredLabels: Labels;

View file

@ -1,5 +1,4 @@
import {
Accordion,
Alert,
Anchor,
Badge,
@ -10,10 +9,7 @@ import {
Text,
} from "@mantine/core";
import { KVSearch } from "@nexucis/kvsearch";
import {
IconAlertTriangle,
IconInfoCircle,
} from "@tabler/icons-react";
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
import { useSuspenseAPIQuery } from "../../api/api";
import { Target, TargetsResult } from "../../api/responseTypes/targets";
import React, { FC, memo, useMemo } from "react";
@ -31,6 +27,7 @@ import panelClasses from "../../Panel.module.css";
import TargetLabels from "./TargetLabels";
import ScrapeTimingDetails from "./ScrapeTimingDetails";
import { targetPoolDisplayLimit } from "./TargetsPage";
import { Accordion } from "../../components/Accordion";
type ScrapePool = {
targets: Target[];