Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,4 @@
"prettier": "^2.2.1"
},
"proxy": "http://localhost:5000"
}
}
6 changes: 1 addition & 5 deletions src/core/components/FilterDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ export const FilterDisplay: React.FC<Props> = ({ filters, setFilters }) => {
</Grid>
{filters.map((filter, index) => (
<Grid key={`${filter}${index}`} item>
<Chip
onClick={handleClear}
onDelete={() => handleRemoveFilter(filter)}
label={filter}
/>
<Chip onDelete={() => handleRemoveFilter(filter)} label={filter} />
</Grid>
))}
<Grid item>
Expand Down
97 changes: 79 additions & 18 deletions src/core/components/FilterModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,46 @@
// 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;
}

export const FilterModal: React.FC<Props> = ({
anchor,
onClose,
fields,
operators,
addFilter,
}) => {
const classes = useStyles();
const { t } = useTranslation();
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) {
Expand All @@ -60,7 +62,9 @@ export const FilterModal: React.FC<Props> = ({
}, [anchor]);

const handleSubmit = () => {
const filter = `${filterField}${filterOperator}${filterValue}`;
const filter = `${filterField}=${filterNegate ? '!' : ''}${
filterCaseInsensitive ? ':' : ''
}${filterOperator}${filterValue}`;
addFilter(filter);
onClose();
};
Expand All @@ -77,6 +81,38 @@ export const FilterModal: React.FC<Props> = ({
setFilterValue(event.target.value as string);
};

const handleCaseInsensitiveChange = (
event: React.ChangeEvent<HTMLInputElement>,
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 (
<>
<Popover
Expand Down Expand Up @@ -107,8 +143,8 @@ export const FilterModal: React.FC<Props> = ({
</Grid>
</Grid>
<form>
<Grid container>
<Grid item xs={6} className={classes.form}>
<Grid container alignItems="flex-end">
<Grid item xs={12} sm className={classes.form}>
<InputLabel id="field-filter-label">{t('field')}</InputLabel>
<Select
variant="outlined"
Expand All @@ -125,25 +161,39 @@ export const FilterModal: React.FC<Props> = ({
))}
</Select>
</Grid>
<Grid item xs={6} className={classes.form}>
<Grid item xs={12} sm={4} className={classes.form}>
<InputLabel id="operator-filter-label">
{t('operator')}
</InputLabel>
<Select
fullWidth
size="small"
fullWidth
labelId="operator-filter-label"
value={filterOperator}
onChange={handleOperatorChange}
>
{operators.map((operator) => (
{Object.entries(operators).map(([operator, { label }]) => (
<MenuItem key={operator} value={operator}>
{operator}
{`${label} (${operator})`}
</MenuItem>
))}
</Select>
</Grid>
<Grid item xs={12} className={classes.form}>
<Grid item className={classes.form}>
<InputLabel id="field-rule-label">{t('rule')}</InputLabel>
<Select
variant="outlined"
size="small"
labelId="operator-filtruleer-label"
value={String(filterNegate)}
onChange={handleNegateChange}
disabled={!allowModifiers}
>
<MenuItem value={'false'}>{t('matches')}</MenuItem>
<MenuItem value={'true'}>{t('notMatches')}</MenuItem>
</Select>
</Grid>
<Grid item xs={12} sm={9} className={classes.form}>
<InputLabel id="value-filter-label">{t('value')}</InputLabel>
<TextField
fullWidth
Expand All @@ -152,6 +202,18 @@ export const FilterModal: React.FC<Props> = ({
size="small"
/>
</Grid>
<Grid item xs={3} className={classes.form}>
<FormControlLabel
control={
<Checkbox
checked={!filterCaseInsensitive}
onChange={handleCaseInsensitiveChange}
disabled={!allowModifiers}
/>
}
label={t('caseSensitive')}
/>
</Grid>
</Grid>
<Grid
className={classes.buttonContainer}
Expand Down Expand Up @@ -183,7 +245,6 @@ const useStyles = makeStyles((theme) => ({
},
paper: {
outline: 'none',
minWidth: 450,
padding: theme.spacing(2),
},
close: {
Expand Down
13 changes: 11 additions & 2 deletions src/core/components/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<NamespaceContext.Provider
value={{
Expand All @@ -142,7 +149,9 @@ export const Routes: () => JSX.Element = () => {
setCreatedFilter,
}}
>
<SnackbarContext.Provider value={{ setMessage, setMessageType }}>
<SnackbarContext.Provider
value={{ setMessage, setMessageType, reportFetchError }}
>
<MessageSnackbar
{...{ message }}
{...{ setMessage }}
Expand Down
1 change: 1 addition & 0 deletions src/core/contexts/SnackbarContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SnackbarMessageType } from '../components/MessageSnackbar';
export interface ISnackbarContext {
setMessage: Dispatch<SetStateAction<string>>;
setMessageType: Dispatch<SetStateAction<SnackbarMessageType>>;
reportFetchError: (err: any) => void;
}

export const SnackbarContext = createContext({} as ISnackbarContext);
14 changes: 13 additions & 1 deletion src/core/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
44 changes: 31 additions & 13 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> => {
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;
};
13 changes: 6 additions & 7 deletions src/modules/data/views/Data/Data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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
Expand Down Expand Up @@ -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);
});
Expand All @@ -167,6 +166,7 @@ export const Data: () => JSX.Element = () => {
createdFilter,
lastEvent,
filterString,
reportFetchError,
]);

const records: IDataTableRecord[] = dataItems.map((data: IData) => ({
Expand Down Expand Up @@ -261,7 +261,6 @@ export const Data: () => JSX.Element = () => {
setFilterAnchor(null);
}}
fields={filterFields}
operators={filterOperators}
addFilter={handleAddFilter}
/>
)}
Expand Down
Loading