diff --git a/frontend/src/pages/AllRunsAndArchive.test.jsx.tsx b/frontend/src/pages/AllRunsAndArchive.test.tsx
similarity index 100%
rename from frontend/src/pages/AllRunsAndArchive.test.jsx.tsx
rename to frontend/src/pages/AllRunsAndArchive.test.tsx
diff --git a/frontend/src/pages/ExperimentDetails.test.tsx b/frontend/src/pages/ExperimentDetails.test.tsx
index 71ca9ed9977..d868b61384f 100644
--- a/frontend/src/pages/ExperimentDetails.test.tsx
+++ b/frontend/src/pages/ExperimentDetails.test.tsx
@@ -17,16 +17,15 @@
import * as React from 'react';
import EnhancedExperimentDetails, { ExperimentDetails } from './ExperimentDetails';
import TestUtils from '../TestUtils';
-import { ApiExperiment } from '../apis/experiment';
+import { ApiExperiment, ExperimentStorageState } from '../apis/experiment';
import { Apis } from '../lib/Apis';
import { PageProps } from './Page';
import { ReactWrapper, ShallowWrapper, shallow } from 'enzyme';
import { RoutePage, RouteParams, QUERY_PARAMS } from '../components/Router';
-import { RunStorageState } from '../apis/run';
import { ToolbarProps } from '../components/Toolbar';
import { range } from 'lodash';
import { ButtonKeys } from '../lib/Buttons';
-import { render } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
import { NamespaceContext } from 'src/lib/KubeflowClient';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
@@ -234,7 +233,22 @@ describe('ExperimentDetails', () => {
tree = shallow();
await TestUtils.flushPromises();
- expect(tree.find('RunList').prop('storageState')).toBe(RunStorageState.AVAILABLE.toString());
+ expect(tree.find('RunListsRouter').prop('storageState')).toBe(ExperimentStorageState.AVAILABLE);
+ });
+
+ it('shows a list of archived runs', async () => {
+ await mockNJobs(1);
+
+ getExperimentSpy.mockImplementation(() => {
+ let apiExperiment = newMockExperiment();
+ apiExperiment['storage_state'] = ExperimentStorageState.ARCHIVED;
+ return apiExperiment;
+ });
+
+ tree = shallow();
+ await TestUtils.flushPromises();
+
+ expect(tree.find('RunListsRouter').prop('storageState')).toBe(ExperimentStorageState.ARCHIVED);
});
it("fetches this experiment's recurring runs", async () => {
@@ -303,7 +317,7 @@ describe('ExperimentDetails', () => {
.at(0)
.simulate('click');
await TestUtils.flushPromises();
- expect(tree.state('recurringRunsManagerOpen')).toBe(true);
+ expect(tree.state('recurringRunsManagerOpen')).toBeTruthy();
});
it('closes the recurring run manager modal', async () => {
@@ -318,14 +332,14 @@ describe('ExperimentDetails', () => {
.at(0)
.simulate('click');
await TestUtils.flushPromises();
- expect(tree.state('recurringRunsManagerOpen')).toBe(true);
+ expect(tree.state('recurringRunsManagerOpen')).toBeTruthy();
tree
.find('#closeExperimentRecurringRunManagerBtn')
.at(0)
.simulate('click');
await TestUtils.flushPromises();
- expect(tree.state('recurringRunsManagerOpen')).toBe(false);
+ expect(tree.state('recurringRunsManagerOpen')).toBeFalsy();
});
it('refreshes the number of active recurring runs when the recurring run manager is closed', async () => {
@@ -343,7 +357,7 @@ describe('ExperimentDetails', () => {
.at(0)
.simulate('click');
await TestUtils.flushPromises();
- expect(tree.state('recurringRunsManagerOpen')).toBe(true);
+ expect(tree.state('recurringRunsManagerOpen')).toBeTruthy();
// Called in the recurring run manager to list the recurring runs
expect(listJobsSpy).toHaveBeenCalledTimes(2);
@@ -353,7 +367,7 @@ describe('ExperimentDetails', () => {
.at(0)
.simulate('click');
await TestUtils.flushPromises();
- expect(tree.state('recurringRunsManagerOpen')).toBe(false);
+ expect(tree.state('recurringRunsManagerOpen')).toBeFalsy();
// Called a third time when the manager is closed to update the number of active recurring runs
expect(listJobsSpy).toHaveBeenCalledTimes(3);
@@ -466,15 +480,14 @@ describe('ExperimentDetails', () => {
await TestUtils.flushPromises();
tree.update();
- const compareBtn = (tree.state('runListToolbarProps') as ToolbarProps).actions[
- ButtonKeys.COMPARE
- ];
-
for (let i = 0; i < 12; i++) {
+ const compareBtn = (tree.state('runListToolbarProps') as ToolbarProps).actions[
+ ButtonKeys.COMPARE
+ ];
if (i < 2 || i > 10) {
- expect(compareBtn!.disabled).toBe(true);
+ expect(compareBtn!.disabled).toBeTruthy();
} else {
- expect(compareBtn!.disabled).toBe(false);
+ expect(compareBtn!.disabled).toBeFalsy();
}
tree
.find('.tableRow')
@@ -490,15 +503,67 @@ describe('ExperimentDetails', () => {
await TestUtils.flushPromises();
tree.update();
- const cloneBtn = (tree.state('runListToolbarProps') as ToolbarProps).actions[
- ButtonKeys.CLONE_RUN
- ];
-
for (let i = 0; i < 4; i++) {
+ const cloneBtn = (tree.state('runListToolbarProps') as ToolbarProps).actions[
+ ButtonKeys.CLONE_RUN
+ ];
if (i === 1) {
- expect(cloneBtn!.disabled).toBe(false);
+ expect(cloneBtn!.disabled).toBeFalsy();
+ } else {
+ expect(cloneBtn!.disabled).toBeTruthy();
+ }
+ tree
+ .find('.tableRow')
+ .at(i)
+ .simulate('click');
+ }
+ });
+
+ it('enables Archive button when at least one run is selected', async () => {
+ await mockNRuns(4);
+
+ tree = TestUtils.mountWithRouter();
+ await TestUtils.flushPromises();
+ tree.update();
+
+ for (let i = 0; i < 4; i++) {
+ const archiveButton = (tree.state('runListToolbarProps') as ToolbarProps).actions[
+ ButtonKeys.ARCHIVE
+ ];
+ if (i === 0) {
+ expect(archiveButton!.disabled).toBeTruthy();
+ } else {
+ expect(archiveButton!.disabled).toBeFalsy();
+ }
+ tree
+ .find('.tableRow')
+ .at(i)
+ .simulate('click');
+ }
+ });
+
+ it('enables Restore button when at least one run is selected', async () => {
+ await mockNRuns(4);
+
+ tree = TestUtils.mountWithRouter();
+ await TestUtils.flushPromises();
+ tree.update();
+
+ tree
+ .find('MD2Tabs')
+ .find('Button')
+ .at(1) // `Archived` tab button
+ .simulate('click');
+ await TestUtils.flushPromises();
+
+ for (let i = 0; i < 4; i++) {
+ const restoreButton = (tree.state('runListToolbarProps') as ToolbarProps).actions[
+ ButtonKeys.RESTORE
+ ];
+ if (i === 0) {
+ expect(restoreButton!.disabled).toBeTruthy();
} else {
- expect(cloneBtn!.disabled).toBe(true);
+ expect(restoreButton!.disabled).toBeFalsy();
}
tree
.find('.tableRow')
@@ -507,6 +572,92 @@ describe('ExperimentDetails', () => {
}
});
+ it('switches to another tab will change Archive/Restore button', async () => {
+ await mockNRuns(4);
+
+ tree = TestUtils.mountWithRouter();
+ await TestUtils.flushPromises();
+ tree.update();
+
+ tree
+ .find('MD2Tabs')
+ .find('Button')
+ .at(1) // `Archived` tab button
+ .simulate('click');
+ await TestUtils.flushPromises();
+ expect(
+ (tree.state('runListToolbarProps') as ToolbarProps).actions[ButtonKeys.ARCHIVE],
+ ).toBeUndefined();
+ expect(
+ (tree.state('runListToolbarProps') as ToolbarProps).actions[ButtonKeys.RESTORE],
+ ).toBeDefined();
+
+ tree
+ .find('MD2Tabs')
+ .find('Button')
+ .at(0) // `Active` tab button
+ .simulate('click');
+ await TestUtils.flushPromises();
+ expect(
+ (tree.state('runListToolbarProps') as ToolbarProps).actions[ButtonKeys.ARCHIVE],
+ ).toBeDefined();
+ expect(
+ (tree.state('runListToolbarProps') as ToolbarProps).actions[ButtonKeys.RESTORE],
+ ).toBeUndefined();
+ });
+
+ it('switches to active/archive tab will show active/archive runs', async () => {
+ await mockNRuns(4);
+ tree = TestUtils.mountWithRouter();
+ await TestUtils.flushPromises();
+ tree.update();
+ expect(tree.find('.tableRow').length).toEqual(4);
+
+ await mockNRuns(2);
+ tree
+ .find('MD2Tabs')
+ .find('Button')
+ .at(1) // `Archived` tab button
+ .simulate('click');
+ await TestUtils.flushPromises();
+ tree.update();
+ expect(tree.find('.tableRow').length).toEqual(2);
+ });
+
+ it('switches to another tab will change Archive/Restore button', async () => {
+ await mockNRuns(4);
+
+ tree = TestUtils.mountWithRouter();
+ await TestUtils.flushPromises();
+ tree.update();
+
+ tree
+ .find('MD2Tabs')
+ .find('Button')
+ .at(1) // `Archived` tab button
+ .simulate('click');
+ await TestUtils.flushPromises();
+ expect(
+ (tree.state('runListToolbarProps') as ToolbarProps).actions[ButtonKeys.ARCHIVE],
+ ).toBeUndefined();
+ expect(
+ (tree.state('runListToolbarProps') as ToolbarProps).actions[ButtonKeys.RESTORE],
+ ).toBeDefined();
+
+ tree
+ .find('MD2Tabs')
+ .find('Button')
+ .at(0) // `Active` tab button
+ .simulate('click');
+ await TestUtils.flushPromises();
+ expect(
+ (tree.state('runListToolbarProps') as ToolbarProps).actions[ButtonKeys.ARCHIVE],
+ ).toBeDefined();
+ expect(
+ (tree.state('runListToolbarProps') as ToolbarProps).actions[ButtonKeys.RESTORE],
+ ).toBeUndefined();
+ });
+
describe('EnhancedExperimentDetails', () => {
it('renders ExperimentDetails initially', () => {
render();
diff --git a/frontend/src/pages/ExperimentDetails.tsx b/frontend/src/pages/ExperimentDetails.tsx
index 66af136ffe6..9a17117b999 100644
--- a/frontend/src/pages/ExperimentDetails.tsx
+++ b/frontend/src/pages/ExperimentDetails.tsx
@@ -23,19 +23,19 @@ import DialogContent from '@material-ui/core/DialogContent';
import Paper from '@material-ui/core/Paper';
import PopOutIcon from '@material-ui/icons/Launch';
import RecurringRunsManager from './RecurringRunsManager';
-import RunList from '../pages/RunList';
+import RunListsRouter, { RunListsGroupTab } from './RunListsRouter';
import Toolbar, { ToolbarProps } from '../components/Toolbar';
import Tooltip from '@material-ui/core/Tooltip';
import { ApiExperiment, ExperimentStorageState } from '../apis/experiment';
import { Apis } from '../lib/Apis';
import { Page, PageProps } from './Page';
import { RoutePage, RouteParams } from '../components/Router';
-import { RunStorageState } from '../apis/run';
import { classes, stylesheet } from 'typestyle';
import { color, commonCss, padding } from '../Css';
import { logger } from '../lib/Utils';
import { useNamespaceChangeEvent } from 'src/lib/KubeflowClient';
import { Redirect } from 'react-router-dom';
+import { RunStorageState } from 'src/apis/run';
const css = stylesheet({
card: {
@@ -104,44 +104,42 @@ interface ExperimentDetailsState {
experiment: ApiExperiment | null;
recurringRunsManagerOpen: boolean;
selectedIds: string[];
- selectedTab: number;
+ runStorageState: RunStorageState;
runListToolbarProps: ToolbarProps;
+ runlistRefreshCount: number;
}
export class ExperimentDetails extends Page<{}, ExperimentDetailsState> {
- private _runlistRef = React.createRef();
-
constructor(props: any) {
super(props);
- const buttons = new Buttons(this.props, this.refresh.bind(this));
this.state = {
activeRecurringRunsCount: 0,
experiment: null,
recurringRunsManagerOpen: false,
runListToolbarProps: {
- actions: buttons
- .newRun(() => this.props.match.params[RouteParams.experimentId])
- .newRecurringRun(this.props.match.params[RouteParams.experimentId])
- .compareRuns(() => this.state.selectedIds)
- .cloneRun(() => this.state.selectedIds, false)
- .archive(
- 'run',
- () => this.state.selectedIds,
- false,
- ids => this._selectionChanged(ids),
- )
- .getToolbarActionMap(),
+ actions: this._getRunInitialToolBarButtons().getToolbarActionMap(),
breadcrumbs: [],
pageTitle: 'Runs',
topLevelToolbar: false,
},
// TODO: remove
selectedIds: [],
- selectedTab: 0,
+ runStorageState: RunStorageState.AVAILABLE,
+ runlistRefreshCount: 0,
};
}
+ private _getRunInitialToolBarButtons(): Buttons {
+ const buttons = new Buttons(this.props, this.refresh.bind(this));
+ buttons
+ .newRun(() => this.props.match.params[RouteParams.experimentId])
+ .newRecurringRun(this.props.match.params[RouteParams.experimentId])
+ .compareRuns(() => this.state.selectedIds)
+ .cloneRun(() => this.state.selectedIds, false);
+ return buttons;
+ }
+
public getInitialToolbarState(): ToolbarProps {
const buttons = new Buttons(this.props, this.refresh.bind(this));
return {
@@ -227,14 +225,15 @@ export class ExperimentDetails extends Page<{}, ExperimentDetailsState> {
-
@@ -267,17 +266,14 @@ export class ExperimentDetails extends Page<{}, ExperimentDetailsState> {
public async refresh(): Promise {
await this.load();
- if (this._runlistRef.current) {
- await this._runlistRef.current.refresh();
- }
return;
}
public async componentDidMount(): Promise {
- return this.load();
+ return this.load(true);
}
- public async load(): Promise {
+ public async load(isFirstTimeLoad: boolean = false): Promise {
this.clearBanner();
const experimentId = this.props.match.params[RouteParams.experimentId];
@@ -296,6 +292,19 @@ export class ExperimentDetails extends Page<{}, ExperimentDetailsState> {
experiment.storage_state === ExperimentStorageState.ARCHIVED
? buttons.restore('experiment', idGetter, true, () => this.refresh())
: buttons.archive('experiment', idGetter, true, () => this.refresh());
+ // If experiment is archived, shows archived runs list by default.
+ // If experiment is active, shows active runs list by default.
+ let runStorageState = this.state.runStorageState;
+ // Determine the default Active/Archive run list tab based on experiment status.
+ // After component is mounted, it is up to user to decide the run storage state they
+ // want to view.
+ if (isFirstTimeLoad) {
+ runStorageState =
+ experiment.storage_state === ExperimentStorageState.ARCHIVED
+ ? RunStorageState.ARCHIVED
+ : RunStorageState.AVAILABLE;
+ }
+
const actions = buttons.getToolbarActionMap();
this.props.updateToolbar({
actions,
@@ -326,19 +335,73 @@ export class ExperimentDetails extends Page<{}, ExperimentDetailsState> {
logger.error(`Error fetching recurring runs for experiment: ${experimentId}`, err);
}
- this.setStateSafe({ activeRecurringRunsCount, experiment });
+ let runlistRefreshCount = this.state.runlistRefreshCount + 1;
+ this.setStateSafe({
+ activeRecurringRunsCount,
+ experiment,
+ runStorageState,
+ runlistRefreshCount,
+ });
+ this._selectionChanged([]);
} catch (err) {
await this.showPageError(`Error: failed to retrieve experiment: ${experimentId}.`, err);
logger.error(`Error loading experiment: ${experimentId}`, err);
}
}
- private _selectionChanged(selectedIds: string[]): void {
- const toolbarActions = this.state.runListToolbarProps.actions;
+ /**
+ * Users can choose to show runs list in different run storage states.
+ *
+ * @param tab selected by user for run storage state
+ */
+ _onRunTabSwitch = (tab: RunListsGroupTab) => {
+ let runStorageState = RunStorageState.AVAILABLE;
+ if (tab === RunListsGroupTab.ARCHIVE) {
+ runStorageState = RunStorageState.ARCHIVED;
+ }
+ let runlistRefreshCount = this.state.runlistRefreshCount + 1;
+ this.setStateSafe(
+ {
+ runStorageState,
+ runlistRefreshCount,
+ },
+ () => {
+ this._selectionChanged([]);
+ },
+ );
+
+ return;
+ };
+
+ _selectionChanged = (selectedIds: string[]) => {
+ const toolbarButtons = this._getRunInitialToolBarButtons();
+ // If user selects to show Active runs list, shows `Archive` button for selected runs.
+ // If user selects to show Archive runs list, shows `Restore` button for selected runs.
+ if (this.state.runStorageState === RunStorageState.AVAILABLE) {
+ toolbarButtons.archive(
+ 'run',
+ () => this.state.selectedIds,
+ false,
+ ids => this._selectionChanged(ids),
+ );
+ } else {
+ toolbarButtons.restore(
+ 'run',
+ () => this.state.selectedIds,
+ false,
+ ids => this._selectionChanged(ids),
+ );
+ }
+ const toolbarActions = toolbarButtons.getToolbarActionMap();
toolbarActions[ButtonKeys.COMPARE].disabled =
selectedIds.length <= 1 || selectedIds.length > 10;
toolbarActions[ButtonKeys.CLONE_RUN].disabled = selectedIds.length !== 1;
- toolbarActions[ButtonKeys.ARCHIVE].disabled = !selectedIds.length;
+ if (toolbarActions[ButtonKeys.ARCHIVE]) {
+ toolbarActions[ButtonKeys.ARCHIVE].disabled = !selectedIds.length;
+ }
+ if (toolbarActions[ButtonKeys.RESTORE]) {
+ toolbarActions[ButtonKeys.RESTORE].disabled = !selectedIds.length;
+ }
this.setState({
runListToolbarProps: {
actions: toolbarActions,
@@ -348,7 +411,7 @@ export class ExperimentDetails extends Page<{}, ExperimentDetailsState> {
},
selectedIds,
});
- }
+ };
private _recurringRunsManagerClosed(): void {
this.setState({ recurringRunsManagerOpen: false });
diff --git a/frontend/src/pages/RunListsRouter.test.tsx b/frontend/src/pages/RunListsRouter.test.tsx
new file mode 100644
index 00000000000..de121578ae5
--- /dev/null
+++ b/frontend/src/pages/RunListsRouter.test.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { render, screen } from '@testing-library/react';
+import produce from 'immer';
+import RunListsRouter, { RunListsRouterProps } from './RunListsRouter';
+import React from 'react';
+import { RouteParams } from 'src/components/Router';
+import { ApiRunDetail, RunStorageState } from 'src/apis/run';
+import { ApiExperiment } from 'src/apis/experiment';
+import { Apis } from 'src/lib/Apis';
+import * as Utils from '../lib/Utils';
+import { BrowserRouter } from 'react-router-dom';
+import { PredicateOp } from 'src/apis/filter';
+
+describe('RunListsRouter', () => {
+ let historyPushSpy: any;
+ let runStorageState = RunStorageState.AVAILABLE;
+
+ const onSelectionChangeMock = jest.fn();
+ const listRunsSpy = jest.spyOn(Apis.runServiceApi, 'listRuns');
+ const getRunSpy = jest.spyOn(Apis.runServiceApi, 'getRun');
+ const getPipelineSpy = jest.spyOn(Apis.pipelineServiceApi, 'getPipeline');
+ const getExperimentSpy = jest.spyOn(Apis.experimentServiceApi, 'getExperiment');
+ const formatDateStringSpy = jest.spyOn(Utils, 'formatDateString');
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => null);
+
+ const MOCK_EXPERIMENT = newMockExperiment();
+ const archiveRunDisplayName = 'run with id: achiverunid';
+ const activeRunDisplayName = 'run with id: activerunid';
+
+ function newMockExperiment(): ApiExperiment {
+ return {
+ description: 'mock experiment description',
+ id: 'some-mock-experiment-id',
+ name: 'some mock experiment name',
+ };
+ }
+
+ function generateProps(): RunListsRouterProps {
+ const runListsRouterProps: RunListsRouterProps = {
+ onTabSwitch: jest.fn((newTab: number) => {
+ // this.refresh();
+ if (newTab === 1) {
+ runStorageState = RunStorageState.ARCHIVED;
+ } else {
+ runStorageState = RunStorageState.AVAILABLE;
+ }
+ }),
+ hideExperimentColumn: true,
+ history: { push: historyPushSpy } as any,
+ location: '' as any,
+ match: { params: { [RouteParams.experimentId]: MOCK_EXPERIMENT.id } } as any,
+ onSelectionChange: onSelectionChangeMock,
+ selectedIds: [],
+ storageState: runStorageState,
+ refreshCount: 0,
+ noFilterBox: false,
+ disablePaging: false,
+ disableSorting: true,
+ disableSelection: false,
+ hideMetricMetadata: false,
+ onError: consoleErrorSpy,
+ };
+ return runListsRouterProps;
+ }
+
+ beforeEach(() => {
+ getRunSpy.mockImplementation(id =>
+ Promise.resolve(
+ produce({} as Partial, draft => {
+ draft.run = draft.run || {};
+ draft.run.id = id;
+ draft.run.name = 'run with id: ' + id;
+ }),
+ ),
+ );
+ listRunsSpy.mockImplementation((pageToken, pageSize, sortBy, keyType, keyId, filter) => {
+ let filterForArchive = JSON.parse(decodeURIComponent('{"predicates": []}'));
+ filterForArchive = encodeURIComponent(
+ JSON.stringify({
+ predicates: [
+ {
+ key: 'storage_state',
+ op: PredicateOp.EQUALS,
+ string_value: RunStorageState.ARCHIVED.toString(),
+ },
+ ],
+ }),
+ );
+ if (filter === filterForArchive) {
+ return Promise.resolve({
+ runs: [
+ {
+ id: 'achiverunid',
+ name: archiveRunDisplayName,
+ },
+ ],
+ });
+ }
+ return Promise.resolve({
+ runs: [
+ {
+ id: 'activerunid',
+ name: activeRunDisplayName,
+ },
+ ],
+ });
+ });
+ getPipelineSpy.mockImplementation(() => ({ name: 'some pipeline' }));
+ getExperimentSpy.mockImplementation(() => ({ name: 'some experiment' }));
+ formatDateStringSpy.mockImplementation((date?: Date) => {
+ return date ? '1/2/2019, 12:34:56 PM' : '-';
+ });
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('shows Active and Archive tabs', () => {
+ render(
+
+
+ ,
+ );
+
+ screen.getByText('Active');
+ screen.getByText('Archived');
+ });
+});
diff --git a/frontend/src/pages/RunListsRouter.tsx b/frontend/src/pages/RunListsRouter.tsx
new file mode 100644
index 00000000000..bbd37668e08
--- /dev/null
+++ b/frontend/src/pages/RunListsRouter.tsx
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as React from 'react';
+import { RunStorageState } from 'src/apis/run';
+import MD2Tabs from 'src/atoms/MD2Tabs';
+import { commonCss, padding } from 'src/Css';
+import { classes } from 'typestyle';
+import RunList, { RunListProps } from './RunList';
+
+export enum RunListsGroupTab {
+ ACTIVE = 0,
+ ARCHIVE = 1,
+}
+
+export type RunListsRouterProps = RunListProps & {
+ storageState: RunStorageState;
+ refreshCount: number;
+ onTabSwitch?: (tab: RunListsGroupTab) => void;
+};
+
+/**
+ * Contains two tab buttons which allows user to see Active or Archived runs list.
+ */
+class RunListsRouter extends React.PureComponent {
+ private _runlistRef = React.createRef();
+
+ switchTab = (newTab: number) => {
+ if (this._getSelectedTab() === newTab) {
+ return;
+ }
+ if (this.props.onTabSwitch) {
+ this.props.onTabSwitch(newTab);
+ }
+ };
+
+ componentDidUpdate(prevProps: { refreshCount: number }) {
+ if (prevProps.refreshCount === this.props.refreshCount) {
+ return;
+ }
+ this.refresh();
+ }
+
+ public async refresh(): Promise {
+ if (this._runlistRef.current) {
+ await this._runlistRef.current.refresh();
+ }
+ return;
+ }
+
+ public render(): JSX.Element {
+ return (
+
+
+
+ {
+
+ }
+
+ );
+ }
+
+ private _getSelectedTab() {
+ return this.props.storageState === RunStorageState.ARCHIVED
+ ? RunListsGroupTab.ARCHIVE
+ : RunListsGroupTab.ACTIVE;
+ }
+}
+
+export default RunListsRouter;
diff --git a/frontend/src/pages/__snapshots__/AllRunsAndArchive.test.tsx.snap b/frontend/src/pages/__snapshots__/AllRunsAndArchive.test.tsx.snap
new file mode 100644
index 00000000000..27cd4e21b58
--- /dev/null
+++ b/frontend/src/pages/__snapshots__/AllRunsAndArchive.test.tsx.snap
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RunsAndArchive renders archive page 1`] = `
+
+
+
+
+`;
+
+exports[`RunsAndArchive renders runs page 1`] = `
+
+
+
+
+`;
diff --git a/frontend/src/pages/__snapshots__/ExperimentDetails.test.tsx.snap b/frontend/src/pages/__snapshots__/ExperimentDetails.test.tsx.snap
index fd4724ec16b..05dc4676236 100644
--- a/frontend/src/pages/__snapshots__/ExperimentDetails.test.tsx.snap
+++ b/frontend/src/pages/__snapshots__/ExperimentDetails.test.tsx.snap
@@ -141,7 +141,7 @@ exports[`ExperimentDetails fetches this experiment's recurring runs 1`] = `
pageTitle="Runs"
topLevelToolbar={false}
/>
-
-
-
-