Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(recordings): more tools for filtering by labels #503

Merged
merged 98 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
9745727
fix(tests,history): specify initEntries for createMemoryHistory
Aug 8, 2022
064846e
fix(test, recordings): add router for all rendered components
Aug 8, 2022
48c4e4e
fix(test, activeRecording): add cleanup after each tests
Aug 8, 2022
22bcc1c
fix(filters): filter dropdown should close after selecting
Sep 1, 2022
1d5d206
chore(filters): move state filter to its own source file
Sep 1, 2022
fdf6b81
fix(filters): current filter category should remain
Sep 1, 2022
b249eff
fix(filters): users should able to bulk delete filters in the same ca…
Sep 2, 2022
f81dd98
!tmp(tests): add mocks until new filter features are implemented
Sep 2, 2022
7cdbee2
chore(filters): move durationFilter to a separate source file
Sep 5, 2022
91ee0d5
chore(filters): move filters to subdirectory of Recordings
Sep 5, 2022
f2816ae
chore(filters): renaming variables
Sep 5, 2022
8efb133
chore(filters): move filter category interfaces to RecordingFilters file
Sep 5, 2022
90874cb
fix(filters): filtered labels should be highlighted
Sep 6, 2022
d2d6539
fix(filters): recording labels are now clickable
Sep 6, 2022
9d4e4e2
fix(test-snapshots): update test snapshots
Sep 6, 2022
e4da3cb
fix(filters): add missing deps in useCallBacks
Sep 6, 2022
f72ef39
fix(filters): better naming for props callback
Sep 6, 2022
8a1373d
fix(filters): use a common func to get label display format
Sep 6, 2022
3ddcaf3
fix(filters): better handling of label filter search
Sep 6, 2022
6fc5d7b
feat(storage): add utility func to store filter states to local storage
Sep 7, 2022
945dddd
feat(storage): set up Redux for state management
Sep 7, 2022
8141aa3
fix(storage): only save recording filters to local storage
Sep 7, 2022
68cd261
fix(redux): redux should store unfiltered recordings
Sep 8, 2022
5acb2cb
chore(storage): move local storage utilities to util directory
Sep 8, 2022
3af10a3
tmp: save selected indices of recordings to redux store
Sep 8, 2022
566fabe
fix(redux): update redux actions and reducers
Sep 9, 2022
a22363b
feat(filters): recording filters now persist across views
Sep 9, 2022
42a9aa6
fix(recording-tables): add missing deps for react callbacks and memos
Sep 12, 2022
59d0a90
fix(filters): fix clearAllFilters button
Sep 12, 2022
a4a5c1d
feat(labels): clickable labels should be highlighted on hover
Sep 12, 2022
c8d41e8
fix(applayout): add unique keys for each child component in list
Sep 12, 2022
f1442af
feat(filters): filter states should be saved to local storage
Sep 12, 2022
07afc2c
fix(filters): state filter now allows toggling
Sep 12, 2022
d0a5346
fix(filters): fix datetime picker component layout
Sep 12, 2022
38a6242
chore(filters): rename disPatch to dispatch
Sep 12, 2022
94d176e
fix(filters): add explain comments for datetime picker appendTo props
Sep 12, 2022
e611736
test(filters): add test for NameFilter
Sep 12, 2022
2136672
fix(filter): header check should update when filter changes
Sep 12, 2022
ff2adf0
fix(filters): labels in filter should be sorted
Sep 12, 2022
f6d88cb
fix(filter): optimize callbacks for NameFilter
Sep 12, 2022
c5436c6
fix(active-recording-table): header and checked indices now update pr…
Sep 12, 2022
772bd6c
fix(archived-recording-table): header and checked indices now update …
Sep 12, 2022
3dd935d
fix(bulk-edit): bulk-edit must choose correctly selected rows
Sep 12, 2022
2a8ac40
fix(filters): name and label filters should excluded the selected opt…
Sep 12, 2022
a1e3392
fix(filters): update callback deps
Sep 13, 2022
812e0e8
fix(fitlers): remove unused states in filter
Sep 13, 2022
8e4596f
fix(jest): add jest-dom as a setup file for nested test files
Sep 13, 2022
f4b5aef
test(filters): update tests for NameFilter
Sep 13, 2022
fbf3bce
test(filters): add unit test for LabelFilter
Sep 13, 2022
875bcff
fix(filters): remove log calls
Sep 13, 2022
5e52ee7
test(filter): add test for DurationFilter
Sep 13, 2022
4764fa4
test(filter): add test for RecordingStateFilter
Sep 13, 2022
b9ebc73
test(filters): fix RecordingStateFilter tests
Sep 13, 2022
85f8110
fix(datetimepicker): set document.body as default element to bind pop…
Sep 14, 2022
d588d97
test(filters): check if recording state filter is closed on toggle
Sep 14, 2022
5ac9bb4
fix(datetimepicker): datetime is now correct without time set and tim…
Sep 14, 2022
7abe2f4
test(filters): add tests for DateTimePicker
Sep 14, 2022
11da963
fix(datetimepicker): move func to find menu mount point outside compo…
Sep 14, 2022
598c12d
test(datepicker): fix test imports
Sep 14, 2022
77b66b2
chore(filters): move ClickableLabel to separate source file
Sep 14, 2022
0edc084
test(clickable-label): add unit tests for clickable label
Sep 15, 2022
9399243
tests(filters): add tests for LabelCell
Sep 15, 2022
c8bebea
fix(filters): calculate hash for archive recording indexes
Sep 15, 2022
62d4b69
fix(bulk-edit): remove unnecessary notification sub
Sep 15, 2022
2427ff6
fix(bulk-edit): use props to check if a recording list is active/arch…
Sep 15, 2022
8e59619
tests(bulk-edit): add unit tests for labe bulk-edit
Sep 15, 2022
805bbe6
fix(redux): reword comment and add type hints
Sep 16, 2022
cd2cfc0
feat(redux): add type for root state and dispatch
Sep 16, 2022
135c8c8
fix(redux): export wrapper setupStore
Sep 16, 2022
f1a253b
feat(redux): allow preloading state into store and define redux type …
Sep 16, 2022
bfe3190
feat(redux-test): add wrapper for redux Provider in tests
Sep 16, 2022
89c59e4
test(filters): add unit test for recording fitlers
Sep 16, 2022
bd95e08
test(recordings): fix tests for active recording table
Sep 16, 2022
86ebb6d
test(recordings): fix archived recording table tests
Sep 16, 2022
ae495f1
fix(local-storage): update comments
Sep 16, 2022
0a6564c
chore(filters): add missing memo deps
Sep 18, 2022
4dedc91
test(filters): add check if approriate filter input is shown
Sep 18, 2022
d5e13e2
chore(filters): refactor toolbar from recording table
Sep 18, 2022
ff63b19
fix(filters): label and name filter should close menu on selection
Sep 19, 2022
006bdb5
fix(toolbars): delete warning should be open on clicked if enabled
Sep 19, 2022
dcf2b79
fix(recordings): limit calls to context.settings with React memo
Sep 19, 2022
989ad60
test(recordings): fix tests
Sep 19, 2022
ca2c0ab
fix(recordings): fix drawer id
Sep 19, 2022
10fe575
test(filter): fix datepicker filter
Sep 19, 2022
d616aef
test(active-recording): update snapshots
Sep 19, 2022
f301084
test(datepicker): clean up date mocks
Sep 19, 2022
2d9f259
chore(filters): rename, clean up comments and callbacks
Sep 19, 2022
637ff48
fix(labelcell): group prop fields
Sep 19, 2022
86376ab
chore(filters): rename recording categories to singulars
Sep 19, 2022
a55f854
chore(filters): move hashCode method to utils module
Sep 19, 2022
3ad2637
fix(filters): add missing deps
Sep 20, 2022
c0c1b36
fix(delete-modal): move delete-modal flag inside toolbars
Sep 20, 2022
33777f7
fix(filters): mount menu to document.body
Sep 20, 2022
404b0b9
fix(datepicker): remove unused deps
Sep 20, 2022
7f51285
fix(filters): fix filter name should in singulars
Sep 20, 2022
12d7edf
test(filters): update tests
Sep 20, 2022
4ec8d89
fix(filters): do not render unncessary filters in Archived view
Sep 20, 2022
a24724d
fix(filters): filters display should be fixed for each type of recording
Sep 20, 2022
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 jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = {
preset: "ts-jest/presets/js-with-ts",

// The path to a module that runs some code to configure or set up the testing framework before each test
setupFilesAfterEnv: ['<rootDir>/test-setup.js'],
setupFilesAfterEnv: ['<rootDir>/test-setup.js', "@testing-library/jest-dom"],

// The test environment that will be used for testing.
testEnvironment: "jsdom",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,12 @@
"@patternfly/react-icons": "^4.75.1",
"@patternfly/react-styles": "^4.74.1",
"@patternfly/react-table": "^4.93.1",
"@reduxjs/toolkit": "^1.8.5",
"@types/lodash": "^4.14.175",
"express": "^4.17.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^8.0.2",
"react-router-last-location": "^2.0.1"
}
}
3 changes: 2 additions & 1 deletion src/app/AppLayout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ const AppLayout: React.FunctionComponent<IAppLayout> = ({children}) => {
<NavList id="nav-list-simple">
{navGroups.map((title) => {
return (
<NavGroup title={title}>
<NavGroup title={title} key={title}>
{routes.filter(route => route.navGroup === title)
.map((route, idx) => {
return (
Expand Down Expand Up @@ -295,6 +295,7 @@ const AppLayout: React.FunctionComponent<IAppLayout> = ({children}) => {
.map(( { key, title, message, variant } ) => (
<Alert
variant={variant}
key={title}
title={title}
actionClose={<AlertActionCloseButton onClose={() => handleMarkNotificationRead(key)} />}
timeout={true}
Expand Down
2 changes: 1 addition & 1 deletion src/app/Modal/DeleteWarningUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,4 @@ export const DeleteWarningKinds : DeleteWarning[] = [
export const getFromWarningMap = (warning: DeleteWarningType): DeleteWarning | undefined => {
const wt = DeleteWarningKinds.find(t => t.id === warning);
return wt;
}
}
40 changes: 18 additions & 22 deletions src/app/RecordingMetadata/BulkEditLabels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { RecordingLabelFields } from './RecordingLabelFields';
import { HelpIcon } from '@patternfly/react-icons';
import { NO_TARGET } from '@app/Shared/Services/Target.service';
import { NotificationCategory } from '@app/Shared/Services/NotificationChannel.service';
import { hashCode } from '@app/utils/utils';

export interface BulkEditLabelsProps {
isTargetRecording: boolean;
Expand All @@ -62,11 +63,20 @@ export const BulkEditLabels: React.FunctionComponent<BulkEditLabelsProps> = (pro
const [valid, setValid] = React.useState(ValidatedOptions.default);
const addSubscription = useSubscriptions();

const getIdxFromRecording = React.useCallback((r: ArchivedRecording): number => {
if (props.isTargetRecording) {
return (r as ActiveRecording).id;
} else {
return hashCode(r.name);
}
}, [hashCode, props.isTargetRecording]);

const handleUpdateLabels = React.useCallback(() => {
const tasks: Observable<any>[] = [];
const toDelete = savedCommonLabels.filter((label) => !includesLabel(commonLabels, label));

recordings.forEach((r: ArchivedRecording, idx) => {
recordings.forEach((r: ArchivedRecording) => {
const idx = getIdxFromRecording(r);
if (props.checkedIndices.includes(idx)) {
let updatedLabels = [...parseLabels(r.metadata.labels), ...commonLabels];
updatedLabels = updatedLabels.filter((label) => {
Expand Down Expand Up @@ -106,7 +116,9 @@ export const BulkEditLabels: React.FunctionComponent<BulkEditLabelsProps> = (pro
const updateCommonLabels = React.useCallback(
(setLabels: (l: RecordingLabel[]) => void) => {
let allRecordingLabels = [] as RecordingLabel[][];
recordings.forEach((r: ArchivedRecording, idx) => {

recordings.forEach((r: ArchivedRecording) => {
const idx = getIdxFromRecording(r);
if (props.checkedIndices.includes(idx)) {
allRecordingLabels.push(parseLabels(r.metadata.labels));
}
Expand Down Expand Up @@ -159,25 +171,8 @@ export const BulkEditLabels: React.FunctionComponent<BulkEditLabelsProps> = (pro
addSubscription(context.target.target().subscribe(refreshRecordingList));
}, [addSubscription, context, context.target, refreshRecordingList]);

React.useEffect(() => {
addSubscription(
combineLatest([
context.target.target(),
merge(
context.notificationChannel.messages(NotificationCategory.ActiveRecordingDeleted),
context.notificationChannel.messages(NotificationCategory.SnapshotDeleted)
),
]).subscribe((parts) => {
const currentTarget = parts[0];
const event = parts[1];
if (currentTarget.connectUrl != event.message.target) {
return;
}
setRecordings((old) => old.filter((r) => r.name != event.message.recording.name));
})
);
}, [addSubscription, context, context.notificationChannel, setRecordings]);

// Depends only on RecordingMetadataUpdated notifications
// since updates on list of recordings will mount a completely new BulkEditLabels.
React.useEffect(() => {
addSubscription(
combineLatest([
Expand Down Expand Up @@ -236,7 +231,7 @@ export const BulkEditLabels: React.FunctionComponent<BulkEditLabelsProps> = (pro
</Split>
</StackItem>
<StackItem>
<LabelCell labels={savedCommonLabels} />
<LabelCell target='' labels={savedCommonLabels} />
</StackItem>
<StackItem>
{editing ? (
Expand All @@ -263,6 +258,7 @@ export const BulkEditLabels: React.FunctionComponent<BulkEditLabelsProps> = (pro
) : (
<Button
key="edit labels"
aria-label='Edit Labels'
variant="secondary"
onClick={handleEditLabels}
isDisabled={!props.checkedIndices.length}
Expand Down
88 changes: 88 additions & 0 deletions src/app/RecordingMetadata/ClickableLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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 { getLabelDisplay } from "@app/Recordings/Filters/LabelFilter";
import { Label } from "@patternfly/react-core";
import React from "react";
import { RecordingLabel } from "./RecordingLabel";


export interface ClickableLabelCellProps {
label: RecordingLabel;
isSelected: boolean;
onLabelClick: (label: RecordingLabel) => void
}

export const ClickableLabel: React.FunctionComponent<ClickableLabelCellProps> = (props) => {
const [isHoveredOrFocused, setIsHoveredOrFocused] = React.useState(false);
const labelColor = React.useMemo(() => props.isSelected? "blue": "grey", [props.isSelected]);

const handleHoveredOrFocused = React.useCallback(() => setIsHoveredOrFocused(true), [setIsHoveredOrFocused]);
const handleNonHoveredOrFocused = React.useCallback(() => setIsHoveredOrFocused(false), [setIsHoveredOrFocused]);

const style = React.useMemo(() => {
if (isHoveredOrFocused) {
const defaultStyle = { cursor: "pointer", "--pf-c-label__content--before--BorderWidth": "2.5px"};
if (props.isSelected) {
return {...defaultStyle, "--pf-c-label__content--before--BorderColor": "#06c"}
}
return {...defaultStyle, "--pf-c-label__content--before--BorderColor": "#8a8d90"}
}
return {};
}, [props.isSelected, isHoveredOrFocused]);

const handleLabelClicked = React.useCallback(
() => props.onLabelClick(props.label),
[props.label, props.onLabelClick, getLabelDisplay]
);

return <>
<Label
aria-label={`${props.label.key}: ${props.label.value}`}
style={style}
onMouseEnter={handleHoveredOrFocused}
onMouseLeave={handleNonHoveredOrFocused}
tthvo marked this conversation as resolved.
Show resolved Hide resolved
onFocus={handleHoveredOrFocused}
onClick={handleLabelClicked}
key={props.label.key}
color={labelColor}
>
{`${props.label.key}: ${props.label.value}`}
</Label>
</>;
}
47 changes: 43 additions & 4 deletions src/app/RecordingMetadata/LabelCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,60 @@
* SOFTWARE.
*/

import { getLabelDisplay } from '@app/Recordings/Filters/LabelFilter';
import { UpdateFilterOptions } from '@app/Shared/Redux/RecordingFilterReducer';
import { Label, Text } from '@patternfly/react-core';
import React from 'react';
import { ClickableLabel } from './ClickableLabel';
import { RecordingLabel } from './RecordingLabel';

export interface LabelCellProps {
target: string;
labels: RecordingLabel[];
clickableOptions?: { // If undefined, labels are not clickable (i.e. display only) and only displayed in grey.
labelFilters: string[];
updateFilters: (target: string, updateFilterOptions: UpdateFilterOptions) => void
}
}

export const LabelCell: React.FunctionComponent<LabelCellProps> = (props) => {
// TODO make labels clickable to select multiple recordings with the same label
const isLabelSelected = React.useCallback((label: RecordingLabel) => {
if (props.clickableOptions) {
const labelFilterSet = new Set(props.clickableOptions.labelFilters);
return labelFilterSet.has(getLabelDisplay(label))
}
return false;
}, [getLabelDisplay, props.clickableOptions]);

const getLabelColor = React.useCallback((label: RecordingLabel) => isLabelSelected(label)? "blue": "grey", [isLabelSelected]);
const onLabelSelectToggle = React.useCallback(
(clickedLabel: RecordingLabel) => {
if (props.clickableOptions) {
props.clickableOptions.updateFilters(props.target, {filterKey: "Label", filterValue: getLabelDisplay(clickedLabel), deleted: isLabelSelected(clickedLabel)})
}
},
[props.clickableOptions, props.target, getLabelDisplay]);

return (
<>
{!!props.labels && props.labels.length ? (
props.labels.map((l) => <Label key={l.key} color="grey">{`${l.key}: ${l.value}`}</Label>)
) : (
{!!props.labels && props.labels.length? (
props.labels.map((label) =>
props.clickableOptions?
<ClickableLabel
key={label.key}
label={label}
isSelected={isLabelSelected(label)}
onLabelClick={onLabelSelectToggle}
/> :
<Label
aria-label={`${label.key}: ${label.value}`}
key={label.key}
color={getLabelColor(label)}
>
{`${label.key}: ${label.value}`}
</Label>

)) : (
<Text>-</Text>
)}
</>
Expand Down
4 changes: 2 additions & 2 deletions src/app/RecordingMetadata/RecordingLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export const parseLabels = (jsonLabels) => {
});
};

export const includesLabel = (arr, searchLabel) => {
export const includesLabel = (arr: RecordingLabel[], searchLabel: RecordingLabel) => {
return arr.some(l => isEqualLabel(searchLabel, l));
}

const isEqualLabel = (a, b) => {
const isEqualLabel = (a: RecordingLabel, b: RecordingLabel) => {
return (a.key === b.key) && (a.value === b.value);
}
2 changes: 1 addition & 1 deletion src/app/RecordingMetadata/RecordingLabelFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const RecordingLabelFields: React.FunctionComponent<RecordingLabelFieldsP

return (
<>
<Button onClick={handleAddLabelButtonClick} variant="link" icon={<PlusCircleIcon />}>
<Button aria-label='Add Label' onClick={handleAddLabelButtonClick} variant="link" icon={<PlusCircleIcon />}>
Add Label
</Button>
{!!props.labels && props.labels.map((label, idx) => (
Expand Down
Loading