Skip to content

Commit

Permalink
feat(match-expression): match expression should be evaluated on backend
Browse files Browse the repository at this point in the history
Signed-off-by: Thuan Vo <thvo@redhat.com>
  • Loading branch information
Thuan Vo committed Jul 27, 2023
1 parent 9ed8cb1 commit 18b0558
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 177 deletions.
32 changes: 13 additions & 19 deletions src/app/Rules/CreateRule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { ServiceContext } from '@app/Shared/Services/Services';
import { Target } from '@app/Shared/Services/Target.service';
import { SearchExprService, SearchExprServiceContext } from '@app/Topology/Shared/utils';
import { useSubscriptions } from '@app/utils/useSubscriptions';
import { evaluateTargetWithExpr, portalRoot } from '@app/utils/utils';
import { portalRoot } from '@app/utils/utils';
import {
ActionGroup,
Button,
Expand Down Expand Up @@ -286,26 +286,20 @@ const CreateRuleForm: React.FC<CreateRuleFormProps> = ({ ...props }) => {
}, [addSubscription, context.targets, setTargets]);

React.useEffect(() => {
// Set validations
let validation: ValidatedOptions = ValidatedOptions.default;
let matches: Target[] = [];
if (matchExpression !== '' && targets.length > 0) {
try {
matches = targets.filter((t) => {
const res = evaluateTargetWithExpr(t, matchExpression);
if (typeof res === 'boolean') {
return res;
}
throw new Error('The expression matching failed.');
});
validation = matches.length ? ValidatedOptions.success : ValidatedOptions.warning;
} catch (err) {
validation = ValidatedOptions.error;
}
addSubscription(
context.api.matchTargetsWithExpr(matchExpression, targets).subscribe({
next: (ts) => {
setMatchExpressionValid(ts.length ? ValidatedOptions.success : ValidatedOptions.warning);
matchedTargets.next(ts);
},
error: (_) => {
setMatchExpressionValid(ValidatedOptions.error);
},
})
);
}
setMatchExpressionValid(validation);
matchedTargets.next(matches);
}, [matchExpression, targets, matchedTargets, setMatchExpressionValid]);
}, [matchExpression, targets, matchedTargets, context.api, setMatchExpressionValid, addSubscription]);

const createButtonLoadingProps = React.useMemo(
() =>
Expand Down
28 changes: 12 additions & 16 deletions src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { ServiceContext } from '@app/Shared/Services/Services';
import { Target } from '@app/Shared/Services/Target.service';
import { SearchExprService, SearchExprServiceContext } from '@app/Topology/Shared/utils';
import { useSubscriptions } from '@app/utils/useSubscriptions';
import { evaluateTargetWithExpr, portalRoot, StreamOf } from '@app/utils/utils';
import { portalRoot, StreamOf } from '@app/utils/utils';
import {
Button,
Card,
Expand Down Expand Up @@ -170,23 +170,19 @@ export const AuthForm: React.FC<AuthFormProps> = ({ onDismiss, onPropsSave, prog
}, [addSubscription, context.targets, setTargets]);

React.useEffect(() => {
let validation: ValidatedOptions = ValidatedOptions.default;
if (matchExpression !== '' && targets.length > 0) {
try {
const atLeastOne = targets.some((t) => {
const res = evaluateTargetWithExpr(t, matchExpression);
if (typeof res === 'boolean') {
return res;
}
throw new Error('The expression matching failed.');
});
validation = atLeastOne ? ValidatedOptions.success : ValidatedOptions.warning;
} catch (err) {
validation = ValidatedOptions.error;
}
addSubscription(
context.api.matchTargetsWithExpr(matchExpression, targets).subscribe({
next: (ts) => {
setMatchExpressionValid(ts.length ? ValidatedOptions.success : ValidatedOptions.warning);
},
error: (_) => {
setMatchExpressionValid(ValidatedOptions.error);
},
})
);
}
setMatchExpressionValid(validation);
}, [matchExpression, targets, setMatchExpressionValid]);
}, [matchExpression, targets, context.api, setMatchExpressionValid, addSubscription]);

