Add sorting, improve styles

This commit is contained in:
Konrad Lalik
2022-02-22 15:09:35 +01:00
parent 4ade488494
commit 3c87f43e00
@@ -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;