Skip to content

Commit

Permalink
feat(credentials): nested match expressions table (#495)
Browse files Browse the repository at this point in the history
* Add nested table

* fixup! Add nested table

* Start adding support for matching target counts

* Use notifications to update counts

* Finish using notifications to update count

* Start fixing broken notification state updating

* Continue troubleshooting

* Continue troubleshooting

* Fix notification-based state updating

* Refactoring. Cleanup target notification handling in the AllTargetsArchivedRecordingsTable as well

* fixup! Refactoring. Cleanup target notification handling in the AllTargetsArchivedRecordingsTable as well

* Continue refactoring

* Fix header check delete

* Resolve issue where deleting multiple credentials at once doesn't properly update the view

* Start updating tests

* Finish updating tests

* Refactoring

* Start converting from useState to useReducer

* Continue converting to useReducer

* Finish converting to useReducer

* Update tests

* Ensure synchronous target notification handling in the AllTargetsArchivedRecordingsTable
  • Loading branch information
Hareet Dhillon authored Aug 15, 2022
1 parent 756d9ea commit e871759
Show file tree
Hide file tree
Showing 8 changed files with 800 additions and 194 deletions.
73 changes: 43 additions & 30 deletions src/app/Archives/AllTargetsArchivedRecordingsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { TableComposable, Th, Thead, Tbody, Tr, Td, ExpandableRowContent } from
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
import { ArchivedRecordingsTable } from '@app/Recordings/ArchivedRecordingsTable';
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
import { concatMap, map } from 'rxjs/operators';
import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service';
import { LoadingView } from '@app/LoadingView/LoadingView';
import _ from 'lodash';
Expand All @@ -72,17 +72,15 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent<AllTarge
];

const updateCount = React.useCallback((connectUrl: string, delta: number) => {
let idx = 0;
for (const t of targets) {
if (t.connectUrl === connectUrl) {
for(let i = 0; i < targets.length; i++) {
if(targets[i].connectUrl === connectUrl) {
setCounts(old => {
let updated = [...old];
updated[idx] += delta;
updated[i] += delta;
return updated;
});
break;
}
idx++;
}
}, [targets, setCounts]);

Expand Down Expand Up @@ -146,14 +144,43 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent<AllTarge
);
},[addSubscription, context, context.api, setCounts]);

React.useEffect(() => {
searchedTargetsRef.current = searchedTargets;
});
const handleLostTarget = React.useCallback((target: Target) => {
let idx;
setTargets(old => {
for (idx = 0; idx < old.length; idx++) {
if (_.isEqual(target, old[idx])) break;
}
return old.filter(o => !_.isEqual(o, target));
});
setExpandedTargets(old => old.filter(o => !_.isEqual(o, target)));
setCounts(old => {
let updated = [...old];
updated.splice(idx, 1);
return updated;
});
}, [setTargets, setExpandedTargets, setCounts]);

const handleTargetNotification = React.useCallback((evt: TargetDiscoveryEvent) => {
const target: Target = {
connectUrl: evt.serviceRef.connectUrl,
alias: evt.serviceRef.alias,
}
if (evt.kind === 'FOUND') {
setTargets(old => old.concat(target));
getCountForNewTarget(target);
} else if (evt.kind === 'LOST') {
handleLostTarget(target);
}
}, [setTargets, getCountForNewTarget, handleLostTarget]);

React.useEffect(() => {
refreshTargetsAndCounts();
}, []);

React.useEffect(() => {
searchedTargetsRef.current = searchedTargets;
});

React.useEffect(() => {
if (!context.settings.autoRefreshEnabled()) {
return;
Expand All @@ -172,31 +199,17 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent<AllTarge
}

if (!_.isEqual(searchedTargetsRef.current, updatedSearchedTargets)) {
setSearchedTargets([...updatedSearchedTargets]);
setSearchedTargets(updatedSearchedTargets);
}
}, [search, targets]);

React.useEffect(() => {
addSubscription(
context.notificationChannel.messages(NotificationCategory.TargetJvmDiscovery)
.subscribe(v => {
const evt: TargetDiscoveryEvent = v.message.event;
const target: Target = {
connectUrl: evt.serviceRef.connectUrl,
alias: evt.serviceRef.alias,
}
if (evt.kind === 'FOUND') {
setTargets(old => old.concat(target));
getCountForNewTarget(target);
} else if (evt.kind === 'LOST') {
const idx = targets.indexOf(target);
setTargets(old => old.filter(o => o.connectUrl != target.connectUrl));
setExpandedTargets(old => old.filter(o => o != target));
setCounts(old => old.splice(idx, 1));
}
})
.pipe(concatMap(v => of(handleTargetNotification(v.message.event))))
.subscribe(() => {} /* do nothing - callback will have already handled updating state */)
);
}, [addSubscription, context, context.notificationChannel, getCountForNewTarget, setTargets, setCounts]);
}, [addSubscription, context, context.notificationChannel, handleTargetNotification]);

React.useEffect(() => {
addSubscription(
Expand Down Expand Up @@ -265,7 +278,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent<AllTarge
</Tr>
);
});
}, [targets, expandedTargets, counts, searchedTargets, hideEmptyTargets]);
}, [targets, expandedTargets, counts, isHidden]);

