Add sorting, improve styles
This commit is contained in:
@@ -2,8 +2,10 @@ import { css } from '@emotion/css';
|
|||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Card, FilterInput, Icon, Pagination, TagList, useStyles2 } from '@grafana/ui';
|
import { Card, FilterInput, Icon, Pagination, TagList, useStyles2 } from '@grafana/ui';
|
||||||
|
import { SortPicker } from 'app/core/components/Select/SortPicker';
|
||||||
import { FolderState } from 'app/types';
|
import { FolderState } from 'app/types';
|
||||||
import { isEqual, uniqWith } from 'lodash';
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
|
import { isEqual, orderBy, uniqWith } from 'lodash';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useDebounce } from 'react-use';
|
import { useDebounce } from 'react-use';
|
||||||
@@ -15,15 +17,25 @@ import { labelsMatchMatchers, matchersToString, parseMatcher, parseMatchers } fr
|
|||||||
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||||
import { createViewLink } from './utils/misc';
|
import { createViewLink } from './utils/misc';
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 6;
|
||||||
interface Props {
|
interface Props {
|
||||||
folder: FolderState;
|
folder: FolderState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 20;
|
type SortOrder = 'alpha-asc' | 'alpha-desc';
|
||||||
|
|
||||||
export const AlertsFolderView = ({ folder }: Props) => {
|
export const AlertsFolderView = ({ folder }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const [sortOrder, setSortOrder] = useState<SortOrder>('alpha-asc');
|
||||||
|
|
||||||
|
const onTagClick = (tagName: string) => {
|
||||||
|
const matchers = parseMatchers(labelFilter);
|
||||||
|
const tagMatcherField = parseMatcher(tagName);
|
||||||
|
const uniqueMatchers = uniqWith([...matchers, tagMatcherField], isEqual);
|
||||||
|
const matchersString = matchersToString(uniqueMatchers);
|
||||||
|
setLabelFilter(matchersString);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchPromRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
|
dispatch(fetchPromRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
|
||||||
@@ -36,72 +48,71 @@ export const AlertsFolderView = ({ folder }: Props) => {
|
|||||||
const matchingNamespace = combinedNamespaces.find((namespace) => namespace.name === folder.title);
|
const matchingNamespace = combinedNamespaces.find((namespace) => namespace.name === folder.title);
|
||||||
const alertRules = matchingNamespace?.groups[0]?.rules ?? [];
|
const alertRules = matchingNamespace?.groups[0]?.rules ?? [];
|
||||||
|
|
||||||
const matchers = parseMatchers(labelFilter);
|
const filteredRules = filterAndSortRules(alertRules, nameFilter, labelFilter, sortOrder);
|
||||||
const filteredRules = alertRules.filter(
|
|
||||||
(rule) => rule.name.toLowerCase().includes(nameFilter.toLowerCase()) && labelsMatchMatchers(rule.labels, matchers)
|
|
||||||
);
|
|
||||||
|
|
||||||
const showNoResultsText = alertRules.length === 0 || filteredRules.length === 0;
|
const showNoResultsText = alertRules.length === 0 || filteredRules.length === 0;
|
||||||
const { page, numberOfPages, onPageChange, pageItems } = usePagination(filteredRules, 1, ITEMS_PER_PAGE);
|
const { page, numberOfPages, onPageChange, pageItems } = usePagination(filteredRules, 1, ITEMS_PER_PAGE);
|
||||||
|
|
||||||
const onTagClick = (tagName: string) => {
|
|
||||||
const tagMatcherField = parseMatcher(tagName);
|
|
||||||
const uniqueMatchers = uniqWith([...matchers, tagMatcherField], isEqual);
|
|
||||||
const matchersString = matchersToString(uniqueMatchers);
|
|
||||||
setLabelFilter(matchersString);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" gap={1}>
|
<div className={styles.container}>
|
||||||
<Stack direction="row">
|
<Stack direction="column" gap={3}>
|
||||||
<FilterInput
|
<FilterInput
|
||||||
value={nameFilter}
|
value={nameFilter}
|
||||||
onChange={setNameFilter}
|
onChange={setNameFilter}
|
||||||
placeholder="Search alert rules by name"
|
placeholder="Search alert rules by name"
|
||||||
className={styles.filterInput}
|
|
||||||
data-testid="name-filter"
|
data-testid="name-filter"
|
||||||
/>
|
/>
|
||||||
<FilterInput
|
<Stack direction="row">
|
||||||
value={labelFilter}
|
<SortPicker
|
||||||
onChange={setLabelFilter}
|
value={sortOrder}
|
||||||
placeholder="Search alert labels"
|
onChange={({ value }) => setSortOrder(value)}
|
||||||
className={styles.filterInput}
|
filter={['alpha-asc', 'alpha-desc']}
|
||||||
data-testid="label-filter"
|
/>
|
||||||
/>
|
<FilterInput
|
||||||
|
value={labelFilter}
|
||||||
|
onChange={setLabelFilter}
|
||||||
|
placeholder="Search alerts by labels"
|
||||||
|
className={styles.filterLabelsInput}
|
||||||
|
data-testid="label-filter"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap={1}>
|
||||||
|
{pageItems.map((currentRule) => (
|
||||||
|
<Card
|
||||||
|
key={currentRule.name}
|
||||||
|
href={createViewLink('grafana', currentRule, '')}
|
||||||
|
className={styles.card}
|
||||||
|
data-testid="alert-card-row"
|
||||||
|
>
|
||||||
|
<Card.Heading>{currentRule.name}</Card.Heading>
|
||||||
|
<Card.Tags>
|
||||||
|
<TagList
|
||||||
|
onClick={onTagClick}
|
||||||
|
tags={Object.keys(currentRule.labels).map(
|
||||||
|
(labelKey) => `${labelKey}=${currentRule.labels[labelKey]}`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Card.Tags>
|
||||||
|
<Card.Meta>
|
||||||
|
<div>
|
||||||
|
<Icon name="folder" /> {folder.title}
|
||||||
|
</div>
|
||||||
|
</Card.Meta>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
{showNoResultsText && <div className={styles.noResults}>No alert rules found</div>}
|
||||||
|
<div className={styles.pagination}>
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
numberOfPages={numberOfPages}
|
||||||
|
onNavigate={onPageChange}
|
||||||
|
hideWhenSinglePage={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<div>
|
</div>
|
||||||
{pageItems.map((currentRule) => (
|
|
||||||
<Card
|
|
||||||
key={currentRule.name}
|
|
||||||
href={createViewLink('grafana', currentRule, '')}
|
|
||||||
className={styles.card}
|
|
||||||
data-testid="alert-card-row"
|
|
||||||
>
|
|
||||||
<Card.Heading>{currentRule.name}</Card.Heading>
|
|
||||||
<Card.Tags>
|
|
||||||
<TagList
|
|
||||||
onClick={onTagClick}
|
|
||||||
tags={Object.keys(currentRule.labels).map((labelKey) => `${labelKey}=${currentRule.labels[labelKey]}`)}
|
|
||||||
/>
|
|
||||||
</Card.Tags>
|
|
||||||
<Card.Meta>
|
|
||||||
<div>
|
|
||||||
<Icon name="folder" /> {folder.title}
|
|
||||||
</div>
|
|
||||||
</Card.Meta>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{showNoResultsText && <div className={styles.noResults}>No alert rules found</div>}
|
|
||||||
<div className={styles.pagination}>
|
|
||||||
<Pagination
|
|
||||||
currentPage={page}
|
|
||||||
numberOfPages={numberOfPages}
|
|
||||||
onNavigate={onPageChange}
|
|
||||||
hideWhenSinglePage={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,18 +150,36 @@ function useAlertsFolderViewFilters() {
|
|||||||
return { nameFilter, labelFilter, setNameFilter, setLabelFilter };
|
return { nameFilter, labelFilter, setNameFilter, setLabelFilter };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterAndSortRules(
|
||||||
|
originalRules: CombinedRule[],
|
||||||
|
nameFilter: string,
|
||||||
|
labelFilter: string,
|
||||||
|
sortOrder: SortOrder
|
||||||
|
) {
|
||||||
|
const matchers = parseMatchers(labelFilter);
|
||||||
|
let rules = originalRules.filter(
|
||||||
|
(rule) => rule.name.toLowerCase().includes(nameFilter.toLowerCase()) && labelsMatchMatchers(rule.labels, matchers)
|
||||||
|
);
|
||||||
|
|
||||||
|
return orderBy(rules, (x) => x.name, [sortOrder === 'alpha-asc' ? 'asc' : 'desc']);
|
||||||
|
}
|
||||||
|
|
||||||
function getNonEmptyStringOrUndefined(value: string | undefined | null) {
|
function getNonEmptyStringOrUndefined(value: string | undefined | null) {
|
||||||
return value || undefined;
|
return value || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
container: css`
|
||||||
|
padding: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
card: css`
|
card: css`
|
||||||
grid-template-columns: auto 1fr 2fr;
|
grid-template-columns: auto 1fr 2fr;
|
||||||
|
margin: 0;
|
||||||
`,
|
`,
|
||||||
pagination: css`
|
pagination: css`
|
||||||
align-self: center;
|
align-self: center;
|
||||||
`,
|
`,
|
||||||
filterInput: css`
|
filterLabelsInput: css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
|
|||||||
Reference in New Issue
Block a user