diff --git a/dev/provisioning/datasources/example.yaml b/dev/provisioning/datasources/example.yaml index d17be313f..05509a9ab 100644 --- a/dev/provisioning/datasources/example.yaml +++ b/dev/provisioning/datasources/example.yaml @@ -5,18 +5,19 @@ # access: proxy # url: https://logs-prod-us-central1.grafana.net # basicAuth: true -# basicAuthUser: -# basicAuthPassword: +# basicAuthUser: # jsonData: # maxLines: 1000 +# secureJsonData: +# basicAuthPassword: # - name: grafanacloud--prom # type: prometheus # access: proxy # url: https://prometheus-us-central1.grafana.net/api/prom # basicAuth: true -# basicAuthUser: +# basicAuthUser: # jsonData: # timeInterval: 1s # secureJsonData: -# basicAuthPassword: +# basicAuthPassword: diff --git a/src/components/CheckFilters.tsx b/src/components/CheckFilters.tsx index c8f43d833..5f4f7374f 100644 --- a/src/components/CheckFilters.tsx +++ b/src/components/CheckFilters.tsx @@ -41,6 +41,18 @@ export const defaultFilters: CheckFiltersType = { probes: [], }; +export const getDefaultFilters = (): CheckFiltersType => { + const storedFilters = localStorage.getItem('checkFilters'); + if (storedFilters) { + try { + return JSON.parse(storedFilters) as CheckFiltersType; + } catch (e) { + return defaultFilters; + } + } + return defaultFilters; +}; + export function CheckFilters({ handleResetFilters, onChange, diff --git a/src/components/CheckList/CheckFilterGroup.tsx b/src/components/CheckList/CheckFilterGroup.tsx index a9b03d65a..ecb0ad2e8 100644 --- a/src/components/CheckList/CheckFilterGroup.tsx +++ b/src/components/CheckList/CheckFilterGroup.tsx @@ -4,7 +4,6 @@ import { Button, Icon, useStyles2 } from '@grafana/ui'; import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { CheckFiltersType } from 'types'; -import { defaultFilters } from 'components/CheckFilters'; const groupStyles = (theme: GrafanaTheme2) => ({ container: css` @@ -58,8 +57,29 @@ const CheckFilterGroup = ({ children, onReset, filters }: Props) => { // Count which filters have been applied Object.keys(filters).forEach((key) => { // Search filter is handled separately - if (key !== 'search' && filters[key] !== defaultFilters[key]) { - active += 1; + switch (key) { + case 'labels': + if (filters.labels.length > 0) { + active += 1; + } + break; + case 'search': + break; + case 'status': + if (filters.status.value !== 0) { + active += 1; + } + break; + case 'probes': + if (filters.probes.length > 0) { + active += 1; + } + break; + case 'type': + if (filters.type !== 'all') { + active += 1; + } + break; } }); setActiveFilters(active); diff --git a/src/components/CheckList/CheckList.test.tsx b/src/components/CheckList/CheckList.test.tsx index 9e2fa1607..9ead7cc45 100644 --- a/src/components/CheckList/CheckList.test.tsx +++ b/src/components/CheckList/CheckList.test.tsx @@ -132,6 +132,10 @@ const renderCheckList = ({ checks = defaultChecks } = {} as RenderChecklist) => ); }; +beforeEach(() => { + localStorage.clear(); +}); + test('renders empty state', async () => { renderCheckList({ checks: [] }); const emptyWarning = await screen.findByText('This account does not currently have any checks configured', { @@ -251,6 +255,88 @@ test('filters by probe', async () => { expect(checks.length).toBe(2); }); +test('loads search from localStorage', async () => { + localStorage.setItem( + 'checkFilters', + JSON.stringify({ + search: 'chimichurri', + labels: [], + type: 'all', + status: { label: 'All', value: 0 }, + probes: [], + }) + ); + renderCheckList(); + const searchInput = await screen.findByPlaceholderText('Search by job name, endpoint, or label'); + expect(searchInput).toHaveValue('chimichurri'); + + const checks = await screen.findAllByTestId('check-card'); + expect(checks.length).toBe(1); +}); + +test('loads status filter from localStorage', async () => { + localStorage.setItem( + 'checkFilters', + JSON.stringify({ + search: '', + labels: [], + type: 'all', + status: { label: 'Disabled', value: 2 }, + probes: [], + }) + ); + renderCheckList(); + const additionalFilters = await screen.findByRole('button', { name: 'Additional Filters (1 active)', exact: false }); + userEvent.click(additionalFilters); + const statusFilter = await screen.findByTestId('check-status-filter'); + expect(statusFilter).toHaveValue('2'); + + const checks = await screen.findAllByTestId('check-card'); + expect(checks.length).toBe(1); +}); + +test('loads type filter from localStorage', async () => { + localStorage.setItem( + 'checkFilters', + JSON.stringify({ + search: '', + labels: [], + type: 'http', + status: { label: 'All', value: 0 }, + probes: [], + }) + ); + renderCheckList(); + const additionalFilters = await screen.findByRole('button', { name: 'Additional Filters (1 active)', exact: false }); + userEvent.click(additionalFilters); + const typeFilter = await screen.findByTestId('check-type-filter'); + expect(typeFilter).toHaveValue('http'); + + const checks = await screen.findAllByTestId('check-card'); + expect(checks.length).toBe(1); +}); + +test('loads labels from localStorage', async () => { + localStorage.setItem( + 'checkFilters', + JSON.stringify({ + search: '', + labels: ['agreat: label'], + type: 'all', + status: { label: 'All', value: 0 }, + probes: [], + }) + ); + renderCheckList(); + const additionalFilters = await screen.findByRole('button', { name: 'Additional Filters (1 active)', exact: false }); + userEvent.click(additionalFilters); + const filterInput = await screen.findByTestId('check-label-filter'); + expect(filterInput).toHaveValue(['agreat: label']); + + const checks = await screen.findAllByTestId('check-card'); + expect(checks.length).toBe(1); +}); + test('clicking type chiclet adds it to filter', async () => { const { user } = renderCheckList(); const additionalFilters = await screen.findByRole('button', { name: 'Additional Filters' }); diff --git a/src/components/CheckList/CheckList.tsx b/src/components/CheckList/CheckList.tsx index daac36691..a6f1247a4 100644 --- a/src/components/CheckList/CheckList.tsx +++ b/src/components/CheckList/CheckList.tsx @@ -7,7 +7,7 @@ import { AppEvents, GrafanaTheme2, OrgRole, SelectableValue } from '@grafana/dat import { config } from '@grafana/runtime'; import { Button, ButtonCascader, Checkbox, Icon, InlineSwitch, Pagination, Select, useStyles2 } from '@grafana/ui'; import { BulkEditModal } from 'components/BulkEditModal'; -import { CheckFilters, defaultFilters } from 'components/CheckFilters'; +import { CheckFilters, defaultFilters, getDefaultFilters } from 'components/CheckFilters'; import { ChecksContextProvider } from 'components/ChecksContextProvider'; import { PluginPage } from 'components/PluginPage'; import { SuccessRateContext, SuccessRateTypes } from 'contexts/SuccessRateContext'; @@ -113,7 +113,7 @@ interface Props { } export const CheckList = ({ instance, checks, onCheckUpdate }: Props) => { - const [checkFilters, setCheckFilters] = useState(defaultFilters); + const [checkFilters, setCheckFilters] = useState(getDefaultFilters()); const [filteredChecks, setFilteredChecks] = useState([]); const [currentPage, setCurrentPage] = useState(1); @@ -177,21 +177,26 @@ export const CheckList = ({ instance, checks, onCheckUpdate }: Props) => { const handleResetFilters = () => { setCheckFilters(defaultFilters); + localStorage.removeItem('checkFilters'); }; const handleLabelSelect = (label: Label) => { setCheckFilters((cf) => { - return { + const updated = { ...cf, labels: [...cf.labels, `${label.name}: ${label.value}`], }; + localStorage.setItem('checkFilters', JSON.stringify(updated)); + return updated; }); setCurrentPage(1); }; const handleTypeSelect = (checkType: CheckType) => { setCheckFilters((cf) => { - return { ...cf, type: checkType }; + const updated = { ...cf, type: checkType }; + localStorage.setItem('checkFilters', JSON.stringify(updated)); + return updated; }); setCurrentPage(1); }; @@ -201,10 +206,12 @@ export const CheckList = ({ instance, checks, onCheckUpdate }: Props) => { const option = CHECK_LIST_STATUS_OPTIONS.find(({ value }) => value === status); if (option) { setCheckFilters((cf) => { - return { + const updated = { ...cf, status: option, }; + localStorage.setItem('checkFilters', JSON.stringify(updated)); + return updated; }); setCurrentPage(1); } @@ -318,6 +325,7 @@ export const CheckList = ({ instance, checks, onCheckUpdate }: Props) => { checkFilters={checkFilters} onChange={(filters: CheckFiltersType) => { setCheckFilters(filters); + localStorage.setItem('checkFilters', JSON.stringify(filters)); }} /> {hasRole(OrgRole.Editor) && ( diff --git a/src/components/CheckList/checkFilters.ts b/src/components/CheckList/checkFilters.ts index b2c0d1dec..e9cfa1a7a 100644 --- a/src/components/CheckList/checkFilters.ts +++ b/src/components/CheckList/checkFilters.ts @@ -33,10 +33,16 @@ const matchesSearchFilter = ({ target, job, labels }: Check, searchFilter: strin }; const matchesLabelFilter = ({ labels }: Check, labelFilters: string[]) => { - if (labelFilters.length === 0) { + if (!labelFilters || labelFilters.length === 0) { return true; } - return labels.some(({ name, value }) => labelFilters.some((filter) => filter === `${name}: ${value}`)); + const result = labels?.some(({ name, value }) => { + const filtersResult = labelFilters.some((filter) => { + return filter === `${name}: ${value}`; + }); + return filtersResult; + }); + return result; }; const matchesStatusFilter = ({ enabled }: Check, { value }: SelectableValue) => { diff --git a/src/components/CheckTestButton.tsx b/src/components/CheckTestButton.tsx index 84b64d5a4..ff0476d95 100644 --- a/src/components/CheckTestButton.tsx +++ b/src/components/CheckTestButton.tsx @@ -1,5 +1,5 @@ import React, { useContext, useMemo, useState } from 'react'; -import { Button, Spinner } from '@grafana/ui'; +import { Alert, Button, Modal, Spinner } from '@grafana/ui'; import { CheckTestResultsModal } from './CheckTestResultsModal'; import { AdHocCheckResponse, Check, CheckFormValues, CheckType } from 'types'; import { useFormContext } from 'react-hook-form'; @@ -14,6 +14,8 @@ interface Props { export function CheckTestButton({ check }: Props) { const [isTestModalOpen, setTestModalOpen] = useState(false); + const [isErrorModalOpen, setErrorModalOpen] = useState(false); + const [error, setError] = useState(''); const [testResponse, setTestResponse] = useState(); const [testRequestInFlight, setTestRequestInFlight] = useState(false); const defaultValues = useMemo(() => getDefaultValuesFromCheck(check), [check]); @@ -25,7 +27,7 @@ export function CheckTestButton({ check }: Props) { + {check?.id && (