const recordingRows = React.useMemo(() => {
return targets.map((target, idx) => {
Expand All @@ -288,7 +301,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent<AllTarge
</Tr>
);
});
}, [targets, expandedTargets, searchedTargets, hideEmptyTargets, counts]);
}, [targets, expandedTargets, isHidden]);

const rowPairs = React.useMemo(() => {
let rowPairs: JSX.Element[] = [];
Expand Down Expand Up @@ -324,7 +337,7 @@ export const AllTargetsArchivedRecordingsTable: React.FunctionComponent<AllTarge
<Tr>
<Th key="table-header-expand"/>
{tableColumns.map((key) => (
<Th key={`table-header-${key}`}>{key}</Th>
<Th key={`table-header-${key}`} width={key === 'Target' ? 90 : 15}>{key}</Th>
))}
</Tr>
</Thead>
Expand Down
75 changes: 0 additions & 75 deletions src/app/SecurityPanel/Credentials/CredentialsTableRow.tsx

This file was deleted.

150 changes: 150 additions & 0 deletions src/app/SecurityPanel/Credentials/MatchedTargetsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright The Cryostat Authors
*
* The Universal Permissive License (UPL), Version 1.0
*
* Subject to the condition set forth below, permission is hereby granted to any
* person obtaining a copy of this software, associated documentation and/or data
* (collectively the "Software"), free of charge and under any and all copyright
* rights in the Software, and any and all patent rights owned or freely
* licensable by each licensor hereunder covering either (i) the unmodified
* Software as contributed to or provided by such licensor, or (ii) the Larger
* Works (as defined below), to deal in both
*
* (a) the Software, and
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
* one is included with the Software (each a "Larger Work" to which the Software
* is contributed by such licensors),
*
* without restriction, including without limitation the rights to copy, create
* derivative works of, display, perform, and distribute the Software and make,
* use, sell, offer for sale, import, export, have made, and have sold the
* Software and the Larger Work(s), and to sublicense the foregoing rights on
* either these or other terms.
*
* This license is subject to the following condition:
* The above copyright notice and either this complete permission notice or at
* a minimum a reference to the UPL must be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from 'react';
import { ServiceContext } from '@app/Shared/Services/Services';
import { useSubscriptions } from '@app/utils/useSubscriptions';
import { Target } from '@app/Shared/Services/Target.service';
import { EmptyState, EmptyStateIcon, Title } from '@patternfly/react-core';
import { LoadingView } from '@app/LoadingView/LoadingView';
import { SearchIcon } from '@patternfly/react-icons';
import { InnerScrollContainer, TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service';
import { TargetDiscoveryEvent } from '@app/Shared/Services/Targets.service';
import _ from 'lodash';

export interface MatchedTargetsTableProps {
id: number,
matchExpression: string,
}

export const MatchedTargetsTable: React.FunctionComponent<MatchedTargetsTableProps> = (props) => {
const context = React.useContext(ServiceContext);

const [targets, setTargets] = React.useState([] as Target[]);
const [isLoading, setIsLoading] = React.useState(false);
const addSubscription = useSubscriptions();

const tableColumns: string[] = [
'Target',
];

const refreshTargetsList = React.useCallback(() => {
setIsLoading(true);
addSubscription(
context.api.getCredential(props.id)
.subscribe(
v => {
setTargets(v.targets);
setIsLoading(false);
}
)
);
}, [setIsLoading, addSubscription, context, context.api, setTargets]);

React.useEffect(() => {
refreshTargetsList();
}, []);

React.useEffect(() => {
addSubscription(
context.notificationChannel.messages(NotificationCategory.TargetJvmDiscovery)
.subscribe(v => {
const evt: TargetDiscoveryEvent = v.message.event;
const target: Target = evt.serviceRef;
if (evt.kind === 'FOUND') {
const match: boolean = eval(props.matchExpression);
if (match) {
setTargets(old => old.concat(target));
}
} else if (evt.kind === 'LOST') {
setTargets(old => old.filter(o => !_.isEqual(o, target)));
}
})
);
}, [addSubscription, context, context.notificationChannel, setTargets]);

const targetRows = React.useMemo(() => {
return targets.map((target, idx) => {
return (
<Tr key={`target-${idx}`}>
<Td key={`target-table-row-${idx}_0`}>
{(target.alias == target.connectUrl) || !target.alias ?
`${target.connectUrl}`
:
`${target.alias} (${target.connectUrl})`}
</Td>
</Tr>
);
});
}, [targets]);

let view: JSX.Element;
if (isLoading) {
view = (<LoadingView/>);
} else if (targets.length === 0) {
view =(<>
<EmptyState>
<EmptyStateIcon icon={SearchIcon}/>
<Title headingLevel="h4" size="lg">
No Targets
</Title>
</EmptyState>
</>);
} else {
view = (<>
<InnerScrollContainer style={{height: '300px'}}>
<TableComposable aria-label="matched-targets-table" isStickyHeader={true} variant={'compact'}>
<Thead>
<Tr>
{tableColumns.map((key) => (
<Th key={`table-header-${key}`}>{key}</Th>
))}
</Tr>
</Thead>
<Tbody>
{targetRows}
</Tbody>
</TableComposable>
</InnerScrollContainer>
</>);
}

return (<>
{view}
</>);
};
Loading

0 comments on commit e871759

Please sign in to comment.