From 66f3aa0aa1461cc5d286abe91249e7e54ed7e22e Mon Sep 17 00:00:00 2001 From: lyonlu13 <59022542+lyonlu13@users.noreply.github.com> Date: Tue, 23 Jan 2024 03:52:55 +0800 Subject: [PATCH 1/3] feat: display execution tags Signed-off-by: Lyon Lu --- .../Tables/WorkflowExecutionTable/cells.tsx | 23 +++++++++++++++++++ .../Tables/WorkflowExecutionTable/strings.ts | 1 + .../Tables/WorkflowExecutionTable/styles.ts | 9 ++++++++ .../useWorkflowExecutionsTableColumns.tsx | 8 +++++++ .../components/Executions/Tables/constants.ts | 3 ++- 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/cells.tsx b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/cells.tsx index 0a0e68718..3309296ba 100644 --- a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/cells.tsx +++ b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/cells.tsx @@ -4,6 +4,7 @@ import { IconButton, Button, CircularProgress, + Chip, } from '@material-ui/core'; import ArchiveOutlined from '@material-ui/icons/ArchiveOutlined'; import UnarchiveOutline from '@material-ui/icons/UnarchiveOutlined'; @@ -100,6 +101,28 @@ export function getDurationCell(execution: Execution): React.ReactNode { ); } +export function getExecutionTagsCell( + execution: Execution, + className: string, +): React.ReactNode { + const isArchived = isExecutionArchived(execution); + const tags = execution.spec.tags ?? []; + return ( +
+ {tags.map(tag => { + return ( + + ); + })} +
+ ); +} + export function getLaunchPlanCell( execution: Execution, className: string, diff --git a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/strings.ts b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/strings.ts index 36885a578..6b9550730 100644 --- a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/strings.ts +++ b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/strings.ts @@ -5,6 +5,7 @@ import { Protobuf } from '@flyteorg/flyteidl-types'; const str = { tableLabel_name: 'execution id', + tableLabel_tags: 'tags', tableLabel_launchPlan: 'launch plan', tableLabel_phase: 'status', tableLabel_startedAt: 'start time', diff --git a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/styles.ts b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/styles.ts index e5b01992a..beab8e557 100644 --- a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/styles.ts +++ b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/styles.ts @@ -10,6 +10,11 @@ export const useStyles = makeStyles((theme: Theme) => ({ flexBasis: workflowExecutionsTableColumnWidths.name, whiteSpace: 'normal', }, + columnExecutionTags: { + flexGrow: 1, + flexBasis: workflowExecutionsTableColumnWidths.tags, + whiteSpace: 'normal', + }, columnLaunchPlan: { flexGrow: 1, flexBasis: workflowExecutionsTableColumnWidths.launchPlan, @@ -56,4 +61,8 @@ export const useStyles = makeStyles((theme: Theme) => ({ width: '100px', // same as confirmationButton size textAlign: 'center', }, + executionTagsStack: { + display: 'flex', + gap: theme.spacing(0.5), + }, })); diff --git a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/useWorkflowExecutionsTableColumns.tsx b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/useWorkflowExecutionsTableColumns.tsx index 25d378987..52c94bc18 100644 --- a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/useWorkflowExecutionsTableColumns.tsx +++ b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/useWorkflowExecutionsTableColumns.tsx @@ -12,6 +12,7 @@ import { getStartTimeCell, getStatusCell, getLaunchPlanCell, + getExecutionTagsCell, } from './cells'; import { useStyles } from './styles'; import t, { patternKey } from './strings'; @@ -45,6 +46,13 @@ export function useWorkflowExecutionsTableColumns( key: 'name', label: t(patternKey('tableLabel', 'name')), }, + { + cellRenderer: ({ execution }) => + getExecutionTagsCell(execution, styles.executionTagsStack), + className: styles.columnExecutionTags, + key: 'tags', + label: t(patternKey('tableLabel', 'tags')), + }, { cellRenderer: ({ execution }) => getLaunchPlanCell(execution, commonStyles.textWrapped), diff --git a/packages/console/src/components/Executions/Tables/constants.ts b/packages/console/src/components/Executions/Tables/constants.ts index 19e428e79..bcb6de413 100644 --- a/packages/console/src/components/Executions/Tables/constants.ts +++ b/packages/console/src/components/Executions/Tables/constants.ts @@ -2,10 +2,11 @@ export const workflowExecutionsTableColumnWidths = { duration: 100, actions: 130, lastRun: 130, - name: 240, + name: 200, launchPlan: 120, phase: 120, startedAt: 200, + tags: 120, }; export const nodeExecutionsTableColumnWidths = { From ec124c914d7893c52f33011f654e4fe4269abb57 Mon Sep 17 00:00:00 2001 From: lyonlu13 <59022542+lyonlu13@users.noreply.github.com> Date: Fri, 26 Jan 2024 02:40:26 +0800 Subject: [PATCH 2/3] feat: execution tags column line break Signed-off-by: Lyon Lu --- .../Executions/Tables/WorkflowExecutionTable/styles.ts | 1 + packages/console/src/components/Executions/Tables/constants.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/styles.ts b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/styles.ts index beab8e557..9853f67f4 100644 --- a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/styles.ts +++ b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/styles.ts @@ -64,5 +64,6 @@ export const useStyles = makeStyles((theme: Theme) => ({ executionTagsStack: { display: 'flex', gap: theme.spacing(0.5), + flexWrap: 'wrap', }, })); diff --git a/packages/console/src/components/Executions/Tables/constants.ts b/packages/console/src/components/Executions/Tables/constants.ts index bcb6de413..5efe984e1 100644 --- a/packages/console/src/components/Executions/Tables/constants.ts +++ b/packages/console/src/components/Executions/Tables/constants.ts @@ -6,7 +6,7 @@ export const workflowExecutionsTableColumnWidths = { launchPlan: 120, phase: 120, startedAt: 200, - tags: 120, + tags: 200, }; export const nodeExecutionsTableColumnWidths = { From cac3afeb53fad007be6229a0bd50285c88f8c968 Mon Sep 17 00:00:00 2001 From: lyonlu13 <59022542+lyonlu13@users.noreply.github.com> Date: Fri, 2 Feb 2024 03:12:18 +0800 Subject: [PATCH 3/3] feat: colored execution tags and tags filter Signed-off-by: Lyon Lu --- .../Executions/ExecutionFilters.tsx | 10 ++ .../Tables/WorkflowExecutionTable/cells.tsx | 6 +- .../Executions/filters/constants.ts | 1 + .../components/Executions/filters/types.ts | 14 +- .../filters/useExecutionFiltersState.ts | 7 + .../Executions/filters/useTagsFilterState.ts | 87 ++++++++++ .../src/components/common/TagsInputForm.tsx | 154 ++++++++++++++++++ .../console/src/components/utils/index.ts | 19 +++ 8 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 packages/console/src/components/Executions/filters/useTagsFilterState.ts create mode 100644 packages/console/src/components/common/TagsInputForm.tsx diff --git a/packages/console/src/components/Executions/ExecutionFilters.tsx b/packages/console/src/components/Executions/ExecutionFilters.tsx index 43b494117..2b22adc82 100644 --- a/packages/console/src/components/Executions/ExecutionFilters.tsx +++ b/packages/console/src/components/Executions/ExecutionFilters.tsx @@ -5,12 +5,14 @@ import { MultiSelectForm } from 'components/common/MultiSelectForm'; import { SearchInputForm } from 'components/common/SearchInputForm'; import { SingleSelectForm } from 'components/common/SingleSelectForm'; import { FilterPopoverButton } from 'components/Tables/filters/FilterPopoverButton'; +import { TagsInputForm } from 'components/common/TagsInputForm'; import { FilterState, MultiFilterState, SearchFilterState, SingleFilterState, BooleanFilterState, + TagsFilterState, } from './filters/types'; const useStyles = makeStyles((theme: Theme) => ({ @@ -49,6 +51,7 @@ export interface ExecutionFiltersProps { const RenderFilter: React.FC<{ filter: FilterState }> = ({ filter }) => { const searchFilterState = filter as SearchFilterState; + const tagsFilterState = filter as TagsFilterState; switch (filter.type) { case 'single': return )} />; @@ -61,6 +64,13 @@ const RenderFilter: React.FC<{ filter: FilterState }> = ({ filter }) => { defaultValue={searchFilterState.value} /> ); + case 'tags': + return ( + + ); default: return null; } diff --git a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/cells.tsx b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/cells.tsx index 3309296ba..29a124127 100644 --- a/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/cells.tsx +++ b/packages/console/src/components/Executions/Tables/WorkflowExecutionTable/cells.tsx @@ -22,6 +22,7 @@ import { Execution } from 'models/Execution/types'; import { ExecutionState, WorkflowExecutionPhase } from 'models/Execution/enums'; import classnames from 'classnames'; import { LaunchPlanLink } from 'components/LaunchPlan/LaunchPlanLink'; +import { getColorFromString } from 'components/utils'; import { WorkflowExecutionsTableState } from '../types'; import { WorkflowExecutionLink } from '../WorkflowExecutionLink'; import { getWorkflowExecutionTimingMS, isExecutionArchived } from '../../utils'; @@ -115,7 +116,10 @@ export function getExecutionTagsCell( key={tag} label={tag} size="small" - color={isArchived ? 'default' : 'primary'} + color="default" + style={{ + backgroundColor: isArchived ? undefined : getColorFromString(tag), + }} /> ); })} diff --git a/packages/console/src/components/Executions/filters/constants.ts b/packages/console/src/components/Executions/filters/constants.ts index 3de575243..e4c325156 100644 --- a/packages/console/src/components/Executions/filters/constants.ts +++ b/packages/console/src/components/Executions/filters/constants.ts @@ -3,4 +3,5 @@ export const filterLabels = { startTime: 'Start Time', status: 'Status', version: 'Version', + tags: 'Tags', }; diff --git a/packages/console/src/components/Executions/filters/types.ts b/packages/console/src/components/Executions/filters/types.ts index 9242f478a..894a71716 100644 --- a/packages/console/src/components/Executions/filters/types.ts +++ b/packages/console/src/components/Executions/filters/types.ts @@ -20,7 +20,12 @@ export interface FilterButtonState { onClick: () => void; } -export type FilterStateType = 'single' | 'multi' | 'search' | 'boolean'; +export type FilterStateType = + | 'single' + | 'multi' + | 'search' + | 'boolean' + | 'tags'; export interface FilterState { active: boolean; @@ -60,3 +65,10 @@ export interface BooleanFilterState extends FilterState { setActive: (active: boolean) => void; type: 'boolean'; } + +export interface TagsFilterState extends FilterState { + onChange: (newTags: string[]) => void; + placeholder: string; + type: 'tags'; + tags: string[]; +} diff --git a/packages/console/src/components/Executions/filters/useExecutionFiltersState.ts b/packages/console/src/components/Executions/filters/useExecutionFiltersState.ts index 1b1cbbd17..336b7bf89 100644 --- a/packages/console/src/components/Executions/filters/useExecutionFiltersState.ts +++ b/packages/console/src/components/Executions/filters/useExecutionFiltersState.ts @@ -14,6 +14,7 @@ import { FilterState } from './types'; import { useMultiFilterState } from './useMultiFilterState'; import { useSearchFilterState } from './useSearchFilterState'; import { useSingleFilterState } from './useSingleFilterState'; +import { useTagsFilterState } from './useTagsFilterState'; export interface ExecutionFiltersState { appliedFilters: FilterOperation[]; @@ -45,6 +46,12 @@ export function useWorkflowExecutionFiltersState() { listHeader: 'Filter By', queryStateKey: 'status', }), + useTagsFilterState({ + filterKey: 'admin_tag.name', + label: filterLabels.tags, + placeholder: 'Enter Tags String', + queryStateKey: 'tags', + }), useSearchFilterState({ filterKey: 'workflow.version', label: filterLabels.version, diff --git a/packages/console/src/components/Executions/filters/useTagsFilterState.ts b/packages/console/src/components/Executions/filters/useTagsFilterState.ts new file mode 100644 index 000000000..4637e4541 --- /dev/null +++ b/packages/console/src/components/Executions/filters/useTagsFilterState.ts @@ -0,0 +1,87 @@ +import { useQueryState } from 'components/hooks/useQueryState'; +import { FilterOperationName } from 'models/AdminEntity/types'; +import { useEffect, useState } from 'react'; +import { TagsFilterState } from './types'; +import { useFilterButtonState } from './useFilterButtonState'; + +function serializeForQueryState(values: any[]) { + return values.join(';'); +} +function deserializeFromQueryState(stateValue = '') { + return stateValue.split(';'); +} + +interface TagsFilterStateStateArgs { + defaultValue?: string[]; + filterKey: string; + filterOperation?: FilterOperationName; + label: string; + placeholder: string; + queryStateKey: string; +} + +/** Maintains the state for a `TagsInputForm` filter. + * The generated `FilterOperation` will use the provided `key` and `operation` + * (defaults to `VALUE_IN`) + * The current search value will be synced to the query string using the + * provided `queryStateKey` value. + */ +export function useTagsFilterState({ + defaultValue = [], + filterKey, + filterOperation = FilterOperationName.VALUE_IN, + label, + placeholder, + queryStateKey, +}: TagsFilterStateStateArgs): TagsFilterState { + const { params, setQueryStateValue } = + useQueryState>(); + const queryStateValue = params[queryStateKey]; + + const [tags, setTags] = useState(defaultValue); + const active = tags.length !== 0; + + const button = useFilterButtonState(); + const onChange = (newValue: string[]) => { + setTags(newValue); + }; + + const onReset = () => { + setTags(defaultValue); + button.setOpen(false); + }; + + useEffect(() => { + const queryValue = tags.length ? serializeForQueryState(tags) : undefined; + setQueryStateValue(queryStateKey, queryValue); + }, [tags.join(), queryStateKey]); + + useEffect(() => { + if (queryStateValue) { + setTags(deserializeFromQueryState(queryStateValue)); + } + }, [queryStateValue]); + + const getFilter = () => + tags.length + ? [ + { + value: tags, + key: filterKey, + operation: filterOperation, + }, + ] + : []; + + return { + active, + button, + getFilter, + onChange, + onReset, + label, + placeholder, + tags, + type: 'tags', + }; +} diff --git a/packages/console/src/components/common/TagsInputForm.tsx b/packages/console/src/components/common/TagsInputForm.tsx new file mode 100644 index 000000000..3d1fa0dc4 --- /dev/null +++ b/packages/console/src/components/common/TagsInputForm.tsx @@ -0,0 +1,154 @@ +import { + Chip, + FormControl, + FormLabel, + IconButton, + InputAdornment, + makeStyles, + OutlinedInput, + Theme, + Link, +} from '@material-ui/core'; +import { Add } from '@material-ui/icons'; +import { getColorFromString } from 'components/utils'; +import * as React from 'react'; + +const useStyles = makeStyles((theme: Theme) => ({ + input: { + margin: `${theme.spacing(1)}px 0`, + }, + listHeader: { + color: theme.palette.text.secondary, + lineHeight: 1.5, + textTransform: 'uppercase', + }, + resetLink: { + marginLeft: theme.spacing(4), + width: theme.spacing(5), + }, + title: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + margin: 0, + textTransform: 'uppercase', + color: theme.palette.text.secondary, + }, + tagStack: { + display: 'flex', + gap: theme.spacing(0.5), + flexWrap: 'wrap', + width: '240px', + margin: `${theme.spacing(1)}px 0`, + }, +})); + +export interface TagsInputFormProps { + label: string; + placeholder?: string; + onChange: (tags: string[]) => void; + defaultValue: string[]; +} + +/** Form content for rendering a header and search input. The value is applied + * on submission of the form. + */ +export const TagsInputForm: React.FC = ({ + label, + placeholder, + onChange, + defaultValue, +}) => { + const [tags, setTags] = React.useState(defaultValue); + const [value, setValue] = React.useState(''); + const composition = React.useRef(false); + + const styles = useStyles(); + const onInputChange: React.ChangeEventHandler = ({ + target: { value }, + }) => setValue(value); + + const addTag = () => { + const newTag = value.trim(); + setValue(''); + if (!tags.includes(newTag) && newTag !== '') { + const newTags = [...tags, newTag]; + setTags(newTags); + onChange(newTags); + } + }; + + const removeTag = (tag: string) => { + const newTags = tags.filter(t => t !== tag); + setTags(newTags); + onChange(newTags); + }; + + const handleClickReset = () => { + setTags([]); + onChange([]); + }; + + const resetControl = tags.length ? ( + + Reset + + ) : ( +
+ ); + + return ( +
+
+ {label} + {resetControl} +
+ + (composition.current = true)} + onCompositionEnd={() => (composition.current = false)} + placeholder={placeholder} + type="text" + value={value} + onKeyDown={e => { + if (e.key === 'Enter' && !composition.current) { + addTag(); + } + }} + endAdornment={ + + + + + + } + /> +
+ {tags.map(tag => { + return ( + removeTag(tag)} + style={{ + backgroundColor: getColorFromString(tag), + }} + /> + ); + })} +
+
+
+ ); +}; diff --git a/packages/console/src/components/utils/index.ts b/packages/console/src/components/utils/index.ts index 794f1f5f9..8e223fb10 100644 --- a/packages/console/src/components/utils/index.ts +++ b/packages/console/src/components/utils/index.ts @@ -1,3 +1,22 @@ export const removeLeadingSlash = (pathName: string | null): string => { return pathName?.replace(/^\//, '') || ''; }; + +export const getColorFromString = (str: string) => { + const strToDec = (string: string) => { + return ( + Array.from(string) + .map(c => c.codePointAt(0) || 0) + .reduce((sum, char, i) => sum + ((i + 1) * char) / 256, 0) % 1 + ); + }; + return ( + 'hsl(' + + 360 * strToDec(str) + + ',' + + (40 + 60 * strToDec(str)) + + '%,' + + (75 + 10 * strToDec(str)) + + '%)' + ); +};