React.useEffect(() => {
progressChange && progressChange(saving);
Expand Down
86 changes: 53 additions & 33 deletions src/app/SecurityPanel/Credentials/CredentialTestTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@
*/
import { LinearDotSpinner } from '@app/Shared/LinearDotSpinner';
import { ServiceContext } from '@app/Shared/Services/Services';
import { Target } from '@app/Shared/Services/Target.service';
import { Target, includesTarget } from '@app/Shared/Services/Target.service';
import { useSearchExpression } from '@app/Topology/Shared/utils';
import { useSort } from '@app/utils/useSort';
import { useSubscriptions } from '@app/utils/useSubscriptions';
import { TableColumn, evaluateTargetWithExpr, portalRoot, sortResources } from '@app/utils/utils';
import { TableColumn, portalRoot, sortResources } from '@app/utils/utils';
import {
Bullseye,
Button,
Expand Down Expand Up @@ -103,53 +103,73 @@ export const CredentialTestTable: React.FC<CredentialTestTableProps> = ({ ...pro
const [matchExpression] = useSearchExpression();
const [sortBy, getSortParams] = useSort();

const [targets, setTargets] = React.useState<Target[]>([]);
const [targets, setTargets] = React.useState<{ target: Target; matched: boolean }[]>([]);
const [filters, setFilters] = React.useState<CredentialTestState[]>([]);
const [searchText, setSearchText] = React.useState('');
const [loading, setLoading] = React.useState(false);
const [err, setErr] = React.useState<Error>();

React.useEffect(() => {
addSubscription(context.targets.targets().subscribe(setTargets));
addSubscription(
context.targets.targets().subscribe((targets) => {
setTargets(targets.map((t) => ({ target: t, matched: false })));
})
);
}, [addSubscription, context.targets, setTargets]);

const matchedTargets = React.useMemo(() => {
try {
return targets.filter((t) => {
const res = evaluateTargetWithExpr(t, matchExpression);
if (typeof res === 'boolean') {
return res;
}
throw new Error('Invalid match expression');
});
} catch (err) {
return [];
React.useEffect(() => {
if (!targets.length) {
return;
} else if (!matchExpression) {
setTargets((ts) => ts.map((t) => ({ ...t, matched: false })));
} else {
setLoading(true);
addSubscription(
context.api
.matchTargetsWithExpr(
matchExpression,
targets.map((t) => t.target)
)
.subscribe({
next: (ts) => {
setLoading(false);
const matched = targets.filter((t) => includesTarget(ts, t.target)).map((t) => ({ ...t, matched: true }));
// Matched targets are by default pushed to top
setTargets([...matched, ...targets.filter((t) => !includesTarget(ts, t.target))]);
},
error: (err: Error) => {
setLoading(false);
setErr(err);
},
})
);
}
}, [targets, matchExpression]);
}, [targets, matchExpression, context.api, addSubscription]);

const rows = React.useMemo(
() =>
sortResources(
{
index: sortBy.index ?? 0,
direction: sortBy.direction ?? SortByDirection.asc,
},
matchedTargets,
tableColumns
).map((t) => <CredentialTestRow target={t} key={t.connectUrl} filters={filters} searchText={searchText} />),
[matchedTargets, filters, searchText, sortBy]
);
const rows = React.useMemo(() => {
const matchedTargets = targets.filter((t) => t.matched).map((t) => t.target);
return sortResources(
{
index: sortBy.index ?? 0,
direction: sortBy.direction ?? SortByDirection.asc,
},
matchedTargets,
tableColumns
).map((t) => <CredentialTestRow target={t} key={t.connectUrl} filters={filters} searchText={searchText} />);
}, [targets, filters, searchText, sortBy]);

const toolbar = React.useMemo(
() => (
const toolbar = React.useMemo(() => {
const matchedTargets = targets.filter((t) => t.matched).map((t) => t.target);
return (
<CredentialToolbar
onFilter={setFilters}
onSearch={setSearchText}
filters={filters}
searchText={searchText}
matchedTargets={matchedTargets}
/>
),
[setFilters, setSearchText, filters, searchText, matchedTargets]
);
);
}, [setFilters, setSearchText, filters, searchText, targets]);

return rows.length ? (
<OuterScrollContainer>
Expand Down
56 changes: 19 additions & 37 deletions src/app/SecurityPanel/Credentials/StoreCredentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,9 @@ import { DeleteOrDisableWarningType } from '@app/Modal/DeleteWarningUtils';
import { StoredCredential } from '@app/Shared/Services/Api.service';
import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service';
import { ServiceContext } from '@app/Shared/Services/Services';
import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service';
import { useSort } from '@app/utils/useSort';
import { useSubscriptions } from '@app/utils/useSubscriptions';
import { TableColumn, evaluateTargetWithExpr, sortResources } from '@app/utils/utils';
import { TableColumn, sortResources } from '@app/utils/utils';
import {
Badge,
Button,
Expand All @@ -66,14 +65,14 @@ import { ExpandableRowContent, TableComposable, Tbody, Td, Th, Thead, Tr } from
import _ from 'lodash';
import * as React from 'react';
import { Link } from 'react-router-dom';
import { concatMap, forkJoin, Observable, of } from 'rxjs';
import { forkJoin, Observable } from 'rxjs';
import { SecurityCard } from '../SecurityPanel';
import { CreateCredentialModal } from './CreateCredentialModal';
import { MatchedTargetsTable } from './MatchedTargetsTable';

const enum Actions {
HANDLE_REFRESH,
HANDLE_TARGET_NOTIFICATION,
HANDLE_MATCHED_TARGET_NOTIFICATION,
HANDLE_CREDENTIALS_STORED_NOTIFICATION,
HANDLE_CREDENTIALS_DELETED_NOTIFICATION,
HANDLE_ROW_CHECK,
Expand All @@ -99,27 +98,15 @@ const reducer = (state: State, action) => {
credentials: credentials,
};
}
case Actions.HANDLE_TARGET_NOTIFICATION: {
case Actions.HANDLE_MATCHED_TARGET_NOTIFICATION: {
return {
...state,
credentials: state.credentials.map((credential) => {
let matched = false;
try {
const res = evaluateTargetWithExpr(action.payload.target, credential.matchExpression);
if (typeof res === 'boolean') {
matched = res;
}
} catch (_error) {
matched = false;
}
if (matched) {
const delta = action.payload.kind === 'FOUND' ? 1 : -1;
return {
...credential,
numMatchingTargets: credential.numMatchingTargets + delta,
};
}
return credential;
const delta = action.payload.kind === 'FOUND' ? 1 : -1;
return {
...credential,
numMatchingTargets: credential.numMatchingTargets + delta,
};
}),
};
}
Expand Down Expand Up @@ -268,31 +255,26 @@ export const StoreCredentials = () => {
addSubscription(
context.notificationChannel
.messages(NotificationCategory.CredentialsDeleted)
.pipe(
concatMap((v) =>
of(dispatch({ type: Actions.HANDLE_CREDENTIALS_DELETED_NOTIFICATION, payload: { credential: v.message } }))
)
.subscribe((v) =>
dispatch({ type: Actions.HANDLE_CREDENTIALS_DELETED_NOTIFICATION, payload: { credential: v.message } })
)
.subscribe(() => undefined /* do nothing - dispatch will have already handled updating state */)
);
}, [addSubscription, context, context.notificationChannel]);

const handleTargetNotification = (evt: TargetDiscoveryEvent) => {
dispatch({ type: Actions.HANDLE_TARGET_NOTIFICATION, payload: { target: evt.serviceRef, kind: evt.kind } });
};

React.useEffect(() => {
addSubscription(
context.notificationChannel
.messages(NotificationCategory.TargetJvmDiscovery)
.pipe(concatMap((v) => of(handleTargetNotification(v.message.event))))
.subscribe(() => undefined /* do nothing - dispatch will have already handled updating state */)
.subscribe((_) => refreshStoredCredentialsAndCounts())
);
}, [addSubscription, context, context.notificationChannel]);
}, [addSubscription, refreshStoredCredentialsAndCounts, context, context.notificationChannel, context.api]);

const handleHeaderCheck = React.useCallback((checked: boolean) => {
dispatch({ type: Actions.HANDLE_HEADER_CHECK, payload: { checked: checked } });
}, []);
const handleHeaderCheck = React.useCallback(
(checked: boolean) => {
dispatch({ type: Actions.HANDLE_HEADER_CHECK, payload: { checked: checked } });
},
[dispatch]
);

const handleDeleteCredentials = React.useCallback(() => {
const tasks: Observable<boolean>[] = [];
Expand Down
Loading

0 comments on commit 18b0558

Please sign in to comment.