From ff3ad0ee76217943f44ae4ca77d91bcc93dad31a Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 6 Jan 2022 17:51:13 -0500 Subject: [PATCH] Support filters per FIR-5 and add error snackbars Signed-off-by: Peter Broadhurst --- package-lock.json | 2 +- package.json | 2 +- src/core/components/FilterDisplay.tsx | 6 +- src/core/components/FilterModal.tsx | 97 +++++++++++++++---- src/core/components/Routes.tsx | 13 ++- src/core/contexts/SnackbarContext.tsx | 1 + src/core/translations/en.json | 14 ++- src/core/utils.ts | 44 ++++++--- src/modules/data/views/Data/Data.tsx | 13 ++- src/modules/data/views/Events/Events.tsx | 34 ++++--- src/modules/data/views/Messages/Messages.tsx | 2 - .../data/views/Operations/Operations.tsx | 13 ++- .../data/views/Transactions/Transactions.tsx | 2 - src/modules/data/views/Types/Types.tsx | 13 ++- 14 files changed, 174 insertions(+), 82 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4fb6ae68..449f9c27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firefly-ui", - "version": "0.4.0", + "version": "0.4.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c67359da..0a03bd91 100644 --- a/package.json +++ b/package.json @@ -74,4 +74,4 @@ "prettier": "^2.2.1" }, "proxy": "http://localhost:5000" -} \ No newline at end of file +} diff --git a/src/core/components/FilterDisplay.tsx b/src/core/components/FilterDisplay.tsx index b4ecf897..1bc39eb2 100644 --- a/src/core/components/FilterDisplay.tsx +++ b/src/core/components/FilterDisplay.tsx @@ -46,11 +46,7 @@ export const FilterDisplay: React.FC = ({ filters, setFilters }) => { {filters.map((filter, index) => ( - handleRemoveFilter(filter)} - label={filter} - /> + handleRemoveFilter(filter)} label={filter} /> ))} diff --git a/src/core/components/FilterModal.tsx b/src/core/components/FilterModal.tsx index e48b68a7..3cb96472 100644 --- a/src/core/components/FilterModal.tsx +++ b/src/core/components/FilterModal.tsx @@ -14,28 +14,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useState, useEffect } from 'react'; import { + Button, + Checkbox, + FormControlLabel, Grid, + InputLabel, + MenuItem, Paper, - Typography, Popover, - Button, - InputLabel, Select, - MenuItem, - TextField, SelectChangeEvent, + TextField, + Typography, } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; import CloseIcon from 'mdi-react/CloseIcon'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; interface Props { anchor: HTMLButtonElement | null; onClose: () => void; fields: string[]; - operators: string[]; addFilter: (filter: string) => void; } @@ -43,7 +44,6 @@ export const FilterModal: React.FC = ({ anchor, onClose, fields, - operators, addFilter, }) => { const classes = useStyles(); @@ -51,7 +51,9 @@ export const FilterModal: React.FC = ({ const [open, setOpen] = useState(false); const [filterValue, setFilterValue] = useState(''); const [filterField, setFilterField] = useState(''); - const [filterOperator, setFilterOperator] = useState(''); + const [filterOperator, setFilterOperator] = useState('='); + const [filterCaseInsensitive, setFilterCaseInsensitive] = useState(false); + const [filterNegate, setFilterNegate] = useState(false); useEffect(() => { if (anchor) { @@ -60,7 +62,9 @@ export const FilterModal: React.FC = ({ }, [anchor]); const handleSubmit = () => { - const filter = `${filterField}${filterOperator}${filterValue}`; + const filter = `${filterField}=${filterNegate ? '!' : ''}${ + filterCaseInsensitive ? ':' : '' + }${filterOperator}${filterValue}`; addFilter(filter); onClose(); }; @@ -77,6 +81,38 @@ export const FilterModal: React.FC = ({ setFilterValue(event.target.value as string); }; + const handleCaseInsensitiveChange = ( + event: React.ChangeEvent, + checked: boolean + ) => { + setFilterCaseInsensitive(!checked); + }; + + const handleNegateChange = (event: SelectChangeEvent) => { + setFilterNegate(event.target.value === 'true'); + }; + + const operators: { [op: string]: { label: string; modifiers: boolean } } = { + '=': { label: t('equal'), modifiers: true }, + '>>': { label: t('greaterThan'), modifiers: false }, + '>=': { label: t('greaterThanOrEqual'), modifiers: false }, + '<<': { label: t('lessThan'), modifiers: false }, + '<=': { label: t('lessThanOrEqual'), modifiers: false }, + '@': { label: t('contains'), modifiers: true }, + '^': { label: t('startsWith'), modifiers: true }, + $: { label: t('endsWith'), modifiers: true }, + }; + + const operatorSettings = operators[filterOperator]; + const allowModifiers = operatorSettings?.modifiers || false; + + useEffect(() => { + if (!allowModifiers) { + setFilterNegate(false); + setFilterCaseInsensitive(false); + } + }, [allowModifiers]); + return ( <> = ({
- - + + {t('field')} - + {t('operator')} - + + {t('rule')} + + + {t('value')} = ({ size="small" /> + + + } + label={t('caseSensitive')} + /> + ({ }, paper: { outline: 'none', - minWidth: 450, padding: theme.spacing(2), }, close: { diff --git a/src/core/components/Routes.tsx b/src/core/components/Routes.tsx index 03967a1a..0ddf9f29 100644 --- a/src/core/components/Routes.tsx +++ b/src/core/components/Routes.tsx @@ -31,7 +31,7 @@ import { import { NamespaceContext } from '../contexts/NamespaceContext'; import { ApplicationContext } from '../contexts/ApplicationContext'; import { NavWrapper } from './NavWrapper'; -import { fetchWithCredentials } from '../utils'; +import { fetchWithCredentials, summarizeFetchError } from '../utils'; import { CircularProgress } from '@mui/material'; import { SnackbarContext } from '../contexts/SnackbarContext'; import { MessageSnackbar, SnackbarMessageType } from './MessageSnackbar'; @@ -121,6 +121,13 @@ export const Routes: () => JSX.Element = () => { const routes: IRoute[] = registerModuleRoutes(); + const reportFetchError = (err: any) => { + summarizeFetchError(err).then((message: string) => { + setMessageType('error'); + setMessage(message); + }); + }; + return ( JSX.Element = () => { setCreatedFilter, }} > - + >; setMessageType: Dispatch>; + reportFetchError: (err: any) => void; } export const SnackbarContext = createContext({} as ISnackbarContext); diff --git a/src/core/translations/en.json b/src/core/translations/en.json index 27bff263..c517195f 100644 --- a/src/core/translations/en.json +++ b/src/core/translations/en.json @@ -16,5 +16,17 @@ "field": "Field", "value": "Value", "selectedFilters": "Selected Filters", - "clear": "Clear" + "clear": "Clear", + "equal": "Equals", + "greaterThan": "Greater than", + "greaterThanOrEqual": "Greater than or equal", + "lessThan": "Less than", + "lessThanOrEqual": "Less than or equal", + "contains": "Contains", + "startsWith": "Starts with", + "endsWith": "Ends with", + "caseSensitive": "Case sensitive", + "rule": "Rule", + "matches": "Matches", + "notMatches": "Does not match" } diff --git a/src/core/utils.ts b/src/core/utils.ts index 5840dfee..2514311b 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -74,16 +74,34 @@ export const jsNumberForAddress = (address: string): number => { return seed; }; -// https://github.com/hyperledger/firefly/blob/04cd7184e0562a3a5a5344b0430bf68cc76415b1/internal/apiserver/restfilter.go#L126 -export const filterOperators = [ - '=', - '>', - '>=', - '<', - '<=', - '@', - '^', - '!', - '!@', - '!^', -]; +export const summarizeFetchError = async ( + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + errOrResponse: any +): Promise => { + console.log('Fetch error', errOrResponse); + let message = 'Fetch failed'; + if (errOrResponse.status) { + message += ` [${errOrResponse.status}]`; + } + if (errOrResponse.message) { + message += `: ${errOrResponse.message}`; + } + if (typeof errOrResponse.json === 'function') { + let jsonData: any; + try { + jsonData = await errOrResponse.json(); + } catch (err1) { + console.log('Failed to parse response as JSON: ' + err1); + } + if (jsonData?.error) { + message += `: ${jsonData.error}`; + } else { + try { + message += `: ${await errOrResponse.text()}`; + } catch (err2) { + console.log('Failed to get response as text: ' + err2); + } + } + } + return message; +}; diff --git a/src/modules/data/views/Data/Data.tsx b/src/modules/data/views/Data/Data.tsx index 0196d8b2..8ceeba58 100644 --- a/src/modules/data/views/Data/Data.tsx +++ b/src/modules/data/views/Data/Data.tsx @@ -34,16 +34,13 @@ import { FilterModal } from '../../../../core/components/FilterModal'; import { HashPopover } from '../../../../core/components/HashPopover'; import { ApplicationContext } from '../../../../core/contexts/ApplicationContext'; import { NamespaceContext } from '../../../../core/contexts/NamespaceContext'; +import { SnackbarContext } from '../../../../core/contexts/SnackbarContext'; import { ICreatedFilter, IData, IDataTableRecord, } from '../../../../core/interfaces'; -import { - fetchWithCredentials, - filterOperators, - getCreatedFilter, -} from '../../../../core/utils'; +import { fetchWithCredentials, getCreatedFilter } from '../../../../core/utils'; import { useDataTranslation } from '../../registration'; import { DataDetails } from './DataDetails'; @@ -68,6 +65,7 @@ export const Data: () => JSX.Element = () => { 'filters', withDefault(ArrayParam, []) ); + const { reportFetchError } = useContext(SnackbarContext); useEffect(() => { // set filters if they are present in the URL @@ -154,9 +152,10 @@ export const Data: () => JSX.Element = () => { if (response.ok) { setDataItems(await response.json()); } else { - console.log('error fetching data'); + reportFetchError(response); } }) + .catch((err) => reportFetchError(err)) .finally(() => { setLoading(false); }); @@ -167,6 +166,7 @@ export const Data: () => JSX.Element = () => { createdFilter, lastEvent, filterString, + reportFetchError, ]); const records: IDataTableRecord[] = dataItems.map((data: IData) => ({ @@ -261,7 +261,6 @@ export const Data: () => JSX.Element = () => { setFilterAnchor(null); }} fields={filterFields} - operators={filterOperators} addFilter={handleAddFilter} /> )} diff --git a/src/modules/data/views/Events/Events.tsx b/src/modules/data/views/Events/Events.tsx index edbbd0b7..8a7e290c 100644 --- a/src/modules/data/views/Events/Events.tsx +++ b/src/modules/data/views/Events/Events.tsx @@ -36,6 +36,7 @@ import { FilterModal } from '../../../../core/components/FilterModal'; import { HashPopover } from '../../../../core/components/HashPopover'; import { ApplicationContext } from '../../../../core/contexts/ApplicationContext'; import { NamespaceContext } from '../../../../core/contexts/NamespaceContext'; +import { SnackbarContext } from '../../../../core/contexts/SnackbarContext'; import { FFColors, ICreatedFilter, @@ -43,11 +44,7 @@ import { IEvent, IMetric, } from '../../../../core/interfaces'; -import { - fetchWithCredentials, - filterOperators, - getCreatedFilter, -} from '../../../../core/utils'; +import { fetchWithCredentials, getCreatedFilter } from '../../../../core/utils'; import { useDataTranslation } from '../../registration'; const PAGE_LIMITS = [10, 25]; @@ -71,6 +68,7 @@ export const Events: () => JSX.Element = () => { 'filters', withDefault(ArrayParam, []) ); + const { reportFetchError } = useContext(SnackbarContext); useEffect(() => { // set filters if they are present in the URL @@ -160,9 +158,10 @@ export const Events: () => JSX.Element = () => { const eventsJson: IEvent[] = await eventsResponse.json(); setEventItems(eventsJson); } else { - console.log('error fetching data'); + reportFetchError(eventsResponse); } }) + .catch((err) => reportFetchError(err)) .finally(() => { setLoading(false); }); @@ -173,6 +172,7 @@ export const Events: () => JSX.Element = () => { createdFilter, lastEvent, filterString, + reportFetchError, ]); // Chart @@ -182,15 +182,18 @@ export const Events: () => JSX.Element = () => { fetchWithCredentials( `/api/v1/namespaces/${selectedNamespace}/charts/histogram/events?startTime=${createdFilterObject.filterTime}&endTime=${currentTime}&buckets=100` - ).then(async (eventsMetricsResponse) => { - if (eventsMetricsResponse.ok) { - const eventsMetricsJson: IMetric[] = await eventsMetricsResponse.json(); - setEventMetrics(eventsMetricsJson); - } else { - console.log('error fetching data'); - } - }); - }, [selectedNamespace, createdFilter]); + ) + .then(async (eventsMetricsResponse) => { + if (eventsMetricsResponse.ok) { + const eventsMetricsJson: IMetric[] = + await eventsMetricsResponse.json(); + setEventMetrics(eventsMetricsJson); + } else { + reportFetchError(eventsMetricsResponse); + } + }) + .catch((err) => reportFetchError(err)); + }, [selectedNamespace, createdFilter, reportFetchError]); const records: IDataTableRecord[] = eventItems.map((data: IEvent) => ({ key: data.id, @@ -307,7 +310,6 @@ export const Events: () => JSX.Element = () => { setFilterAnchor(null); }} fields={filterFields} - operators={filterOperators} addFilter={handleAddFilter} /> )} diff --git a/src/modules/data/views/Messages/Messages.tsx b/src/modules/data/views/Messages/Messages.tsx index 47c34e7d..423f35ef 100644 --- a/src/modules/data/views/Messages/Messages.tsx +++ b/src/modules/data/views/Messages/Messages.tsx @@ -25,7 +25,6 @@ import { FilterDisplay } from '../../../../core/components/FilterDisplay'; import { FilterModal } from '../../../../core/components/FilterModal'; import { ApplicationContext } from '../../../../core/contexts/ApplicationContext'; import { IHistory, IMessage } from '../../../../core/interfaces'; -import { filterOperators } from '../../../../core/utils'; import { useDataTranslation } from '../../registration'; import { MessageList } from './MessageList'; import { MessageTimeline } from './MessageTimeline'; @@ -157,7 +156,6 @@ export const Messages: () => JSX.Element = () => { setFilterAnchor(null); }} fields={filterFields} - operators={filterOperators} addFilter={handleAddFilter} /> )} diff --git a/src/modules/data/views/Operations/Operations.tsx b/src/modules/data/views/Operations/Operations.tsx index 9ac17fee..2b4d586a 100644 --- a/src/modules/data/views/Operations/Operations.tsx +++ b/src/modules/data/views/Operations/Operations.tsx @@ -39,6 +39,7 @@ import { FilterModal } from '../../../../core/components/FilterModal'; import { HashPopover } from '../../../../core/components/HashPopover'; import { ApplicationContext } from '../../../../core/contexts/ApplicationContext'; import { NamespaceContext } from '../../../../core/contexts/NamespaceContext'; +import { SnackbarContext } from '../../../../core/contexts/SnackbarContext'; import { FFColors, ICreatedFilter, @@ -47,11 +48,7 @@ import { IOperation, OperationStatus, } from '../../../../core/interfaces'; -import { - fetchWithCredentials, - filterOperators, - getCreatedFilter, -} from '../../../../core/utils'; +import { fetchWithCredentials, getCreatedFilter } from '../../../../core/utils'; import { useDataTranslation } from '../../registration'; const PAGE_LIMITS = [10, 25]; @@ -75,6 +72,7 @@ export const Operations: () => JSX.Element = () => { 'filters', withDefault(ArrayParam, []) ); + const { reportFetchError } = useContext(SnackbarContext); useEffect(() => { // set filters if they are present in the URL @@ -165,9 +163,10 @@ export const Operations: () => JSX.Element = () => { const opsJson: IOperation[] = await opsResponse.json(); setOperationItems(opsJson); } else { - console.log('error fetching data'); + reportFetchError(opsResponse); } }) + .catch((err) => reportFetchError(err)) .finally(() => { setLoading(false); }); @@ -178,6 +177,7 @@ export const Operations: () => JSX.Element = () => { createdFilter, lastEvent, filterString, + reportFetchError, ]); // Chart @@ -318,7 +318,6 @@ export const Operations: () => JSX.Element = () => { setFilterAnchor(null); }} fields={filterFields} - operators={filterOperators} addFilter={handleAddFilter} /> )} diff --git a/src/modules/data/views/Transactions/Transactions.tsx b/src/modules/data/views/Transactions/Transactions.tsx index 4636fd71..d425dd65 100644 --- a/src/modules/data/views/Transactions/Transactions.tsx +++ b/src/modules/data/views/Transactions/Transactions.tsx @@ -25,7 +25,6 @@ import { useDataTranslation } from '../../registration'; import { FilterDisplay } from '../../../../core/components/FilterDisplay'; import { ArrayParam, withDefault, useQueryParam } from 'use-query-params'; import { FilterModal } from '../../../../core/components/FilterModal'; -import { filterOperators } from '../../../../core/utils'; import { DatePicker } from '../../../../core/components/DatePicker'; export const Transactions: () => JSX.Element = () => { @@ -135,7 +134,6 @@ export const Transactions: () => JSX.Element = () => { setFilterAnchor(null); }} fields={filterFields} - operators={filterOperators} addFilter={handleAddFilter} /> )} diff --git a/src/modules/data/views/Types/Types.tsx b/src/modules/data/views/Types/Types.tsx index 652da764..f4095200 100644 --- a/src/modules/data/views/Types/Types.tsx +++ b/src/modules/data/views/Types/Types.tsx @@ -37,14 +37,11 @@ import { IDataTableRecord, IDataType, } from '../../../../core/interfaces'; -import { - fetchWithCredentials, - filterOperators, - getCreatedFilter, -} from '../../../../core/utils'; +import { fetchWithCredentials, getCreatedFilter } from '../../../../core/utils'; import { useDataTranslation } from '../../registration'; import { DatePicker } from '../../../../core/components/DatePicker'; import { DataTableEmptyState } from '../../../../core/components/DataTable/DataTableEmptyState'; +import { SnackbarContext } from '../../../../core/contexts/SnackbarContext'; const PAGE_LIMITS = [10, 25]; @@ -66,6 +63,7 @@ export const Types: () => JSX.Element = () => { 'filters', withDefault(ArrayParam, []) ); + const { reportFetchError } = useContext(SnackbarContext); useEffect(() => { // set filters if they are present in the URL @@ -155,9 +153,10 @@ export const Types: () => JSX.Element = () => { const dataTypes: IDataType[] = await response.json(); setDataTypeItems(dataTypes); } else { - console.log('error fetching data'); + reportFetchError(response); } }) + .catch((err) => reportFetchError(err)) .finally(() => { setLoading(false); }); @@ -168,6 +167,7 @@ export const Types: () => JSX.Element = () => { createdFilter, lastEvent, filterString, + reportFetchError, ]); const records: IDataTableRecord[] = dataTypeItems.map((data: IDataType) => ({ @@ -268,7 +268,6 @@ export const Types: () => JSX.Element = () => { setFilterAnchor(null); }} fields={filterFields} - operators={filterOperators} addFilter={handleAddFilter} /> )}