From c541796778e3bee068ff3cf3af8ce22bfec0c8a1 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Fri, 8 Apr 2022 07:56:32 -0700 Subject: [PATCH 01/13] feat: archive workflows base Signed-off-by: Carina Ursu --- src/components/Project/ProjectWorkflows.tsx | 30 +-- .../Workflow/SearchableWorkflowNameList.tsx | 213 +++++++++++++++++- .../SearchableWorkflowNameList.stories.tsx | 9 +- .../filters/useWorkflowShowArchivedState.ts | 34 +++ src/components/Workflow/types.ts | 3 + .../Workflow/useWorkflowInfoList.ts | 3 +- src/components/Workflow/utils.ts | 14 ++ src/models/Workflow/api.ts | 31 ++- src/models/Workflow/enums.ts | 11 + src/models/__mocks__/sampleWorkflowNames.ts | 2 + 10 files changed, 322 insertions(+), 28 deletions(-) create mode 100644 src/components/Workflow/filters/useWorkflowShowArchivedState.ts create mode 100644 src/components/Workflow/utils.ts create mode 100644 src/models/Workflow/enums.ts diff --git a/src/components/Project/ProjectWorkflows.tsx b/src/components/Project/ProjectWorkflows.tsx index 5d0d151ed..1ab38b99d 100644 --- a/src/components/Project/ProjectWorkflows.tsx +++ b/src/components/Project/ProjectWorkflows.tsx @@ -1,8 +1,8 @@ import { WaitForData } from 'components/common/WaitForData'; +import { useWorkflowShowArchivedState } from 'components/Workflow/filters/useWorkflowShowArchivedState'; import { SearchableWorkflowNameList } from 'components/Workflow/SearchableWorkflowNameList'; -import { Admin } from 'flyteidl'; import { limits } from 'models/AdminEntity/constants'; -import { FilterOperationName, SortDirection } from 'models/AdminEntity/types'; +import { SortDirection } from 'models/AdminEntity/types'; import { workflowSortFields } from 'models/Workflow/constants'; import * as React from 'react'; import { useWorkflowInfoList } from '../Workflow/useWorkflowInfoList'; @@ -12,33 +12,33 @@ export interface ProjectWorkflowsProps { domainId: string; } +const defaultSort = { + direction: SortDirection.ASCENDING, + key: workflowSortFields.name, +}; + /** A listing of the Workflows registered for a project */ export const ProjectWorkflows: React.FC = ({ domainId: domain, projectId: project, }) => { + const archivedFilter = useWorkflowShowArchivedState(); const workflows = useWorkflowInfoList( { domain, project }, { limit: limits.NONE, - sort: { - direction: SortDirection.ASCENDING, - key: workflowSortFields.name, - }, - // Hide archived workflows from the list - filter: [ - { - key: 'state', - operation: FilterOperationName.EQ, - value: Admin.NamedEntityState.NAMED_ENTITY_ACTIVE, - }, - ], + sort: defaultSort, + filter: [archivedFilter.getFilter()!], }, ); return ( - + ); }; diff --git a/src/components/Workflow/SearchableWorkflowNameList.tsx b/src/components/Workflow/SearchableWorkflowNameList.tsx index 4d4e6bc34..511caf190 100644 --- a/src/components/Workflow/SearchableWorkflowNameList.tsx +++ b/src/components/Workflow/SearchableWorkflowNameList.tsx @@ -11,27 +11,93 @@ import { WorkflowExecutionPhase } from 'models/Execution/enums'; import { Shimmer } from 'components/common/Shimmer'; import { WorkflowExecutionIdentifier } from 'models/Execution/types'; import { debounce } from 'lodash'; -import { Typography } from '@material-ui/core'; +import { + IconButton, + Typography, + FormControlLabel, + Checkbox, + FormGroup, + Button, + CircularProgress, +} from '@material-ui/core'; +import UnarchiveOutline from '@material-ui/icons/UnarchiveOutlined'; +import ArchiveOutlined from '@material-ui/icons/ArchiveOutlined'; +import { useMutation } from 'react-query'; +import { WorkflowExecutionState } from 'models/Workflow/enums'; +import { updateWorkflowState } from 'models/Workflow/api'; +import { useState } from 'react'; +import { useSnackbar } from 'notistack'; import { WorkflowListStructureItem } from './types'; import ProjectStatusBar from '../Project/ProjectStatusBar'; import { workflowNoInputsString } from '../Launch/LaunchForm/constants'; import { SearchableInput } from '../common/SearchableList'; import { useSearchableListState } from '../common/useSearchableListState'; import { useWorkflowInfoItem } from './useWorkflowInfoItem'; +import t from '../Executions/Tables/WorkflowExecutionTable/strings'; +import { getArchiveStateString, isWorkflowArchived } from './utils'; interface SearchableWorkflowNameItemProps { item: WorkflowListStructureItem; } +interface SearchableWorkflowNameItemActionsProps { + item: WorkflowListStructureItem; + setHideItem: (hide: boolean) => void; +} + interface SearchableWorkflowNameListProps { workflows: WorkflowListStructureItem[]; + onArchiveFilterChange: (showArchievedItems: boolean) => void; + showArchived: boolean; } +export const showOnHoverClass = 'showOnHover'; + const useStyles = makeStyles(() => ({ + actionContainer: { + display: 'block', + position: 'absolute', + top: 0, + right: 0, + height: '100%', + }, + actionProgress: { + width: '100px', + textAlign: 'center', + top: '42%', + display: 'block', + position: 'absolute', + right: 0, + }, + archiveButton: { + right: '30px', + position: 'relative', + top: '42%', + height: 'auto', + }, + archiveCheckbox: { + whiteSpace: 'nowrap', + }, + confirmationBox: { + height: '100%', + [`& > button`]: { + height: '100%', + }, + }, + confirmationButton: { + borderRadius: 0, + minWidth: '100px', + minHeight: '53px', + }, container: { padding: 13, paddingRight: 71, }, + filterGroup: { + display: 'flex', + flexWrap: 'nowrap', + flexDirection: 'row', + }, itemContainer: { marginBottom: 15, borderRadius: 16, @@ -40,6 +106,15 @@ const useStyles = makeStyles(() => ({ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', + position: 'relative', + // All children using the showOnHover class will be hidden until + // the mouse enters the container + [`& .${showOnHoverClass}`]: { + opacity: 0, + }, + [`&:hover .${showOnHoverClass}`]: { + opacity: 1, + }, }, itemName: { display: 'flex', @@ -98,6 +173,105 @@ const padExecutionPaths = (items: WorkflowExecutionIdentifier[]) => { return [...items.map((id) => Routes.ExecutionDetails.makeUrl(id)), ...emptyExecutions].reverse(); }; +const getArchiveIcon = (isArchived: boolean) => + isArchived ? : ; + +const SearchableWorkflowNameItemActions: React.FC = + React.memo(({ item, setHideItem }) => { + const styles = useStyles(); + const { enqueueSnackbar } = useSnackbar(); + const { id } = item; + const isArchived = isWorkflowArchived(item); + const [isUpdating, setIsUpdating] = useState(false); + const [showConfirmation, setShowConfirmation] = useState(false); + + const mutation = useMutation( + (newState: WorkflowExecutionState) => updateWorkflowState(id, newState), + { + onMutate: () => setIsUpdating(true), + onSuccess: () => { + enqueueSnackbar(t('archiveSuccess', !isArchived), { + variant: 'success', + }); + setHideItem(true); + }, + onError: () => { + enqueueSnackbar(`${mutation.error ?? t('archiveError', !isArchived)}`, { + variant: 'error', + }); + }, + onSettled: () => { + setShowConfirmation(false); + setIsUpdating(false); + }, + }, + ); + + const onArchiveClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + setShowConfirmation(true); + }; + + const onConfirmArchiveClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + mutation.mutate( + isWorkflowArchived(item) + ? WorkflowExecutionState.NAMED_ENTITY_ACTIVE + : WorkflowExecutionState.NAMED_ENTITY_ARCHIVED, + ); + }; + + const onCancelClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + setShowConfirmation(false); + }; + + return isUpdating ? ( +
+ +
+ ) : ( +
+ {showConfirmation ? ( +
+ + +
+ ) : ( + + {getArchiveIcon(isArchived)} + + )} +
+ ); + }); + /** * Renders individual searchable workflow item * @param item @@ -111,6 +285,12 @@ const SearchableWorkflowNameItem: React.FC = Re const { id, description } = item; const { data: workflow, isLoading } = useWorkflowInfoItem(id); + const [hideItem, setHideItem] = useState(false); + + if (hideItem) { + return null; + } + return ( = Re {isLoading ? : workflow?.outputs ?? No output data found.} + ); @@ -174,6 +355,8 @@ const SearchableWorkflowNameItem: React.FC = Re */ export const SearchableWorkflowNameList: React.FC = ({ workflows, + onArchiveFilterChange, + showArchived, }) => { const styles = useStyles(); const [search, setSearch] = React.useState(''); @@ -192,14 +375,26 @@ export const SearchableWorkflowNameList: React.FC - + + + onArchiveFilterChange(checked)} + /> + } + label="Show Only Archived Workflows" + /> +
{results.map(({ value }) => (
{story()}
); -stories.add('basic', () => ); +stories.add('basic', () => ( + +)); diff --git a/src/components/Workflow/filters/useWorkflowShowArchivedState.ts b/src/components/Workflow/filters/useWorkflowShowArchivedState.ts new file mode 100644 index 000000000..97d93843e --- /dev/null +++ b/src/components/Workflow/filters/useWorkflowShowArchivedState.ts @@ -0,0 +1,34 @@ +import { useState } from 'react'; +import { FilterOperation, FilterOperationName } from 'models/AdminEntity/types'; +import { WorkflowExecutionState } from 'models/Workflow/enums'; + +interface ArchiveFilterState { + showArchived: boolean; + setShowArchived: (newValue: boolean) => void; + getFilter: () => FilterOperation | null; +} + +/** + * Allows to filter by Archive state + */ +export function useWorkflowShowArchivedState(): ArchiveFilterState { + const [showArchived, setShowArchived] = useState(false); + + // By default all values are returned with NAMED_ENTITY_ACTIVE state, + // so filter need to be applied only for ARCHIVED executions + const getFilter = (): FilterOperation | null => { + return { + key: 'state', + operation: FilterOperationName.EQ, + value: showArchived + ? WorkflowExecutionState.NAMED_ENTITY_ARCHIVED + : WorkflowExecutionState.NAMED_ENTITY_ACTIVE, + }; + }; + + return { + showArchived, + setShowArchived, + getFilter, + }; +} diff --git a/src/components/Workflow/types.ts b/src/components/Workflow/types.ts index 4bbc0d87c..7fa3b787e 100644 --- a/src/components/Workflow/types.ts +++ b/src/components/Workflow/types.ts @@ -2,6 +2,7 @@ import { WorkflowId } from 'models/Workflow/types'; import { WorkflowExecutionPhase } from 'models/Execution/enums'; import { WorkflowExecutionIdentifier } from 'models/Execution/types'; import { NamedEntityIdentifier } from 'models/Common/types'; +import { WorkflowExecutionState } from 'models/Workflow/enums'; export type WorkflowListItem = { id: WorkflowId; @@ -11,9 +12,11 @@ export type WorkflowListItem = { executionStatus?: WorkflowExecutionPhase[]; executionIds?: WorkflowExecutionIdentifier[]; description?: string; + state: WorkflowExecutionState; }; export type WorkflowListStructureItem = { id: NamedEntityIdentifier; description: string; + state: WorkflowExecutionState; }; diff --git a/src/components/Workflow/useWorkflowInfoList.ts b/src/components/Workflow/useWorkflowInfoList.ts index d46eeb548..5560bb3e7 100644 --- a/src/components/Workflow/useWorkflowInfoList.ts +++ b/src/components/Workflow/useWorkflowInfoList.ts @@ -16,9 +16,10 @@ export const useWorkflowInfoList = (scope: DomainIdentifierScope, config?: Reque ); return { - entities: entities.map(({ id, metadata: { description } }) => ({ + entities: entities.map(({ id, metadata: { description, state } }) => ({ id, description, + state, })), ...rest, }; diff --git a/src/components/Workflow/utils.ts b/src/components/Workflow/utils.ts new file mode 100644 index 000000000..881f0b9b4 --- /dev/null +++ b/src/components/Workflow/utils.ts @@ -0,0 +1,14 @@ +import { WorkflowExecutionState } from 'models/Workflow/enums'; +import { WorkflowListStructureItem } from './types'; + +function isWorkflowStateArchive(workflow: WorkflowListStructureItem): boolean { + const state = workflow?.state ?? null; + return !!state && state === WorkflowExecutionState.NAMED_ENTITY_ARCHIVED; +} +export function isWorkflowArchived(workflow: WorkflowListStructureItem): boolean { + return isWorkflowStateArchive(workflow); +} + +export function getArchiveStateString(workflow: WorkflowListStructureItem) { + return isWorkflowStateArchive(workflow) ? 'Unarchive' : 'Archive'; +} diff --git a/src/models/Workflow/api.ts b/src/models/Workflow/api.ts index 162e22349..72094e2d9 100644 --- a/src/models/Workflow/api.ts +++ b/src/models/Workflow/api.ts @@ -1,8 +1,10 @@ -import { Admin } from 'flyteidl'; -import { getAdminEntity } from 'models/AdminEntity/AdminEntity'; +import { Admin, Core } from 'flyteidl'; +import { getAdminEntity, postAdminEntity } from 'models/AdminEntity/AdminEntity'; import { defaultPaginationConfig } from 'models/AdminEntity/constants'; import { RequestConfig } from 'models/AdminEntity/types'; import { Identifier, IdentifierScope } from 'models/Common/types'; +import { makeNamedEntityPath } from 'models/Common/utils'; +import { WorkflowExecutionState } from './enums'; import { Workflow } from './types'; import { makeWorkflowPath, workflowListTransformer } from './utils'; @@ -26,3 +28,28 @@ export const getWorkflow = (id: Identifier, config?: RequestConfig) => }, config, ); + +/** Updates `Workflow` archive state */ +export const updateWorkflowState = ( + id: Admin.NamedEntityIdentifier, + newState: WorkflowExecutionState, + config?: RequestConfig, +) => { + const path = makeNamedEntityPath({ resourceType: Core.ResourceType.WORKFLOW, ...id }); + return postAdminEntity( + { + data: { + resourceType: Core.ResourceType.WORKFLOW, + id, + metadata: { + state: newState, + }, + }, + path, + requestMessageType: Admin.NamedEntityUpdateRequest, + responseMessageType: Admin.NamedEntityUpdateResponse, + method: 'put', + }, + config, + ); +}; diff --git a/src/models/Workflow/enums.ts b/src/models/Workflow/enums.ts new file mode 100644 index 000000000..4b453637f --- /dev/null +++ b/src/models/Workflow/enums.ts @@ -0,0 +1,11 @@ +import { Admin } from 'flyteidl'; + +/** These enums are only aliased and exported from this file. They should + * be imported directly from here to avoid runtime errors when TS processes + * modules individually (such as when running with ts-jest) + */ + +/* It's an ENUM exports, and as such need to be exported as both type and const value */ +/* eslint-disable @typescript-eslint/no-redeclare */ +export type WorkflowExecutionState = Admin.NamedEntityState; +export const WorkflowExecutionState = Admin.NamedEntityState; diff --git a/src/models/__mocks__/sampleWorkflowNames.ts b/src/models/__mocks__/sampleWorkflowNames.ts index b1b95aef6..f80342771 100644 --- a/src/models/__mocks__/sampleWorkflowNames.ts +++ b/src/models/__mocks__/sampleWorkflowNames.ts @@ -1,4 +1,5 @@ import { WorkflowListStructureItem } from 'components/Workflow/types'; +import { WorkflowExecutionState } from 'models/Workflow/enums'; import { WorkflowId } from 'models/Workflow/types'; export const sampleWorkflowIds: WorkflowId[] = [ @@ -37,4 +38,5 @@ export const sampleWorkflowIds: WorkflowId[] = [ export const sampleWorkflowNames: WorkflowListStructureItem[] = sampleWorkflowIds.map((id) => ({ id, description: '', + state: WorkflowExecutionState.NAMED_ENTITY_ACTIVE, })); From b65fc97fe681d8aa6dd82efd5c3f8c33383eddf9 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 11 Apr 2022 09:49:14 -0700 Subject: [PATCH 02/13] chore: comments Signed-off-by: Carina Ursu --- src/components/Project/ProjectWorkflows.tsx | 6 +++--- .../Workflow/filters/useWorkflowShowArchivedState.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Project/ProjectWorkflows.tsx b/src/components/Project/ProjectWorkflows.tsx index 1ab38b99d..aaa431079 100644 --- a/src/components/Project/ProjectWorkflows.tsx +++ b/src/components/Project/ProjectWorkflows.tsx @@ -12,7 +12,7 @@ export interface ProjectWorkflowsProps { domainId: string; } -const defaultSort = { +const DEFAULT_SORT = { direction: SortDirection.ASCENDING, key: workflowSortFields.name, }; @@ -27,8 +27,8 @@ export const ProjectWorkflows: React.FC = ({ { domain, project }, { limit: limits.NONE, - sort: defaultSort, - filter: [archivedFilter.getFilter()!], + sort: DEFAULT_SORT, + filter: [archivedFilter.getFilter()], }, ); diff --git a/src/components/Workflow/filters/useWorkflowShowArchivedState.ts b/src/components/Workflow/filters/useWorkflowShowArchivedState.ts index 97d93843e..f119fc683 100644 --- a/src/components/Workflow/filters/useWorkflowShowArchivedState.ts +++ b/src/components/Workflow/filters/useWorkflowShowArchivedState.ts @@ -5,7 +5,7 @@ import { WorkflowExecutionState } from 'models/Workflow/enums'; interface ArchiveFilterState { showArchived: boolean; setShowArchived: (newValue: boolean) => void; - getFilter: () => FilterOperation | null; + getFilter: () => FilterOperation; } /** @@ -16,7 +16,7 @@ export function useWorkflowShowArchivedState(): ArchiveFilterState { // By default all values are returned with NAMED_ENTITY_ACTIVE state, // so filter need to be applied only for ARCHIVED executions - const getFilter = (): FilterOperation | null => { + const getFilter = (): FilterOperation => { return { key: 'state', operation: FilterOperationName.EQ, From 2c90136e03f137271bbc64a7f7a086f39bef168d Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 11 Apr 2022 11:48:08 -0700 Subject: [PATCH 03/13] chore: test Signed-off-by: Carina Ursu --- .../Project/test/ProjectWorkflows.test.tsx | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/components/Project/test/ProjectWorkflows.test.tsx b/src/components/Project/test/ProjectWorkflows.test.tsx index 8a4d785c5..31cb87a83 100644 --- a/src/components/Project/test/ProjectWorkflows.test.tsx +++ b/src/components/Project/test/ProjectWorkflows.test.tsx @@ -1,22 +1,36 @@ -import { render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import { APIContext } from 'components/data/apiContext'; import { mockAPIContextValue } from 'components/data/__mocks__/apiContext'; import { Admin } from 'flyteidl'; import { FilterOperationName } from 'models/AdminEntity/types'; -import { listNamedEntities } from 'models/Common/api'; -import { NamedEntity } from 'models/Common/types'; +import { getUserProfile, listNamedEntities } from 'models/Common/api'; +import { NamedEntity, UserProfile } from 'models/Common/types'; import * as React from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; import { MemoryRouter } from 'react-router'; import { createWorkflowName } from 'test/modelUtils'; +import { createTestQueryClient } from 'test/utils'; import { ProjectWorkflows } from '../ProjectWorkflows'; +const sampleUserProfile: UserProfile = { + subject: 'subject', +} as UserProfile; + +jest.mock('notistack', () => ({ + useSnackbar: () => ({ enqueueSnackbar: jest.fn() }), +})); + describe('ProjectWorkflows', () => { const project = 'TestProject'; const domain = 'TestDomain'; let workflowNames: NamedEntity[]; + let queryClient: QueryClient; let mockListNamedEntities: jest.Mock>; + let mockGetUserProfile: jest.Mock>; beforeEach(() => { + mockGetUserProfile = jest.fn().mockResolvedValue(null); + queryClient = createTestQueryClient(); workflowNames = ['MyWorkflow', 'MyOtherWorkflow'].map((name) => createWorkflowName({ domain, name, project }), ); @@ -25,13 +39,16 @@ describe('ProjectWorkflows', () => { const renderComponent = () => render( - - - , + + + + + , { wrapper: MemoryRouter }, ); @@ -52,4 +69,28 @@ describe('ProjectWorkflows', () => { }), ); }); + + it('should display checkbox if user login', async () => { + mockGetUserProfile.mockResolvedValue(sampleUserProfile); + const { getAllByRole } = renderComponent(); + await waitFor(() => {}); + const checkboxes = getAllByRole(/checkbox/i) as HTMLInputElement[]; + expect(checkboxes).toHaveLength(1); + expect(checkboxes[0]).toBeTruthy(); + expect(checkboxes[0]?.checked).toEqual(false); + }); + + /** user doesn't have its own workflow */ + it('clicking show archived should hide active workflows', async () => { + mockGetUserProfile.mockResolvedValue(sampleUserProfile); + const { getByText, queryByText, getAllByRole } = renderComponent(); + await waitFor(() => {}); + const checkboxes = getAllByRole(/checkbox/i) as HTMLInputElement[]; + expect(checkboxes[0]).toBeTruthy(); + expect(checkboxes[0]?.checked).toEqual(false); + await waitFor(() => expect(getByText('MyWorkflow'))); + fireEvent.click(checkboxes[0]); + // when user selects checkbox, table should have no workflows to display + await waitFor(() => expect(queryByText('MyWorkflow')).toBeNull()); + }); }); From 89130bf05efd4a921b93cd5e7397c758d9d49617 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 11 Apr 2022 11:59:21 -0700 Subject: [PATCH 04/13] chore: comments Signed-off-by: Carina Ursu --- .../Workflow/SearchableWorkflowNameList.tsx | 13 ++++----- .../SearchableWorkflowNameList.stories.tsx | 27 +++++++++++++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/components/Workflow/SearchableWorkflowNameList.tsx b/src/components/Workflow/SearchableWorkflowNameList.tsx index 511caf190..957b564c2 100644 --- a/src/components/Workflow/SearchableWorkflowNameList.tsx +++ b/src/components/Workflow/SearchableWorkflowNameList.tsx @@ -1,4 +1,4 @@ -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles, Theme } from '@material-ui/core/styles'; import DeviceHub from '@material-ui/icons/DeviceHub'; import classNames from 'classnames'; import { useNamedEntityListStyles } from 'components/common/SearchableNamedEntityList'; @@ -53,7 +53,7 @@ interface SearchableWorkflowNameListProps { export const showOnHoverClass = 'showOnHover'; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles((theme: Theme) => ({ actionContainer: { display: 'block', position: 'absolute', @@ -90,14 +90,16 @@ const useStyles = makeStyles(() => ({ minHeight: '53px', }, container: { - padding: 13, - paddingRight: 71, + padding: theme.spacing(2), + paddingRight: theme.spacing(5), }, filterGroup: { display: 'flex', flexWrap: 'nowrap', flexDirection: 'row', + margin: theme.spacing(4, 5, 2, 2), }, + itemContainer: { marginBottom: 15, borderRadius: 16, @@ -146,8 +148,7 @@ const useStyles = makeStyles(() => ({ color: workflowLabelColor, }, searchInputContainer: { - padding: '0 13px', - margin: '32px 0 23px', + paddingLeft: 0, }, w100: { flex: 1, diff --git a/src/components/Workflow/__stories__/SearchableWorkflowNameList.stories.tsx b/src/components/Workflow/__stories__/SearchableWorkflowNameList.stories.tsx index c66172cb4..6d777f796 100644 --- a/src/components/Workflow/__stories__/SearchableWorkflowNameList.stories.tsx +++ b/src/components/Workflow/__stories__/SearchableWorkflowNameList.stories.tsx @@ -1,17 +1,34 @@ +import { Collapse } from '@material-ui/core'; import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import { sampleWorkflowNames } from 'models/__mocks__/sampleWorkflowNames'; +import { SnackbarProvider } from 'notistack'; import * as React from 'react'; import { SearchableWorkflowNameList } from '../SearchableWorkflowNameList'; const baseProps = { workflows: [...sampleWorkflowNames] }; +// wrapper - to ensure that error/success notification shown as expected in storybook +const Wrapper = (props) => { + return ( + + {props.children} + + ); +}; + const stories = storiesOf('Workflow/SearchableWorkflowNameList', module); stories.addDecorator((story) =>
{story()}
); stories.add('basic', () => ( - + + + )); From 4190288c2c64fd6e1b57322926de91a0da471522 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 11 Apr 2022 12:03:40 -0700 Subject: [PATCH 05/13] chore: comments Signed-off-by: Carina Ursu --- .../Workflow/SearchableWorkflowNameList.tsx | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/components/Workflow/SearchableWorkflowNameList.tsx b/src/components/Workflow/SearchableWorkflowNameList.tsx index 957b564c2..6ae7f1538 100644 --- a/src/components/Workflow/SearchableWorkflowNameList.tsx +++ b/src/components/Workflow/SearchableWorkflowNameList.tsx @@ -55,29 +55,21 @@ export const showOnHoverClass = 'showOnHover'; const useStyles = makeStyles((theme: Theme) => ({ actionContainer: { - display: 'block', - position: 'absolute', - top: 0, + display: 'flex', right: 0, - height: '100%', - }, - actionProgress: { - width: '100px', - textAlign: 'center', - top: '42%', - display: 'block', + top: 0, position: 'absolute', - right: 0, - }, - archiveButton: { - right: '30px', - position: 'relative', - top: '42%', - height: 'auto', + height: '100%', + overflow: 'hidden', + borderRadius: '0px 16px 16px 0px', // to ensure that cancel button will have rounded corners on the right side }, archiveCheckbox: { whiteSpace: 'nowrap', }, + centeredChild: { + alignItems: 'center', + padding: theme.spacing(2), + }, confirmationBox: { height: '100%', [`& > button`]: { @@ -235,7 +227,7 @@ const SearchableWorkflowNameItemActions: React.FC
) : ( -
+
{showConfirmation ? (
) : ( - - {getArchiveIcon(isArchived)} - +
+ + {getArchiveIcon(isArchived)} + +
)}
); From 7ae70e963715dae491fa8a596e4d4b28f172e224 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 11 Apr 2022 16:09:35 -0700 Subject: [PATCH 11/13] chore: make the buttons work Signed-off-by: Carina Ursu --- .../Workflow/SearchableWorkflowNameList.tsx | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/Workflow/SearchableWorkflowNameList.tsx b/src/components/Workflow/SearchableWorkflowNameList.tsx index ec735a267..9e8e361b2 100644 --- a/src/components/Workflow/SearchableWorkflowNameList.tsx +++ b/src/components/Workflow/SearchableWorkflowNameList.tsx @@ -71,6 +71,7 @@ const useStyles = makeStyles((theme: Theme) => ({ top: '50%', marginTop: -24, marginRight: 24, + height: 'fit-content', }, confirmationBox: { height: '100%', @@ -227,13 +228,13 @@ const SearchableWorkflowNameItemActions: React.FC - -
- ) : ( + return (
- {showConfirmation ? ( + {isUpdating ? ( + + + + ) : showConfirmation ? (
) : ( -
- - {getArchiveIcon(isArchived)} - -
+ + {getArchiveIcon(isArchived)} + )}
); From 6b0839f76ae0631ed965c372981e2e6b6764bafc Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 11 Apr 2022 16:32:35 -0700 Subject: [PATCH 12/13] chore: final fix button Signed-off-by: Carina Ursu --- .../Workflow/SearchableWorkflowNameList.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/components/Workflow/SearchableWorkflowNameList.tsx b/src/components/Workflow/SearchableWorkflowNameList.tsx index 9e8e361b2..391a7cafc 100644 --- a/src/components/Workflow/SearchableWorkflowNameList.tsx +++ b/src/components/Workflow/SearchableWorkflowNameList.tsx @@ -66,12 +66,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, centeredChild: { alignItems: 'center', - padding: theme.spacing(2), - borderRadius: '50%', - top: '50%', - marginTop: -24, marginRight: 24, - height: 'fit-content', }, confirmationBox: { height: '100%', @@ -228,10 +223,11 @@ const SearchableWorkflowNameItemActions: React.FC +
{isUpdating ? ( - + ) : showConfirmation ? ( @@ -258,12 +254,7 @@ const SearchableWorkflowNameItemActions: React.FC
) : ( - + {getArchiveIcon(isArchived)} )} From c6f0d53032ae8fb100c8b2021b41cbae6df9c387 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 11 Apr 2022 16:39:15 -0700 Subject: [PATCH 13/13] chore: comments Signed-off-by: Carina Ursu --- .../Project/test/ProjectWorkflows.test.tsx | 2 +- .../Workflow/SearchableWorkflowNameList.tsx | 178 +++++++++--------- .../filters/useWorkflowShowArchivedState.ts | 3 +- src/components/Workflow/utils.ts | 1 + 4 files changed, 90 insertions(+), 94 deletions(-) diff --git a/src/components/Project/test/ProjectWorkflows.test.tsx b/src/components/Project/test/ProjectWorkflows.test.tsx index 31cb87a83..f47090c81 100644 --- a/src/components/Project/test/ProjectWorkflows.test.tsx +++ b/src/components/Project/test/ProjectWorkflows.test.tsx @@ -91,6 +91,6 @@ describe('ProjectWorkflows', () => { await waitFor(() => expect(getByText('MyWorkflow'))); fireEvent.click(checkboxes[0]); // when user selects checkbox, table should have no workflows to display - await waitFor(() => expect(queryByText('MyWorkflow')).toBeNull()); + await waitFor(() => expect(queryByText('MyWorkflow')).not.toBeInTheDocument()); }); }); diff --git a/src/components/Workflow/SearchableWorkflowNameList.tsx b/src/components/Workflow/SearchableWorkflowNameList.tsx index 391a7cafc..14cd92f9d 100644 --- a/src/components/Workflow/SearchableWorkflowNameList.tsx +++ b/src/components/Workflow/SearchableWorkflowNameList.tsx @@ -68,12 +68,6 @@ const useStyles = makeStyles((theme: Theme) => ({ alignItems: 'center', marginRight: 24, }, - confirmationBox: { - height: '100%', - [`& > button`]: { - height: '100%', - }, - }, confirmationButton: { borderRadius: 0, minWidth: '100px', @@ -170,97 +164,99 @@ const padExecutionPaths = (items: WorkflowExecutionIdentifier[]) => { const getArchiveIcon = (isArchived: boolean) => isArchived ? : ; -const SearchableWorkflowNameItemActions: React.FC = - React.memo(({ item, setHideItem }) => { - const styles = useStyles(); - const { enqueueSnackbar } = useSnackbar(); - const { id } = item; - const isArchived = isWorkflowArchived(item); - const [isUpdating, setIsUpdating] = useState(false); - const [showConfirmation, setShowConfirmation] = useState(false); +const SearchableWorkflowNameItemActions: React.FC = ({ + item, + setHideItem, +}) => { + const styles = useStyles(); + const { enqueueSnackbar } = useSnackbar(); + const { id } = item; + const isArchived = isWorkflowArchived(item); + const [isUpdating, setIsUpdating] = useState(false); + const [showConfirmation, setShowConfirmation] = useState(false); - const mutation = useMutation( - (newState: WorkflowExecutionState) => updateWorkflowState(id, newState), - { - onMutate: () => setIsUpdating(true), - onSuccess: () => { - enqueueSnackbar(t('archiveSuccess', !isArchived), { - variant: 'success', - }); - setHideItem(true); - }, - onError: () => { - enqueueSnackbar(`${mutation.error ?? t('archiveError', !isArchived)}`, { - variant: 'error', - }); - }, - onSettled: () => { - setShowConfirmation(false); - setIsUpdating(false); - }, + const mutation = useMutation( + (newState: WorkflowExecutionState) => updateWorkflowState(id, newState), + { + onMutate: () => setIsUpdating(true), + onSuccess: () => { + enqueueSnackbar(t('archiveSuccess', !isArchived), { + variant: 'success', + }); + setHideItem(true); }, - ); + onError: () => { + enqueueSnackbar(`${mutation.error ?? t('archiveError', !isArchived)}`, { + variant: 'error', + }); + }, + onSettled: () => { + setShowConfirmation(false); + setIsUpdating(false); + }, + }, + ); - const onArchiveClick = (event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - setShowConfirmation(true); - }; + const onArchiveClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + setShowConfirmation(true); + }; - const onConfirmArchiveClick = (event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - mutation.mutate( - isWorkflowArchived(item) - ? WorkflowExecutionState.NAMED_ENTITY_ACTIVE - : WorkflowExecutionState.NAMED_ENTITY_ARCHIVED, - ); - }; + const onConfirmArchiveClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + mutation.mutate( + isWorkflowArchived(item) + ? WorkflowExecutionState.NAMED_ENTITY_ACTIVE + : WorkflowExecutionState.NAMED_ENTITY_ARCHIVED, + ); + }; - const onCancelClick = (event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - setShowConfirmation(false); - }; + const onCancelClick = (event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + setShowConfirmation(false); + }; - const singleItemStyle = isUpdating || !showConfirmation ? styles.centeredChild : ''; - return ( -
- {isUpdating ? ( - - - - ) : showConfirmation ? ( -
- - -
- ) : ( - - {getArchiveIcon(isArchived)} - - )} -
- ); - }); + const singleItemStyle = isUpdating || !showConfirmation ? styles.centeredChild : ''; + return ( +
+ {isUpdating ? ( + + + + ) : showConfirmation ? ( + <> + + + + ) : ( + + {getArchiveIcon(isArchived)} + + )} +
+ ); +}; /** * Renders individual searchable workflow item diff --git a/src/components/Workflow/filters/useWorkflowShowArchivedState.ts b/src/components/Workflow/filters/useWorkflowShowArchivedState.ts index f119fc683..9f9f4a017 100644 --- a/src/components/Workflow/filters/useWorkflowShowArchivedState.ts +++ b/src/components/Workflow/filters/useWorkflowShowArchivedState.ts @@ -14,8 +14,7 @@ interface ArchiveFilterState { export function useWorkflowShowArchivedState(): ArchiveFilterState { const [showArchived, setShowArchived] = useState(false); - // By default all values are returned with NAMED_ENTITY_ACTIVE state, - // so filter need to be applied only for ARCHIVED executions + // By default all values are returned with NAMED_ENTITY_ACTIVE state const getFilter = (): FilterOperation => { return { key: 'state', diff --git a/src/components/Workflow/utils.ts b/src/components/Workflow/utils.ts index 535c660cb..5486b08bc 100644 --- a/src/components/Workflow/utils.ts +++ b/src/components/Workflow/utils.ts @@ -5,6 +5,7 @@ function isWorkflowStateArchive(workflow: WorkflowListStructureItem): boolean { const state = workflow?.state ?? null; return !!state && state === WorkflowExecutionState.NAMED_ENTITY_ARCHIVED; } + export function isWorkflowArchived(workflow: WorkflowListStructureItem): boolean { return isWorkflowStateArchive(workflow); }