Skip to content

Commit

Permalink
feat: archive tasks (flyteorg#395)
Browse files Browse the repository at this point in the history
* feat: archive tasks

Signed-off-by: Carina Ursu <[email protected]>
  • Loading branch information
ursucarina authored Apr 16, 2022
1 parent 3acb21d commit 9ce77a7
Show file tree
Hide file tree
Showing 22 changed files with 603 additions and 89 deletions.
13 changes: 1 addition & 12 deletions src/components/Navigation/SearchableProjectList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Fade, Tooltip, Typography } from '@material-ui/core';
import { makeStyles, Theme } from '@material-ui/core/styles';
import classnames from 'classnames';
import { NoResults } from 'components/common/NoResults';
import { SearchableList, SearchResult } from 'components/common/SearchableList';
import { useCommonStyles } from 'components/common/styles';
import { defaultProjectDescription } from 'components/SelectProject/constants';
Expand All @@ -16,12 +17,6 @@ const useStyles = makeStyles((theme: Theme) => ({
flex: '1 0 0',
fontWeight: 'bold',
},
noResults: {
color: theme.palette.text.disabled,
display: 'flex',
justifyContent: 'center',
marginTop: theme.spacing(4),
},
searchResult: {
alignItems: 'center',
borderLeft: '4px solid transparent',
Expand All @@ -44,12 +39,6 @@ const useStyles = makeStyles((theme: Theme) => ({

type ProjectSelectedCallback = (project: Project) => void;

const NoResults: React.FC = () => (
<Typography className={useStyles().noResults} variant="body2" component="div">
No matching results
</Typography>
);

interface SearchResultsProps {
onProjectSelected: ProjectSelectedCallback;
results: SearchResult<Project>[];
Expand Down
20 changes: 15 additions & 5 deletions src/components/Project/ProjectTasks.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { WaitForData } from 'components/common/WaitForData';
import { useTaskNameList } from 'components/hooks/useNamedEntity';
import { SearchableTaskNameList } from 'components/Task/SearchableTaskNameList';
import { useTaskShowArchivedState } from 'components/Task/useTaskShowArchivedState';
import { limits } from 'models/AdminEntity/constants';
import { SortDirection } from 'models/AdminEntity/types';
import { taskSortFields } from 'models/Task/constants';
Expand All @@ -11,25 +12,34 @@ export interface ProjectTasksProps {
domainId: string;
}

const DEFAULT_SORT = {
direction: SortDirection.ASCENDING,
key: taskSortFields.name,
};

/** A listing of the Tasks registered for a project */
export const ProjectTasks: React.FC<ProjectTasksProps> = ({
domainId: domain,
projectId: project,
}) => {
const archivedFilter = useTaskShowArchivedState();

const taskNames = useTaskNameList(
{ domain, project },
{
limit: limits.NONE,
sort: {
direction: SortDirection.ASCENDING,
key: taskSortFields.name,
},
sort: DEFAULT_SORT,
filter: [archivedFilter.getFilter()],
},
);

return (
<WaitForData {...taskNames}>
<SearchableTaskNameList names={taskNames.value} />
<SearchableTaskNameList
names={taskNames.value}
showArchived={archivedFilter.showArchived}
onArchiveFilterChange={archivedFilter.setShowArchived}
/>
</WaitForData>
);
};
149 changes: 149 additions & 0 deletions src/components/Project/test/ProjectTask.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { APIContext } from 'components/data/apiContext';
import { mockAPIContextValue } from 'components/data/__mocks__/apiContext';
import { FilterOperationName } from 'models/AdminEntity/types';
import { getUserProfile, listNamedEntities } from 'models/Common/api';
import {
NamedEntity,
NamedEntityIdentifier,
NamedEntityMetadata,
ResourceType,
UserProfile,
} from 'models/Common/types';
import { NamedEntityState } from 'models/enums';
import { updateTaskState } from 'models/Task/api';
import * as React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router';
import { createNamedEntity } from 'test/modelUtils';
import { createTestQueryClient } from 'test/utils';
import { ProjectTasks } from '../ProjectTasks';

export function createTask(id: NamedEntityIdentifier, metadata?: Partial<NamedEntityMetadata>) {
return createNamedEntity(ResourceType.TASK, id, metadata);
}

const sampleUserProfile: UserProfile = {
subject: 'subject',
} as UserProfile;

jest.mock('notistack', () => ({
useSnackbar: () => ({ enqueueSnackbar: jest.fn() }),
}));

jest.mock('models/Task/api', () => ({
updateTaskState: jest.fn().mockResolvedValue({}),
}));

describe('ProjectTasks', () => {
const project = 'TestProject';
const domain = 'TestDomain';
let tasks: NamedEntity[];
let queryClient: QueryClient;
let mockListNamedEntities: jest.Mock<ReturnType<typeof listNamedEntities>>;
let mockGetUserProfile: jest.Mock<ReturnType<typeof getUserProfile>>;

beforeEach(() => {
mockGetUserProfile = jest.fn().mockResolvedValue(null);
queryClient = createTestQueryClient();
tasks = ['MyTask', 'MyOtherTask'].map((name) => createTask({ domain, name, project }));
mockListNamedEntities = jest.fn().mockResolvedValue({ entities: tasks });

window.IntersectionObserver = jest.fn().mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
});
});

const renderComponent = () =>
render(
<QueryClientProvider client={queryClient}>
<APIContext.Provider
value={mockAPIContextValue({
listNamedEntities: mockListNamedEntities,
getUserProfile: mockGetUserProfile,
})}
>
<ProjectTasks projectId={project} domainId={domain} />
</APIContext.Provider>
</QueryClientProvider>,
{ wrapper: MemoryRouter },
);

it('does not show archived tasks', async () => {
const { getByText } = renderComponent();
await waitFor(() => {});

expect(mockListNamedEntities).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
filter: [
{
key: 'state',
operation: FilterOperationName.EQ,
value: NamedEntityState.NAMED_ENTITY_ACTIVE,
},
],
}),
);
await waitFor(() => expect(getByText('MyTask')));
});

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);
});

it('should display archive button', async () => {
mockGetUserProfile.mockResolvedValue(sampleUserProfile);
const { getByText, getAllByTitle, findAllByText } = renderComponent();
await waitFor(() => {});

const task = getByText('MyTask');
expect(task).toBeTruthy();

const parent = task?.parentElement?.parentElement?.parentElement!;
fireEvent.mouseOver(parent);

const archiveButton = getAllByTitle('Archive');
expect(archiveButton[0]).toBeTruthy();

fireEvent.click(archiveButton[0]);

const cancelButton = await findAllByText('Cancel');
await waitFor(() => expect(cancelButton.length).toEqual(1));
const confirmArchiveButton = cancelButton?.[0]?.parentElement?.parentElement?.children[0]!;

expect(confirmArchiveButton).toBeTruthy();

fireEvent.click(confirmArchiveButton!);

await waitFor(() => {
expect(updateTaskState).toHaveBeenCalledTimes(1);
});
});

it('clicking show archived should hide active tasks', async () => {
mockGetUserProfile.mockResolvedValue(sampleUserProfile);
const { getByText, queryByText, getAllByRole } = renderComponent();
await waitFor(() => {});

// check the checkbox is present
const checkboxes = getAllByRole(/checkbox/i) as HTMLInputElement[];
expect(checkboxes[0]).toBeTruthy();
expect(checkboxes[0]?.checked).toEqual(false);

// check that my task is in document
await waitFor(() => expect(getByText('MyTask')));
fireEvent.click(checkboxes[0]);

// when user selects checkbox, table should have no tasks to display
await waitFor(() => expect(queryByText('MyTask')).not.toBeInTheDocument());
});
});
4 changes: 2 additions & 2 deletions src/components/Project/test/ProjectWorkflows.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
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 { getUserProfile, listNamedEntities } from 'models/Common/api';
import { NamedEntity, UserProfile } from 'models/Common/types';
import { NamedEntityState } from 'models/enums';
import * as React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router';
Expand Down Expand Up @@ -63,7 +63,7 @@ describe('ProjectWorkflows', () => {
{
key: 'state',
operation: FilterOperationName.EQ,
value: Admin.NamedEntityState.NAMED_ENTITY_ACTIVE,
value: NamedEntityState.NAMED_ENTITY_ACTIVE,
},
],
}),
Expand Down
Loading

0 comments on commit 9ce77a7

Please sign in to comment.