Skip to content

Commit

Permalink
feat: new label editor for recordings
Browse files Browse the repository at this point in the history
  • Loading branch information
tthvo committed Aug 10, 2024
1 parent 7dd7581 commit 04d54de
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 118 deletions.
20 changes: 12 additions & 8 deletions src/app/RecordingMetadata/BulkEditLabels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
Button,
HelperText,
HelperTextItem,
Label,
Stack,
StackItem,
Title,
Expand All @@ -50,6 +51,7 @@ export interface BulkEditLabelsProps {
checkedIndices: number[];
directory?: RecordingDirectory;
directoryRecordings?: ArchivedRecording[];
closePanelFn: () => void;
}

export const BulkEditLabels: React.FC<BulkEditLabelsProps> = ({
Expand All @@ -58,6 +60,7 @@ export const BulkEditLabels: React.FC<BulkEditLabelsProps> = ({
checkedIndices,
directory,
directoryRecordings,
closePanelFn,
}) => {
const context = React.useContext(ServiceContext);
const [recordings, setRecordings] = React.useState<Recording[]>([]);
Expand All @@ -84,10 +87,10 @@ export const BulkEditLabels: React.FC<BulkEditLabelsProps> = ({
recordings.forEach((r: Recording) => {
const idx = getIdxFromRecording(r);
if (checkedIndices.includes(idx)) {
let updatedLabels = [...r.metadata.labels, ...commonLabels];
updatedLabels = updatedLabels.filter((label) => {
return !includesLabel(toDelete, label);
});
const updatedLabels = [...r.metadata.labels, ...commonLabels].filter(
(label) => !includesLabel(toDelete, label),
);

if (directory) {
tasks.push(context.api.postRecordingMetadataForJvmId(directory.jvmId, r.name, updatedLabels).pipe(first()));
}
Expand Down Expand Up @@ -122,11 +125,12 @@ export const BulkEditLabels: React.FC<BulkEditLabelsProps> = ({

const handleCancel = React.useCallback(() => {
setCommonLabels(savedCommonLabels);
}, [setCommonLabels, savedCommonLabels]);
closePanelFn();
}, [setCommonLabels, savedCommonLabels, closePanelFn]);

const updateCommonLabels = React.useCallback(
(setLabels: (l: KeyValue[]) => void) => {
const allRecordingLabels = [] as KeyValue[][];
const allRecordingLabels: KeyValue[][] = [];

recordings.forEach((r: Recording) => {
const idx = getIdxFromRecording(r);
Expand Down Expand Up @@ -293,8 +297,8 @@ export const BulkEditLabels: React.FC<BulkEditLabelsProps> = ({
<StackItem>
<HelperText>
<HelperTextItem>
Labels present on all selected Recordings will appear here. Editing the Labels will affect all selected
Recordings.
Labels present on all selected Recordings will appear here. Editing the labels will affect all selected
Recordings. Specify labels with format <Label isCompact>key=value</Label>.
</HelperTextItem>
</HelperText>
</StackItem>
Expand Down
144 changes: 39 additions & 105 deletions src/app/RecordingMetadata/RecordingLabelFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@
import { LoadingView } from '@app/Shared/Components/LoadingView';
import { KeyValue, keyValueToString } from '@app/Shared/Services/api.types';
import { useSubscriptions } from '@app/utils/hooks/useSubscriptions';
import { portalRoot } from '@app/utils/utils';
import { LABEL_TEXT_MAXWIDTH, portalRoot } from '@app/utils/utils';

Check warning on line 19 in src/app/RecordingMetadata/RecordingLabelFields.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

'LABEL_TEXT_MAXWIDTH' is defined but never used

Check warning on line 19 in src/app/RecordingMetadata/RecordingLabelFields.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

'LABEL_TEXT_MAXWIDTH' is defined but never used
import { Button, Label, LabelGroup, List, ListItem, Popover, Text, ValidatedOptions } from '@patternfly/react-core';
import { ExclamationCircleIcon, FileIcon, UploadIcon } from '@patternfly/react-icons';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { catchError, Observable, of, zip } from 'rxjs';
import { matchesLabelSyntax, getValidatedOption, LabelPattern, parseLabelsFromFile } from './utils';
import {
isValidLabel,
getValidatedOption,
isDuplicateKey,
parseLabelsFromFile,
isValidLabelInput,

Check warning on line 30 in src/app/RecordingMetadata/RecordingLabelFields.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (18.x)

'isValidLabelInput' is defined but never used

Check warning on line 30 in src/app/RecordingMetadata/RecordingLabelFields.tsx

View workflow job for this annotation

GitHub Actions / eslint-check (16.x)

'isValidLabelInput' is defined but never used
getLabelFromInput,
} from './utils';

export interface RecordingLabelFieldsProps {
labels: KeyValue[];
Expand All @@ -46,28 +53,22 @@ export const RecordingLabelFields: React.FC<RecordingLabelFieldsProps> = ({
const [loading, setLoading] = React.useState(false);
const [invalidUploads, setInvalidUploads] = React.useState<string[]>([]);

const handleKeyChange = React.useCallback(
(idx: number, key: string) => {
const updatedLabels = [...labels];
updatedLabels[idx].key = key;
setLabels(updatedLabels);
},
[labels, setLabels],
);
const handleAddLabelButtonClick = React.useCallback(() => {
setLabels([...labels, { key: 'key', value: 'value' }]);
}, [labels, setLabels]);

const handleValueChange = React.useCallback(
(idx: number, value: string) => {
const updatedLabels = [...labels];
updatedLabels[idx].value = value;
setLabels(updatedLabels);
const handleLabelEdit = React.useCallback(
(idx: number, keyValue: string) => {
const label = getLabelFromInput(keyValue);
if (label) {
const updatedLabels = [...labels];
updatedLabels[idx] = label;
setLabels(updatedLabels);
}
},
[labels, setLabels],
);

const handleAddLabelButtonClick = React.useCallback(() => {
setLabels([...labels, { key: '', value: '' } as KeyValue]);
}, [labels, setLabels]);

const handleDeleteLabelButtonClick = React.useCallback(
(idx: number) => {
const updated = [...labels];
Expand All @@ -77,38 +78,10 @@ export const RecordingLabelFields: React.FC<RecordingLabelFieldsProps> = ({
[labels, setLabels],
);

const isDuplicateKey = React.useCallback(
(key: string, labels: KeyValue[]) => labels.filter((label) => label.key === key).length > 1,
[],
);

const validKeys = React.useMemo(() => {
const arr = Array(labels.length).fill(ValidatedOptions.default);
labels.forEach((label, index) => {
if (label.key.length > 0) {
arr[index] = getValidatedOption(LabelPattern.test(label.key) && !isDuplicateKey(label.key, labels));
} // Ignore initial empty key inputs
});
return arr;
}, [labels, isDuplicateKey]);

const validValues = React.useMemo(() => {
const arr = Array(labels.length).fill(ValidatedOptions.default);
labels.forEach((label, index) => {
if (label.value.length > 0) {
arr[index] = getValidatedOption(LabelPattern.test(label.value));
} // Ignore initial empty value inputs
});
return arr;
}, [labels]);

React.useEffect(() => {
const valid = labels.reduce(
(prev, curr) => matchesLabelSyntax(curr) && !isDuplicateKey(curr.key, labels) && prev,
true,
);
const valid = labels.reduce((prev, curr) => isValidLabel(curr) && !isDuplicateKey(curr.key, labels) && prev, true);
setValid(getValidatedOption(valid));
}, [setValid, labels, isDuplicateKey]);
}, [setValid, labels]);

const handleUploadLabel = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -199,71 +172,32 @@ export const RecordingLabelFields: React.FC<RecordingLabelFieldsProps> = ({
numLabels={5}
isEditable
addLabelControl={
<Label color="blue" variant="outline" isOverflowLabel onClick={() => {}}>
<Label
color="blue"
variant="outline"
isOverflowLabel
isDisabled={isDisabled}
onClick={handleAddLabelButtonClick}
>
Add label
</Label>
}
>
{labels.map((label, idx) => (
<Label key={label.key} id={label.key} color="blue" onClose={() => handleDeleteLabelButtonClick(idx)}>
<Label
key={label.key}
id={label.key}
color="grey"
isEditable
onClose={() => handleDeleteLabelButtonClick(idx)}
isDisabled={isDisabled}
onEditCancel={(_, prevText) => handleLabelEdit(idx, prevText)}
onEditComplete={(_, newText) => handleLabelEdit(idx, newText)}
>
{keyValueToString(label)}
</Label>
))}
</LabelGroup>
{/* {labels.map((label, idx) => (
<Split hasGutter key={idx}>
<SplitItem isFilled>
<TextInput
isRequired
type="text"
id="label-key-input"
name="label-key-input"
aria-describedby="label-key-input-helper"
aria-label="Label Key"
value={label.key ?? ''}
onChange={(_event, key) => handleKeyChange(idx, key)}
validated={validKeys[idx]}
isDisabled={isDisabled}
/>
<Text>Key</Text>
<FormHelperText>
<HelperText id="label-error-text">
<HelperTextItem variant="error">
Keys must be unique. Labels should not contain empty spaces.
</HelperTextItem>
</HelperText>
</FormHelperText>
</SplitItem>
<SplitItem isFilled>
<TextInput
isRequired
type="text"
id="label-value-input"
name="label-value-input"
aria-describedby="label-value-input-helper"
aria-label="Label Value"
value={label.value ?? ''}
onChange={(_event, value) => handleValueChange(idx, value)}
validated={validValues[idx]}
isDisabled={isDisabled}
/>
<Text>Value</Text>
</SplitItem>
<SplitItem>
<Button
onClick={() => handleDeleteLabelButtonClick(idx)}
variant="link"
aria-label="Remove Label"
isDisabled={isDisabled}
icon={
<Icon size="sm">
<CloseIcon color="gray" />{' '}
</Icon>
}
/>
</SplitItem>
</Split>
))} */}
</>
);
};
25 changes: 20 additions & 5 deletions src/app/RecordingMetadata/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,25 @@ export const parseLabelsFromFile = (file: File): Observable<KeyValue[]> => {

export const LabelPattern = /^\S+$/;

export const getValidatedOption = (isValid: boolean) => {
return isValid ? ValidatedOptions.success : ValidatedOptions.error;
};
export const LabelInputPattern = /^(\S+)=(\S+)$/;

export const getValidatedOption = (isValid: boolean) => (isValid ? ValidatedOptions.success : ValidatedOptions.error);

export const getLabelFromInput = (labelInput: string): KeyValue | undefined => {
if (!isValidLabelInput(labelInput)) {
return undefined;
}
const matches = labelInput.match(LabelInputPattern);
if (!matches) {
return undefined;
}

export const matchesLabelSyntax = (l: KeyValue) => {
return l && LabelPattern.test(l.key) && LabelPattern.test(l.value);
return { key: matches[1], value: matches[2] };
};

export const isDuplicateKey = (key: string, labels: KeyValue[]) =>
labels.filter((label) => label.key === key).length > 1;

export const isValidLabel = (l: KeyValue) => l && LabelPattern.test(l.key) && LabelPattern.test(l.value);

export const isValidLabelInput = (labelInput: string) => LabelInputPattern.test(labelInput);
1 change: 1 addition & 0 deletions src/app/Recordings/RecordingLabelsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const RecordingLabelsPanel: React.FC<RecordingLabelsPanelProps> = (props)
isUploadsTable={props.isUploadsTable}
directory={props.directory}
directoryRecordings={props.directoryRecordings}
closePanelFn={() => props.setShowPanel(false)}
/>
</DrawerPanelBody>
</DrawerPanelContent>
Expand Down

0 comments on commit 04d54de

Please sign in to comment.