Skip to content

Commit

Permalink
feat: colored execution tags and tags filter
Browse files Browse the repository at this point in the history
Signed-off-by: lyonlu13 <[email protected]>
  • Loading branch information
lyonlu13 committed Feb 1, 2024
1 parent e6cb9c7 commit 22d512a
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 2 deletions.
10 changes: 10 additions & 0 deletions packages/console/src/components/Executions/ExecutionFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down Expand Up @@ -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 <SingleSelectForm {...(filter as SingleFilterState<any>)} />;
Expand All @@ -61,6 +64,13 @@ const RenderFilter: React.FC<{ filter: FilterState }> = ({ filter }) => {
defaultValue={searchFilterState.value}
/>
);
case 'tags':
return (
<TagsInputForm
{...tagsFilterState}
defaultValue={tagsFilterState.tags}
/>
);
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
}}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const filterLabels = {
startTime: 'Start Time',
status: 'Status',
version: 'Version',
tags: 'Tags',
};
14 changes: 13 additions & 1 deletion packages/console/src/components/Executions/filters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Record<string, string>>();
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',
};
}
154 changes: 154 additions & 0 deletions packages/console/src/components/common/TagsInputForm.tsx
Original file line number Diff line number Diff line change
@@ -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<TagsInputFormProps> = ({
label,
placeholder,
onChange,
defaultValue,
}) => {
const [tags, setTags] = React.useState<string[]>(defaultValue);
const [value, setValue] = React.useState('');
const composition = React.useRef(false);

const styles = useStyles();
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = ({
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 ? (
<Link
className={styles.resetLink}
component="button"
variant="body1"
onClick={handleClickReset}
>
Reset
</Link>
) : (
<div className={styles.resetLink} />
);

return (
<div>
<div className={styles.title}>
<FormLabel className={styles.listHeader}>{label}</FormLabel>
{resetControl}
</div>
<FormControl margin="dense" variant="outlined" fullWidth={true}>
<OutlinedInput
autoFocus={true}
className={styles.input}
labelWidth={0}
onChange={onInputChange}
onCompositionStart={() => (composition.current = true)}
onCompositionEnd={() => (composition.current = false)}
placeholder={placeholder}
type="text"
value={value}
onKeyDown={e => {
if (e.key === 'Enter' && !composition.current) {
addTag();
}
}}
endAdornment={
<InputAdornment position="end">
<IconButton component="span" size="small" onClick={addTag}>
<Add />
</IconButton>
</InputAdornment>
}
/>
<div className={styles.tagStack}>
{tags.map(tag => {
return (
<Chip
key={tag}
label={tag}
color="default"
size="small"
onDelete={() => removeTag(tag)}
style={{
backgroundColor: getColorFromString(tag),
}}
/>
);
})}
</div>
</FormControl>
</div>
);
};
19 changes: 19 additions & 0 deletions packages/console/src/components/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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>(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)) +
'%)'
);
};

0 comments on commit 22d512a

Please sign in to comment.