From 2c6ad29737ece5b46be52ca32d3a434ac3f478d4 Mon Sep 17 00:00:00 2001 From: Riley Bauer <34456002+rileyjbauer@users.noreply.github.com> Date: Wed, 30 Jan 2019 14:21:31 -0800 Subject: [PATCH 1/4] Improve runtime graph starting and running experience (#734) * Improve runtime graph starting and running experience - displays spinner when no nodes have started - adds info message to bottom of runtime graph indicating that it is runtime and that the graph will update over time - adds placeholder nodes at end of graph to indicate future progress * Update test snapshots * Fix bug with virtual nodes, and add transition to graph * PR comments, updating tests * Moves IconWithTooltip to atoms/ and adds tests * Update copyright and add further tests --- frontend/src/Css.tsx | 7 +- frontend/src/atoms/IconWithTooltip.test.tsx | 37 ++ frontend/src/atoms/IconWithTooltip.tsx | 41 ++ .../IconWithTooltip.test.tsx.snap | 69 +++ frontend/src/components/Graph.test.tsx | 36 +- frontend/src/components/Graph.tsx | 35 +- .../__snapshots__/Graph.test.tsx.snap | 526 ++++++++++++++++- frontend/src/lib/WorkflowParser.test.ts | 151 ++++- frontend/src/lib/WorkflowParser.ts | 30 +- frontend/src/pages/PipelineDetails.tsx | 2 +- frontend/src/pages/RunDetails.test.tsx | 46 ++ frontend/src/pages/RunDetails.tsx | 62 +- .../ExperimentList.test.tsx.snap | 2 +- .../PipelineDetails.test.tsx.snap | 40 +- .../__snapshots__/RunDetails.test.tsx.snap | 556 +++++++++++++++--- .../pages/__snapshots__/Status.test.tsx.snap | 2 +- 16 files changed, 1469 insertions(+), 173 deletions(-) create mode 100644 frontend/src/atoms/IconWithTooltip.test.tsx create mode 100644 frontend/src/atoms/IconWithTooltip.tsx create mode 100644 frontend/src/atoms/__snapshots__/IconWithTooltip.test.tsx.snap diff --git a/frontend/src/Css.tsx b/frontend/src/Css.tsx index 2eaaf162248..34e55e89e45 100644 --- a/frontend/src/Css.tsx +++ b/frontend/src/Css.tsx @@ -38,7 +38,7 @@ export const color = { theme: '#1a73e8', themeDarker: '#0b59dc', warningBg: '#f9f9e1', - weak: '#999', + weak: '#9AA0A6', }; export const dimension = { @@ -223,6 +223,11 @@ export const commonCss = stylesheet({ paddingBottom: 16, paddingTop: 20, }, + infoIcon: { + color: color.lowContrast, + height: 16, + width: 16 + }, link: { $nest: { '&:hover': { diff --git a/frontend/src/atoms/IconWithTooltip.test.tsx b/frontend/src/atoms/IconWithTooltip.test.tsx new file mode 100644 index 00000000000..a59f595b1f8 --- /dev/null +++ b/frontend/src/atoms/IconWithTooltip.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2019 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 IconWithTooltip from './IconWithTooltip'; +import TestIcon from '@material-ui/icons/Help'; +import { create } from 'react-test-renderer'; + +describe('IconWithTooltip', () => { + it('renders without height or weight', () => { + const tree = create( + + ); + expect(tree).toMatchSnapshot(); + }); + + it('renders with height and weight', () => { + const tree = create( + + ); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/atoms/IconWithTooltip.tsx b/frontend/src/atoms/IconWithTooltip.tsx new file mode 100644 index 00000000000..6c3b4c76106 --- /dev/null +++ b/frontend/src/atoms/IconWithTooltip.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2019 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 Tooltip from '@material-ui/core/Tooltip'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; + +interface IconWithTooltipProps { + Icon: React.ComponentType; + height?: number; + iconColor: string; + tooltip: string; + width?: number; +} + +export default (props: IconWithTooltipProps) => { + const { height, Icon, iconColor, tooltip, width } = props; + + return ( + + + + ); +}; diff --git a/frontend/src/atoms/__snapshots__/IconWithTooltip.test.tsx.snap b/frontend/src/atoms/__snapshots__/IconWithTooltip.test.tsx.snap new file mode 100644 index 00000000000..b4c33081005 --- /dev/null +++ b/frontend/src/atoms/__snapshots__/IconWithTooltip.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IconWithTooltip renders with height and weight 1`] = ` + +`; + +exports[`IconWithTooltip renders without height or weight 1`] = ` + +`; diff --git a/frontend/src/components/Graph.test.tsx b/frontend/src/components/Graph.test.tsx index d12d7af7b5a..4de7c1baefd 100644 --- a/frontend/src/components/Graph.test.tsx +++ b/frontend/src/components/Graph.test.tsx @@ -18,6 +18,8 @@ import * as dagre from 'dagre'; import * as React from 'react'; import { shallow } from 'enzyme'; import Graph from './Graph'; +import SuccessIcon from '@material-ui/icons/CheckCircle'; +import Tooltip from '@material-ui/core/Tooltip'; function newGraph(): dagre.graphlib.Graph { const graph = new dagre.graphlib.Graph(); @@ -26,8 +28,17 @@ function newGraph(): dagre.graphlib.Graph { return graph; } -const newNode = (label: string) => ({ +const testIcon = ( + + + +); + +const newNode = (label: string, isPlaceHolder?: boolean, color?: string, icon?: JSX.Element) => ({ + bgColor: color, height: 10, + icon: icon || testIcon, + isPlaceholder: isPlaceHolder || false, label, width: 10, }); @@ -86,6 +97,29 @@ describe('Graph', () => { expect(shallow()).toMatchSnapshot(); }); + it('renders a graph with colored nodes', () => { + const graph = newGraph(); + graph.setNode('node1', newNode('node1', false, 'red')); + graph.setNode('node2', newNode('node2', false, 'green')); + expect(shallow()).toMatchSnapshot(); + }); + + it('renders a graph with colored edges', () => { + const graph = newGraph(); + graph.setNode('node1', newNode('node1')); + graph.setNode('node2', newNode('node2')); + graph.setEdge('node1', 'node2', { color: 'red' }); + expect(shallow()).toMatchSnapshot(); + }); + + it('renders a graph with a placeholder node and edge', () => { + const graph = newGraph(); + graph.setNode('node1', newNode('node1', false)); + graph.setNode('node2', newNode('node2', true)); + graph.setEdge('node1', 'node2', { isPlaceholder: true }); + expect(shallow()).toMatchSnapshot(); + }); + it('calls onClick callback when node is clicked', () => { const graph = newGraph(); graph.setNode('node1', newNode('node1')); diff --git a/frontend/src/components/Graph.tsx b/frontend/src/components/Graph.tsx index 9fa531b26c5..d1244da3094 100644 --- a/frontend/src/components/Graph.tsx +++ b/frontend/src/components/Graph.tsx @@ -32,9 +32,11 @@ interface Line { } interface Edge { + color?: string; from: string; to: string; lines: Line[]; + isPlaceholder?: boolean; } const css = stylesheet({ @@ -93,6 +95,12 @@ const css = stylesheet({ backgroundColor: '#e4ebff !important', borderColor: color.theme, }, + placeholderNode: { + margin: 10, + position: 'absolute', + // TODO: can this be calculated? + transform: 'translate(73px, 16px)' + }, root: { backgroundColor: color.graphBg, borderLeft: 'solid 1px ' + color.divider, @@ -153,17 +161,28 @@ export default class Graph extends React.Component { } } } - displayEdges.push({ from: edgeInfo.v, to: edgeInfo.w, lines }); + displayEdges.push({ + color: edge.color, + from: edgeInfo.v, + isPlaceholder: edge.isPlaceholder, + lines, + to: edgeInfo.w + }); }); return (
{graph.nodes().map(id => Object.assign(graph.node(id), { id })).map((node, i) => ( -
this.props.onClick && this.props.onClick(node.id)} style={{ + onClick={() => (!node.isPlaceholder && this.props.onClick) && this.props.onClick(node.id)} + style={{ backgroundColor: node.bgColor, left: node.x, - maxHeight: node.height, minHeight: node.height, top: node.y, width: node.width, + maxHeight: node.height, + minHeight: node.height, + top: node.y, + transition: 'left 0.5s, top 0.5s', + width: node.width, }}>
{node.label}
{node.icon}
@@ -173,11 +192,17 @@ export default class Graph extends React.Component { {displayEdges.map((edge, i) => (
{edge.lines.map((line, l) => ( -
))} diff --git a/frontend/src/components/__snapshots__/Graph.test.tsx.snap b/frontend/src/components/__snapshots__/Graph.test.tsx.snap index 02d906451e2..60b6d75c279 100644 --- a/frontend/src/components/__snapshots__/Graph.test.tsx.snap +++ b/frontend/src/components/__snapshots__/Graph.test.tsx.snap @@ -15,6 +15,7 @@ exports[`Graph gracefully renders a graph with a selected node id that does not "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -26,7 +27,13 @@ exports[`Graph gracefully renders a graph with a selected node id that does not
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
`; +exports[`Graph renders a graph with a placeholder node and edge 1`] = ` +
+
+
+ node1 +
+
+ + + +
+
+
+
+ node2 +
+
+ + + +
+
+
+
+
+
+
+
+`; + exports[`Graph renders a graph with a selected node 1`] = `
+ > + + + +
+ + + +
+
+
+
+
+
+
+`; + +exports[`Graph renders a graph with colored edges 1`] = ` +
+
+
+ node1 +
+
+ + + +
+
+
+
+ node2 +
+
+ + + +
+
@@ -628,9 +986,12 @@ exports[`Graph renders a graph with a selected node 1`] = ` key="0" style={ Object { + "borderTopColor": "red", + "borderTopStyle": "solid", "left": -7.75, "top": 22.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -640,9 +1001,12 @@ exports[`Graph renders a graph with a selected node 1`] = ` key="1" style={ Object { + "borderTopColor": "red", + "borderTopStyle": "solid", "left": -7.75, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -662,6 +1026,75 @@ exports[`Graph renders a graph with a selected node 1`] = `
`; +exports[`Graph renders a graph with colored nodes 1`] = ` +
+
+
+ node1 +
+
+ + + +
+
+
+
+ node2 +
+
+ + + +
+
+
+`; + exports[`Graph renders a graph with one node 1`] = `
+ > + + + +
`; @@ -708,6 +1148,7 @@ exports[`Graph renders a graph with two connectd nodes 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -719,7 +1160,13 @@ exports[`Graph renders a graph with two connectd nodes 1`] = `
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
`; diff --git a/frontend/src/lib/WorkflowParser.test.ts b/frontend/src/lib/WorkflowParser.test.ts index dd846136dff..51a5e13e13f 100644 --- a/frontend/src/lib/WorkflowParser.test.ts +++ b/frontend/src/lib/WorkflowParser.test.ts @@ -16,6 +16,7 @@ import WorkflowParser, { StorageService } from './WorkflowParser'; import { NodePhase } from '../pages/Status'; +import { color } from '../Css'; describe('WorkflowParser', () => { describe('createRuntimeGraph', () => { @@ -79,37 +80,165 @@ describe('WorkflowParser', () => { it('creates graph with exit handler attached', () => { const workflow = { - metadata: { name: 'node1' }, + metadata: { name: 'virtualRoot' }, status: { nodes: { node1: { displayName: 'node1', id: 'node1', name: 'node1', - outboundNodes: ['node2'], phase: 'Succeeded', - type: 'Steps', + type: 'Pod', }, node2: { displayName: 'node2', id: 'node2', - name: 'node2', + name: 'virtualRoot.onExit', phase: 'Succeeded', type: 'Pod', }, - node3: { - displayName: 'node3', - id: 'node3', - name: 'node1.onExit', + virtualRoot: { + displayName: 'virtualRoot', + id: 'virtualRoot', + name: 'virtualRoot', + outboundNodes: ['node1'], + phase: 'Succeeded', + type: 'Steps', + }, + }, + } + }; + const g = WorkflowParser.createRuntimeGraph(workflow as any); + expect(g.nodes()).toEqual(['node1', 'node2']); + expect(g.edges()).toEqual([{ v: 'node1', w: 'node2' }]); + }); + + it('creates a graph with placeholder nodes for steps that are not finished', () => { + const workflow = { + metadata: { name: 'testWorkflow' }, + status: { + nodes: { + finishedNode: { + displayName: 'finishedNode', + id: 'finishedNode', + name: 'finishedNode', phase: 'Succeeded', type: 'Pod', - } + }, + pendingNode: { + displayName: 'pendingNode', + id: 'pendingNode', + name: 'pendingNode', + phase: 'Pending', + type: 'Pod', + }, + root: { + children: ['pendingNode', 'runningNode', 'finishedNode'], + displayName: 'root', + id: 'root', + name: 'root', + phase: 'Succeeded', + type: 'Pod', + }, + runningNode: { + displayName: 'runningNode', + id: 'runningNode', + name: 'runningNode', + phase: 'Running', + type: 'Pod', + }, + }, + } + }; + const g = WorkflowParser.createRuntimeGraph(workflow as any); + expect(g.nodes()).toEqual([ + 'finishedNode', + 'pendingNode', + 'pendingNode-running-placeholder', + 'root', + 'runningNode', + 'runningNode-running-placeholder' + ]); + expect(g.edges()).toEqual(expect.arrayContaining([ + { v: 'root', w: 'pendingNode' }, + { v: 'root', w: 'runningNode' }, + { v: 'root', w: 'finishedNode' }, + { v: 'pendingNode', w: 'pendingNode-running-placeholder' }, + { v: 'runningNode', w: 'runningNode-running-placeholder' }, + ])); + }); + + it('sets specific properties for placeholder nodes', () => { + const workflow = { + metadata: { name: 'testWorkflow' }, + status: { + nodes: { + root: { + children: ['runningNode'], + displayName: 'root', + id: 'root', + name: 'root', + phase: 'Succeeded', + type: 'Pod', + }, + runningNode: { + displayName: 'runningNode', + id: 'runningNode', + name: 'runningNode', + phase: 'Running', + type: 'Pod', + }, }, } }; const g = WorkflowParser.createRuntimeGraph(workflow as any); - expect(g.nodes()).toEqual(['node2', 'node3']); - expect(g.edges()).toEqual([{ v: 'node2', w: 'node3' }]); + + const runningNode = g.node('runningNode'); + expect(runningNode.height).toEqual(70); + expect(runningNode.width).toEqual(180); + expect(runningNode.label).toEqual('runningNode'); + expect(runningNode.isPlaceholder).toBeUndefined(); + + const placeholderNode = g.node('runningNode-running-placeholder'); + expect(placeholderNode.height).toEqual(28); + expect(placeholderNode.width).toEqual(28); + expect(placeholderNode.label).toBeUndefined(); + expect(placeholderNode.isPlaceholder).toBe(true); + }); + + it('sets extra properties for placeholder node edges', () => { + const workflow = { + metadata: { name: 'testWorkflow' }, + status: { + nodes: { + root: { + children: ['runningNode'], + displayName: 'root', + id: 'root', + name: 'root', + phase: 'Succeeded', + type: 'Pod', + }, + runningNode: { + displayName: 'runningNode', + id: 'runningNode', + name: 'runningNode', + phase: 'Running', + type: 'Pod', + }, + }, + } + }; + const g = WorkflowParser.createRuntimeGraph(workflow as any); + + g.edges().map(edgeInfo => g.edge(edgeInfo)).forEach(edge => { + if (edge.isPlaceholder) { + expect(edge.color).toEqual(color.weak); + } else { + expect(edge.color).toBeUndefined(); + } + }); + }); it('deletes virtual nodes', () => { diff --git a/frontend/src/lib/WorkflowParser.ts b/frontend/src/lib/WorkflowParser.ts index df2fc82ca00..519dbb21b28 100644 --- a/frontend/src/lib/WorkflowParser.ts +++ b/frontend/src/lib/WorkflowParser.ts @@ -15,8 +15,11 @@ */ import * as dagre from 'dagre'; +import IconWithTooltip from '../atoms/IconWithTooltip'; +import MoreIcon from '@material-ui/icons/MoreHoriz'; import { Workflow, NodeStatus, Parameter } from '../../third_party/argo-ui/argo_template'; -import { statusToIcon, NodePhase } from '../pages/Status'; +import { statusToIcon, NodePhase, hasFinished } from '../pages/Status'; +import { color } from '../Css'; export enum StorageService { GCS = 'gcs', @@ -37,6 +40,7 @@ export default class WorkflowParser { const NODE_WIDTH = 180; const NODE_HEIGHT = 70; + const PLACEHOLDER_NODE_DIMENSION = 28; if (!workflow || !workflow.status || !workflow.status.nodes || !workflow.metadata || !workflow.metadata.name) { @@ -50,8 +54,7 @@ export default class WorkflowParser { // Uses the root node, so this needs to happen before we remove the root // node below. const onExitHandlerNodeId = - Object.keys(workflowNodes).find((id) => - workflowNodes[id].name === `${workflowName}.onExit`); + Object.keys(workflowNodes).find((id) => workflowNodes[id].name === `${workflowName}.onExit`); if (onExitHandlerNodeId) { this.getOutboundNodes(workflow, workflowName).forEach((nodeId) => g.setEdge(nodeId, onExitHandlerNodeId)); @@ -64,17 +67,34 @@ export default class WorkflowParser { delete workflowNodes[workflowName]; } + const runningNodeSuffix = '-running-placeholder'; + // Create dagre graph nodes from workflow nodes. (Object as any).values(workflowNodes) .forEach((node: NodeStatus) => { - const workflowNode = workflowNodes[node.id]; g.setNode(node.id, { height: NODE_HEIGHT, - icon: statusToIcon(workflowNode.phase as NodePhase, workflowNode.startedAt, workflowNode.finishedAt), + icon: statusToIcon(node.phase as NodePhase, node.startedAt, node.finishedAt), label: node.displayName || node.id, width: NODE_WIDTH, ...node, }); + + if (!hasFinished(node.phase as NodePhase) && !this.isVirtual(node)) { + g.setNode(node.id + runningNodeSuffix, { + height: PLACEHOLDER_NODE_DIMENSION, + icon: IconWithTooltip({ + Icon: MoreIcon, + height: 24, + iconColor: color.weak, + tooltip: 'More nodes may appear here', + width: 24, + }), + isPlaceholder: true, + width: PLACEHOLDER_NODE_DIMENSION, + }); + g.setEdge(node.id, node.id + runningNodeSuffix, { color: color.weak, isPlaceholder: true }); + } }); // Connect dagre graph nodes with edges. diff --git a/frontend/src/pages/PipelineDetails.tsx b/frontend/src/pages/PipelineDetails.tsx index aef3c934156..9e22e0cd6ca 100644 --- a/frontend/src/pages/PipelineDetails.tsx +++ b/frontend/src/pages/PipelineDetails.tsx @@ -206,7 +206,7 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> { )}
- + Static pipeline graph
diff --git a/frontend/src/pages/RunDetails.test.tsx b/frontend/src/pages/RunDetails.test.tsx index 2cefd52d814..7ef486bf332 100644 --- a/frontend/src/pages/RunDetails.test.tsx +++ b/frontend/src/pages/RunDetails.test.tsx @@ -668,6 +668,52 @@ describe('RunDetails', () => { expect(tree.state('selectedNodeDetails')).toHaveProperty('phaseMessage', undefined); }); + [NodePhase.RUNNING, NodePhase.PENDING, NodePhase.UNKNOWN].forEach(unfinishedStatus => { + it(`displays a spinner if graph is not defined and run has status: ${unfinishedStatus}`, async () => { + const unfinishedRun = { + pipeline_runtime: { + // No graph + workflow_manifest: '{}', + }, + run: { + id: 'test-run-id', + name: 'test run', + status: unfinishedStatus, + }, + }; + getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun)); + + tree = shallow(); + await getRunSpy; + await TestUtils.flushPromises(); + + expect(tree).toMatchSnapshot(); + }); + }); + + [NodePhase.ERROR, NodePhase.FAILED, NodePhase.SUCCEEDED, NodePhase.SKIPPED].forEach(finishedStatus => { + it(`displays a message indicating there is no graph if graph is not defined and run has status: ${finishedStatus}`, async () => { + const unfinishedRun = { + pipeline_runtime: { + // No graph + workflow_manifest: '{}', + }, + run: { + id: 'test-run-id', + name: 'test run', + status: finishedStatus, + }, + }; + getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun)); + + tree = shallow(); + await getRunSpy; + await TestUtils.flushPromises(); + + expect(tree).toMatchSnapshot(); + }); + }); + describe('auto refresh', () => { beforeEach(() => { testRun.run!.status = NodePhase.PENDING; diff --git a/frontend/src/pages/RunDetails.tsx b/frontend/src/pages/RunDetails.tsx index 5a95d3d50a8..3efdde0476d 100644 --- a/frontend/src/pages/RunDetails.tsx +++ b/frontend/src/pages/RunDetails.tsx @@ -21,6 +21,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'; import DetailsTable from '../components/DetailsTable'; import Graph from '../components/Graph'; import Hr from '../atoms/Hr'; +import InfoIcon from '@material-ui/icons/InfoOutlined'; import LogViewer from '../components/LogViewer'; import MD2Tabs from '../atoms/MD2Tabs'; import PlotCard from '../components/PlotCard'; @@ -39,8 +40,8 @@ import { ToolbarProps } from '../components/Toolbar'; import { URLParser } from '../lib/URLParser'; import { ViewerConfig } from '../components/viewers/Viewer'; import { Workflow } from '../../third_party/argo-ui/argo_template'; -import { classes } from 'typestyle'; -import { commonCss, padding } from '../Css'; +import { classes, stylesheet } from 'typestyle'; +import { commonCss, padding, color, fonts, fontsize } from '../Css'; import { componentMap } from '../components/viewers/ViewerContainer'; import { flatten } from 'lodash'; import { formatDateString, getRunTime, logger, errorToMessage } from '../lib/Utils'; @@ -83,6 +84,27 @@ interface RunDetailsState { workflow?: Workflow; } +export const css = stylesheet({ + footer: { + background: color.graphBg, + display: 'flex', + padding: '0 0 20px 20px', + }, + graphPane: { + backgroundColor: color.graphBg, + overflow: 'hidden', + position: 'relative', + }, + infoSpan: { + color: color.lowContrast, + fontFamily: fonts.secondary, + fontSize: fontsize.small, + letterSpacing: '0.21px', + lineHeight: '24px', + paddingLeft: 6, + }, +}); + class RunDetails extends Page { private _onBlur: EventListener; private _onFocus: EventListener; @@ -121,7 +143,7 @@ class RunDetails extends Page { } public render(): JSX.Element { - const { allArtifactConfigs, graph, runMetadata, selectedTab, selectedNodeDetails, + const { allArtifactConfigs, graph, runFinished, runMetadata, selectedTab, selectedNodeDetails, sidepanelSelectedTab, workflow } = this.state; const selectedNodeId = selectedNodeDetails ? selectedNodeDetails.id : ''; @@ -136,8 +158,8 @@ class RunDetails extends Page { onSwitch={(tab: number) => this.setStateSafe({ selectedTab: tab })} />
- {selectedTab === 0 &&
- {graph &&
+ {selectedTab === 0 &&
+ {graph &&
this._selectNode(id)} /> @@ -145,8 +167,7 @@ class RunDetails extends Page { onClose={() => this.setStateSafe({ selectedNodeDetails: null })} title={selectedNodeId}> {!!selectedNodeDetails && ( {!!selectedNodeDetails.phaseMessage && ( - + )}
{
)} + +
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
} - {!graph && No graph to show} + {!graph && ( +
+ {runFinished && ( + + No graph to show + + )} + {!runFinished && ( + + )} +
+ )}
} {selectedTab === 1 && ( @@ -362,9 +403,10 @@ class RunDetails extends Page { private _stopAutoRefresh(): void { if (this._interval !== undefined) { clearInterval(this._interval); + + // Reset interval to indicate that a new one can be set + this._interval = undefined; } - // Reset interval to indicate that a new one can be set - this._interval = undefined; } private async _loadAllOutputs(): Promise { diff --git a/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap b/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap index 648fe24dae7..6d684d4c38f 100644 --- a/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap @@ -241,7 +241,7 @@ exports[`ExperimentList renders last 5 runs statuses 1`] = `
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
+
+
+
+
+
+`; + +exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has status: Error 1`] = ` +
+
+ +
+
+
+ + No graph to show + +
+
+
+
+
+`; + +exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has status: Failed 1`] = ` +
+
+ +
+
+
+ + No graph to show + +
+
+
+
+
+`; + +exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has status: Skipped 1`] = ` +
+
+ +
+
+
+ + No graph to show + +
+
+
+
+
+`; + +exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has status: Succeeded 1`] = ` +
+
+ +
+
+
+ + No graph to show + +
+
+
+
+
+`; + +exports[`RunDetails displays a spinner if graph is not defined and run has status: Pending 1`] = ` +
+
+ +
+
+
+ +
+
+
+
+
+`; + +exports[`RunDetails displays a spinner if graph is not defined and run has status: Running 1`] = ` +
+
+ +
+
+
+ +
+
+
+
+
+`; + +exports[`RunDetails displays a spinner if graph is not defined and run has status: Unknown 1`] = ` +
+
+ +
+
+
+
@@ -89,16 +371,10 @@ exports[`RunDetails does not load logs if clicked node status is skipped 1`] = ` className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
@@ -188,16 +480,10 @@ exports[`RunDetails keeps side pane open and on same tab when logs change after className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
@@ -381,16 +683,10 @@ exports[`RunDetails loads and shows logs in side pane 1`] = ` className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
@@ -531,16 +843,10 @@ exports[`RunDetails opens side panel when graph node is clicked 1`] = ` className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
@@ -621,17 +943,19 @@ exports[`RunDetails renders an empty run 1`] = ` className="page" >
- + - No graph to show - + > + No graph to show + +
@@ -660,16 +984,10 @@ exports[`RunDetails shows a one-node graph 1`] = ` className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
@@ -727,16 +1061,10 @@ exports[`RunDetails shows clicked node message in side panel 1`] = ` className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
@@ -821,16 +1165,10 @@ exports[`RunDetails shows clicked node output in side pane 1`] = ` className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
@@ -930,16 +1284,10 @@ exports[`RunDetails shows error banner atop logs area if fetching logs failed 1` className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
@@ -1355,16 +1719,10 @@ exports[`RunDetails switches to inputs/outputs tab in side pane 1`] = ` className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
@@ -1472,16 +1846,10 @@ exports[`RunDetails switches to logs tab in side pane 1`] = ` className="page" >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. + +
+
diff --git a/frontend/src/pages/__snapshots__/Status.test.tsx.snap b/frontend/src/pages/__snapshots__/Status.test.tsx.snap index 93abc666d0a..aea28e41549 100644 --- a/frontend/src/pages/__snapshots__/Status.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/Status.test.tsx.snap @@ -3318,7 +3318,7 @@ exports[`Status statusToIcon renders an icon with tooltip for phase: PENDING 1`] Date: Thu, 31 Jan 2019 11:15:54 -0800 Subject: [PATCH 2/4] Return resource count from ListXXX calls (#595) * add count to protos and libs * close db rows before second query * count -> total_size * int32 -> int * move scan count row to util * add comments * add logs when transactions fail * dedup from and where clauses * simplify job count query * job count queries * run count queries * add job_store total size test * added tests for list util * pr comments * list_utils -> list * fix clients and fake clients to support TotalSize * added TotalSize checks in api integration tests --- backend/README.md | 9 +- backend/api/experiment.proto | 1 + backend/api/go_client/experiment.pb.go | 107 ++++---- backend/api/go_client/job.pb.go | 169 +++++++------ backend/api/go_client/pipeline.pb.go | 129 +++++----- backend/api/go_client/run.pb.go | 235 +++++++++--------- .../api_list_experiments_response.go | 3 + .../job_model/api_list_jobs_response.go | 3 + .../api_list_pipelines_response.go | 3 + .../run_model/api_list_runs_response.go | 3 + backend/api/job.proto | 1 + backend/api/pipeline.proto | 1 + backend/api/run.proto | 1 + backend/api/swagger/experiment.swagger.json | 4 + backend/api/swagger/job.swagger.json | 4 + backend/api/swagger/pipeline.swagger.json | 4 + backend/api/swagger/run.swagger.json | 4 + backend/src/apiserver/list/BUILD.bazel | 3 + backend/src/apiserver/list/list.go | 49 +++- backend/src/apiserver/list/list_test.go | 73 +++++- .../apiserver/resource/resource_manager.go | 10 +- .../src/apiserver/server/experiment_server.go | 3 +- backend/src/apiserver/server/job_server.go | 4 +- .../src/apiserver/server/pipeline_server.go | 4 +- .../server/pipeline_upload_server_test.go | 9 +- backend/src/apiserver/server/run_server.go | 4 +- backend/src/apiserver/storage/BUILD.bazel | 3 - .../src/apiserver/storage/db_status_store.go | 3 +- .../src/apiserver/storage/experiment_store.go | 57 ++++- .../storage/experiment_store_test.go | 23 +- backend/src/apiserver/storage/job_store.go | 91 ++++--- .../src/apiserver/storage/job_store_test.go | 58 ++++- backend/src/apiserver/storage/list_util.go | 91 ------- .../src/apiserver/storage/list_util_test.go | 88 ------- .../src/apiserver/storage/pipeline_store.go | 62 ++++- .../apiserver/storage/pipeline_store_test.go | 20 +- backend/src/apiserver/storage/run_store.go | 94 ++++--- .../src/apiserver/storage/run_store_test.go | 75 +++++- .../client/api_server/experiment_client.go | 10 +- .../api_server/experiment_client_fake.go | 8 +- .../common/client/api_server/job_client.go | 10 +- .../client/api_server/job_client_fake.go | 8 +- .../client/api_server/pipeline_client.go | 10 +- .../client/api_server/pipeline_client_fake.go | 8 +- .../common/client/api_server/run_client.go | 10 +- .../client/api_server/run_client_fake.go | 8 +- backend/test/experiment_api_test.go | 26 +- backend/test/job_api_test.go | 26 +- backend/test/pipeline_api_test.go | 23 +- backend/test/run_api_test.go | 23 +- backend/test/test_utils.go | 8 +- 51 files changed, 992 insertions(+), 691 deletions(-) delete mode 100644 backend/src/apiserver/storage/list_util.go delete mode 100644 backend/src/apiserver/storage/list_util_test.go diff --git a/backend/README.md b/backend/README.md index 906b3a7f233..81c7aaad9c9 100644 --- a/backend/README.md +++ b/backend/README.md @@ -3,7 +3,7 @@ Pipelines backend. ## Building & Testing All components can be built using [Bazel](https://bazel.build/). To build -everything under bazel, run: +everything under backend, run: ``` bazel build //backend/... ``` @@ -13,6 +13,11 @@ To run all tests: bazel test //backend/... ``` +## Building Go client library and swagger files +After making changes to proto files, the Go client libraries and swagger +files need to be regenerated and checked-in. The backend/api/generate_api.sh +script takes care of this. + ## Updating BUILD files As the backend is written in Go, the BUILD files can be updated automatically using [Gazelle](https://github.com/bazelbuild/bazel-gazelle). Whenever a Go @@ -27,4 +32,4 @@ bumped in the `go.mod` file, ensure the BUILD files pick this up by updating the WORKSPACE go_repository rules using the following command: ``` bazel run //:gazelle -- update-repos --from_file=go.mod -``` \ No newline at end of file +``` diff --git a/backend/api/experiment.proto b/backend/api/experiment.proto index f93c499df01..770e83aa609 100644 --- a/backend/api/experiment.proto +++ b/backend/api/experiment.proto @@ -105,6 +105,7 @@ message ListExperimentsRequest { message ListExperimentsResponse { repeated Experiment experiments = 1; + int32 total_size = 3; string next_page_token = 2; } diff --git a/backend/api/go_client/experiment.pb.go b/backend/api/go_client/experiment.pb.go index efaa5d527c9..57a7ed71608 100755 --- a/backend/api/go_client/experiment.pb.go +++ b/backend/api/go_client/experiment.pb.go @@ -52,7 +52,7 @@ func (m *CreateExperimentRequest) Reset() { *m = CreateExperimentRequest func (m *CreateExperimentRequest) String() string { return proto.CompactTextString(m) } func (*CreateExperimentRequest) ProtoMessage() {} func (*CreateExperimentRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_experiment_167ae480ac1a4a2d, []int{0} + return fileDescriptor_experiment_99bd38d2be36879f, []int{0} } func (m *CreateExperimentRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_CreateExperimentRequest.Unmarshal(m, b) @@ -90,7 +90,7 @@ func (m *GetExperimentRequest) Reset() { *m = GetExperimentRequest{} } func (m *GetExperimentRequest) String() string { return proto.CompactTextString(m) } func (*GetExperimentRequest) ProtoMessage() {} func (*GetExperimentRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_experiment_167ae480ac1a4a2d, []int{1} + return fileDescriptor_experiment_99bd38d2be36879f, []int{1} } func (m *GetExperimentRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetExperimentRequest.Unmarshal(m, b) @@ -131,7 +131,7 @@ func (m *ListExperimentsRequest) Reset() { *m = ListExperimentsRequest{} func (m *ListExperimentsRequest) String() string { return proto.CompactTextString(m) } func (*ListExperimentsRequest) ProtoMessage() {} func (*ListExperimentsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_experiment_167ae480ac1a4a2d, []int{2} + return fileDescriptor_experiment_99bd38d2be36879f, []int{2} } func (m *ListExperimentsRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListExperimentsRequest.Unmarshal(m, b) @@ -181,6 +181,7 @@ func (m *ListExperimentsRequest) GetFilter() string { type ListExperimentsResponse struct { Experiments []*Experiment `protobuf:"bytes,1,rep,name=experiments,proto3" json:"experiments,omitempty"` + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -191,7 +192,7 @@ func (m *ListExperimentsResponse) Reset() { *m = ListExperimentsResponse func (m *ListExperimentsResponse) String() string { return proto.CompactTextString(m) } func (*ListExperimentsResponse) ProtoMessage() {} func (*ListExperimentsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_experiment_167ae480ac1a4a2d, []int{3} + return fileDescriptor_experiment_99bd38d2be36879f, []int{3} } func (m *ListExperimentsResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListExperimentsResponse.Unmarshal(m, b) @@ -218,6 +219,13 @@ func (m *ListExperimentsResponse) GetExperiments() []*Experiment { return nil } +func (m *ListExperimentsResponse) GetTotalSize() int32 { + if m != nil { + return m.TotalSize + } + return 0 +} + func (m *ListExperimentsResponse) GetNextPageToken() string { if m != nil { return m.NextPageToken @@ -236,7 +244,7 @@ func (m *DeleteExperimentRequest) Reset() { *m = DeleteExperimentRequest func (m *DeleteExperimentRequest) String() string { return proto.CompactTextString(m) } func (*DeleteExperimentRequest) ProtoMessage() {} func (*DeleteExperimentRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_experiment_167ae480ac1a4a2d, []int{4} + return fileDescriptor_experiment_99bd38d2be36879f, []int{4} } func (m *DeleteExperimentRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DeleteExperimentRequest.Unmarshal(m, b) @@ -277,7 +285,7 @@ func (m *Experiment) Reset() { *m = Experiment{} } func (m *Experiment) String() string { return proto.CompactTextString(m) } func (*Experiment) ProtoMessage() {} func (*Experiment) Descriptor() ([]byte, []int) { - return fileDescriptor_experiment_167ae480ac1a4a2d, []int{5} + return fileDescriptor_experiment_99bd38d2be36879f, []int{5} } func (m *Experiment) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Experiment.Unmarshal(m, b) @@ -506,47 +514,48 @@ var _ExperimentService_serviceDesc = grpc.ServiceDesc{ } func init() { - proto.RegisterFile("backend/api/experiment.proto", fileDescriptor_experiment_167ae480ac1a4a2d) -} - -var fileDescriptor_experiment_167ae480ac1a4a2d = []byte{ - // 602 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x54, 0x41, 0x6f, 0xd3, 0x4c, - 0x10, 0xfd, 0xec, 0xf4, 0x4b, 0xdb, 0x09, 0x69, 0xe9, 0x82, 0x9a, 0xd4, 0x49, 0xa9, 0xf1, 0xa1, - 0x14, 0x44, 0x6d, 0xb5, 0x9c, 0xe0, 0xd6, 0x42, 0x85, 0x84, 0x40, 0xaa, 0xd2, 0x9e, 0xb8, 0x44, - 0x6b, 0x7b, 0x6a, 0x56, 0x4d, 0xbc, 0x66, 0x77, 0x53, 0xda, 0x20, 0x2e, 0x48, 0x1c, 0xb9, 0xc0, - 0xdf, 0xe2, 0xc6, 0x5f, 0xe0, 0x87, 0x20, 0xaf, 0x9d, 0xc6, 0x71, 0x1c, 0x71, 0x4a, 0x76, 0xe6, - 0x79, 0xde, 0xbc, 0xe7, 0xe7, 0x85, 0xae, 0x4f, 0x83, 0x4b, 0x8c, 0x43, 0x8f, 0x26, 0xcc, 0xc3, - 0xeb, 0x04, 0x05, 0x1b, 0x62, 0xac, 0xdc, 0x44, 0x70, 0xc5, 0x49, 0x8d, 0x26, 0xcc, 0xea, 0x46, - 0x9c, 0x47, 0x03, 0xd4, 0x08, 0x1a, 0xc7, 0x5c, 0x51, 0xc5, 0x78, 0x2c, 0x33, 0x88, 0xd5, 0xc9, - 0xbb, 0xfa, 0xe4, 0x8f, 0x2e, 0x3c, 0x1c, 0x26, 0xea, 0x26, 0x6f, 0xee, 0x94, 0x9b, 0x8a, 0x0d, - 0x51, 0x2a, 0x3a, 0x4c, 0x72, 0xc0, 0x53, 0xfd, 0x13, 0xec, 0x47, 0x18, 0xef, 0xcb, 0x4f, 0x34, - 0x8a, 0x50, 0x78, 0x3c, 0xd1, 0xf3, 0x2b, 0xb8, 0x5a, 0x33, 0xcb, 0x0a, 0xc1, 0x45, 0xd6, 0x70, - 0xde, 0x40, 0xeb, 0xa5, 0x40, 0xaa, 0xf0, 0xe4, 0x56, 0x41, 0x0f, 0x3f, 0x8e, 0x50, 0x2a, 0xe2, - 0x01, 0x4c, 0x65, 0xb5, 0x0d, 0xdb, 0xd8, 0x6b, 0x1c, 0xae, 0xbb, 0x34, 0x61, 0x6e, 0x01, 0x5b, - 0x80, 0x38, 0xbb, 0x70, 0xff, 0x35, 0xaa, 0xf9, 0x41, 0x6b, 0x60, 0xb2, 0x50, 0x0f, 0x58, 0xed, - 0x99, 0x2c, 0x74, 0xbe, 0x19, 0xb0, 0xf9, 0x96, 0xc9, 0x02, 0x52, 0x4e, 0xa0, 0xdb, 0x00, 0x09, - 0x8d, 0xb0, 0xaf, 0xf8, 0x25, 0xc6, 0xf9, 0x23, 0xab, 0x69, 0xe5, 0x3c, 0x2d, 0x90, 0x0e, 0xe8, - 0x43, 0x5f, 0xb2, 0x31, 0xb6, 0x4d, 0xdb, 0xd8, 0xfb, 0xbf, 0xb7, 0x92, 0x16, 0xce, 0xd8, 0x18, - 0x49, 0x0b, 0x96, 0x25, 0x17, 0xaa, 0xef, 0xdf, 0xb4, 0x6b, 0xfa, 0xc1, 0x7a, 0x7a, 0x3c, 0xbe, - 0x21, 0x9b, 0x50, 0xbf, 0x60, 0x03, 0x85, 0xa2, 0xbd, 0x94, 0xd5, 0xb3, 0x93, 0xa3, 0xa0, 0x35, - 0xb7, 0x86, 0x4c, 0x78, 0x2c, 0x91, 0x1c, 0x40, 0x63, 0x2a, 0x4c, 0xb6, 0x0d, 0xbb, 0x56, 0x25, - 0xbe, 0x88, 0x21, 0xbb, 0xb0, 0x1e, 0xe3, 0xb5, 0xea, 0x17, 0xf6, 0x37, 0x35, 0x5d, 0x33, 0x2d, - 0x9f, 0x4e, 0x34, 0x38, 0x8f, 0xa1, 0xf5, 0x0a, 0x07, 0x58, 0xe5, 0x78, 0xd9, 0xa8, 0xef, 0x06, - 0xc0, 0x14, 0x55, 0x6e, 0x13, 0x02, 0x4b, 0x31, 0x1d, 0x62, 0x4e, 0xa3, 0xff, 0x13, 0x1b, 0x1a, - 0x21, 0xca, 0x40, 0x30, 0x1d, 0x85, 0xdc, 0x88, 0x62, 0x89, 0x3c, 0x07, 0x08, 0xf4, 0x1b, 0x0f, - 0xfb, 0x54, 0x69, 0x47, 0x1a, 0x87, 0x96, 0x9b, 0xc5, 0xcd, 0x9d, 0xc4, 0xcd, 0x3d, 0x9f, 0xc4, - 0xad, 0xb7, 0x9a, 0xa3, 0x8f, 0xd4, 0xe1, 0xaf, 0x1a, 0x6c, 0x4c, 0xf7, 0x39, 0x43, 0x71, 0xc5, - 0x02, 0x24, 0x09, 0xdc, 0x2d, 0x47, 0x88, 0x74, 0xb5, 0x55, 0x0b, 0x92, 0x65, 0x95, 0x8d, 0x74, - 0xf6, 0xbf, 0xfe, 0xfe, 0xf3, 0xd3, 0x7c, 0xe4, 0x6c, 0xa5, 0xf9, 0x94, 0xde, 0xd5, 0x81, 0x8f, - 0x8a, 0x1e, 0x14, 0xbe, 0x2a, 0xf9, 0xa2, 0x10, 0x34, 0x12, 0x40, 0x73, 0x26, 0x68, 0x64, 0x4b, - 0x0f, 0xac, 0x0a, 0xdf, 0x3c, 0xd7, 0xae, 0xe6, 0xb2, 0xc9, 0x83, 0x85, 0x5c, 0xde, 0x67, 0x16, - 0x7e, 0x21, 0x31, 0xac, 0xcd, 0xa6, 0x83, 0x74, 0xf4, 0xa8, 0xea, 0xe4, 0x5a, 0xdd, 0xea, 0x66, - 0x96, 0x27, 0xe7, 0xa1, 0x26, 0xed, 0x90, 0xc5, 0x02, 0x53, 0x1b, 0xcb, 0xb9, 0xc8, 0x6d, 0x5c, - 0x10, 0x17, 0x6b, 0x73, 0xee, 0xad, 0x9d, 0xa4, 0x37, 0xc8, 0x44, 0xe1, 0x93, 0x7f, 0x28, 0x3c, - 0x3e, 0xfd, 0x71, 0xf4, 0xae, 0xd7, 0x85, 0xe5, 0x10, 0x2f, 0xe8, 0x68, 0xa0, 0xc8, 0x06, 0x59, - 0x87, 0xa6, 0xd5, 0xd0, 0x9c, 0x67, 0x8a, 0xaa, 0x91, 0x7c, 0xbf, 0x03, 0xdb, 0x50, 0x3f, 0x46, - 0x2a, 0x50, 0x90, 0x7b, 0x2b, 0xa6, 0xd5, 0xa4, 0x23, 0xf5, 0x81, 0x0b, 0x36, 0xd6, 0xb7, 0x8b, - 0x6d, 0xfa, 0x77, 0x00, 0x6e, 0x01, 0xff, 0xf9, 0x75, 0xbd, 0xc9, 0xb3, 0xbf, 0x01, 0x00, 0x00, - 0xff, 0xff, 0x41, 0x6a, 0x6d, 0x69, 0x1c, 0x05, 0x00, 0x00, + proto.RegisterFile("backend/api/experiment.proto", fileDescriptor_experiment_99bd38d2be36879f) +} + +var fileDescriptor_experiment_99bd38d2be36879f = []byte{ + // 615 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x54, 0xc1, 0x6e, 0xd3, 0x40, + 0x10, 0xc5, 0x4e, 0x49, 0x9b, 0x09, 0x69, 0xe9, 0x82, 0x9a, 0xd4, 0x49, 0xa9, 0xf1, 0xa1, 0x14, + 0x44, 0x6d, 0xb5, 0x9c, 0xe0, 0xd6, 0x42, 0x85, 0x84, 0x40, 0xaa, 0xd2, 0x9e, 0xb8, 0x44, 0x6b, + 0x7b, 0x6a, 0x56, 0x4d, 0xbc, 0x66, 0x77, 0x53, 0xda, 0x22, 0x2e, 0x48, 0x1c, 0xb9, 0xd0, 0xdf, + 0xe2, 0xc6, 0x2f, 0xf0, 0x21, 0xc8, 0x6b, 0xa7, 0x71, 0x1d, 0x47, 0x9c, 0x92, 0x99, 0x79, 0x3b, + 0xf3, 0xde, 0xec, 0xf3, 0x42, 0xcf, 0xa7, 0xc1, 0x19, 0xc6, 0xa1, 0x47, 0x13, 0xe6, 0xe1, 0x45, + 0x82, 0x82, 0x8d, 0x30, 0x56, 0x6e, 0x22, 0xb8, 0xe2, 0xa4, 0x46, 0x13, 0x66, 0xf5, 0x22, 0xce, + 0xa3, 0x21, 0x6a, 0x04, 0x8d, 0x63, 0xae, 0xa8, 0x62, 0x3c, 0x96, 0x19, 0xc4, 0xea, 0xe6, 0x55, + 0x1d, 0xf9, 0xe3, 0x53, 0x0f, 0x47, 0x89, 0xba, 0xcc, 0x8b, 0x9b, 0xe5, 0xa2, 0x62, 0x23, 0x94, + 0x8a, 0x8e, 0x92, 0x1c, 0xf0, 0x5c, 0xff, 0x04, 0x3b, 0x11, 0xc6, 0x3b, 0xf2, 0x0b, 0x8d, 0x22, + 0x14, 0x1e, 0x4f, 0x74, 0xff, 0x8a, 0x59, 0xed, 0x5b, 0x64, 0x85, 0xe0, 0x22, 0x2b, 0x38, 0xef, + 0xa0, 0xfd, 0x5a, 0x20, 0x55, 0x78, 0x78, 0xa3, 0xa0, 0x8f, 0x9f, 0xc7, 0x28, 0x15, 0xf1, 0x00, + 0xa6, 0xb2, 0x3a, 0x86, 0x6d, 0x6c, 0x37, 0xf7, 0x56, 0x5c, 0x9a, 0x30, 0xb7, 0x80, 0x2d, 0x40, + 0x9c, 0x2d, 0x78, 0xf8, 0x16, 0xd5, 0x6c, 0xa3, 0x65, 0x30, 0x59, 0xa8, 0x1b, 0x34, 0xfa, 0x26, + 0x0b, 0x9d, 0x1f, 0x06, 0xac, 0xbd, 0x67, 0xb2, 0x80, 0x94, 0x13, 0xe8, 0x06, 0x40, 0x42, 0x23, + 0x1c, 0x28, 0x7e, 0x86, 0x71, 0x7e, 0xa4, 0x91, 0x66, 0x4e, 0xd2, 0x04, 0xe9, 0x82, 0x0e, 0x06, + 0x92, 0x5d, 0x61, 0xc7, 0xb4, 0x8d, 0xed, 0xbb, 0xfd, 0xa5, 0x34, 0x71, 0xcc, 0xae, 0x90, 0xb4, + 0x61, 0x51, 0x72, 0xa1, 0x06, 0xfe, 0x65, 0xa7, 0xa6, 0x0f, 0xd6, 0xd3, 0xf0, 0xe0, 0x92, 0xac, + 0x41, 0xfd, 0x94, 0x0d, 0x15, 0x8a, 0xce, 0x42, 0x96, 0xcf, 0x22, 0xe7, 0xda, 0x80, 0xf6, 0x0c, + 0x0f, 0x99, 0xf0, 0x58, 0x22, 0xd9, 0x85, 0xe6, 0x54, 0x99, 0xec, 0x18, 0x76, 0xad, 0x4a, 0x7d, + 0x11, 0x93, 0x72, 0x57, 0x5c, 0xd1, 0x61, 0xc6, 0xae, 0xa6, 0xd9, 0x35, 0x74, 0x46, 0xd3, 0xdb, + 0x82, 0x95, 0x18, 0x2f, 0xd4, 0xa0, 0xa0, 0xcf, 0xd4, 0x74, 0x5a, 0x69, 0xfa, 0x68, 0xa2, 0xd1, + 0x79, 0x0a, 0xed, 0x37, 0x38, 0xc4, 0xaa, 0x1b, 0x29, 0x2f, 0xf2, 0xa7, 0x01, 0x30, 0x45, 0x95, + 0xcb, 0x84, 0xc0, 0x42, 0x4c, 0x47, 0x98, 0x8f, 0xd1, 0xff, 0x89, 0x0d, 0xcd, 0x10, 0x65, 0x20, + 0x98, 0xb6, 0x4a, 0xbe, 0xa8, 0x62, 0x8a, 0xbc, 0x04, 0x08, 0xb4, 0x23, 0xc2, 0x01, 0x55, 0x7a, + 0x63, 0xcd, 0x3d, 0xcb, 0xcd, 0xec, 0xe8, 0x4e, 0xec, 0xe8, 0x9e, 0x4c, 0xec, 0xd8, 0x6f, 0xe4, + 0xe8, 0x7d, 0xb5, 0xf7, 0xbb, 0x06, 0xab, 0x53, 0x3e, 0xc7, 0x28, 0xce, 0x59, 0x80, 0x24, 0x81, + 0xfb, 0x65, 0x8b, 0x91, 0x9e, 0xde, 0xe4, 0x1c, 0xe7, 0x59, 0xe5, 0x3d, 0x3b, 0x3b, 0xdf, 0xff, + 0xfc, 0xbd, 0x36, 0x9f, 0x38, 0xeb, 0xa9, 0x7f, 0xa5, 0x77, 0xbe, 0xeb, 0xa3, 0xa2, 0xbb, 0x85, + 0xaf, 0x4e, 0xbe, 0x2a, 0x18, 0x91, 0x04, 0xd0, 0xba, 0x65, 0x44, 0xb2, 0xae, 0x1b, 0x56, 0x99, + 0x73, 0x76, 0xd6, 0x96, 0x9e, 0x65, 0x93, 0x47, 0x73, 0x67, 0x79, 0x5f, 0x59, 0xf8, 0x8d, 0xc4, + 0xb0, 0x7c, 0xdb, 0x3c, 0xa4, 0xab, 0x5b, 0x55, 0x3b, 0xdb, 0xea, 0x55, 0x17, 0x33, 0xbb, 0x39, + 0x8f, 0xf5, 0xd0, 0x2e, 0x99, 0x2f, 0x30, 0x5d, 0x63, 0xd9, 0x17, 0xf9, 0x1a, 0xe7, 0xd8, 0xc5, + 0x5a, 0x9b, 0xb9, 0xb5, 0xc3, 0xf4, 0x85, 0x99, 0x28, 0x7c, 0xf6, 0x1f, 0x85, 0x07, 0x47, 0xbf, + 0xf6, 0x3f, 0xf4, 0x7b, 0xb0, 0x18, 0xe2, 0x29, 0x1d, 0x0f, 0x15, 0x59, 0x25, 0x2b, 0xd0, 0xb2, + 0x9a, 0x7a, 0xe6, 0xb1, 0xa2, 0x6a, 0x2c, 0x3f, 0x6e, 0xc2, 0x06, 0xd4, 0x0f, 0x90, 0x0a, 0x14, + 0xe4, 0xc1, 0x92, 0x69, 0xb5, 0xe8, 0x58, 0x7d, 0xe2, 0x82, 0x5d, 0xe9, 0xd7, 0xc7, 0x36, 0xfd, + 0x7b, 0x00, 0x37, 0x80, 0x3b, 0x7e, 0x5d, 0x33, 0x79, 0xf1, 0x2f, 0x00, 0x00, 0xff, 0xff, 0xf5, + 0x68, 0xa6, 0x7f, 0x3c, 0x05, 0x00, 0x00, } diff --git a/backend/api/go_client/job.pb.go b/backend/api/go_client/job.pb.go index 20bfc9a254a..9b104ad1554 100755 --- a/backend/api/go_client/job.pb.go +++ b/backend/api/go_client/job.pb.go @@ -64,7 +64,7 @@ func (x Job_Mode) String() string { return proto.EnumName(Job_Mode_name, int32(x)) } func (Job_Mode) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{10, 0} + return fileDescriptor_job_073b88ec5135fd11, []int{10, 0} } type CreateJobRequest struct { @@ -78,7 +78,7 @@ func (m *CreateJobRequest) Reset() { *m = CreateJobRequest{} } func (m *CreateJobRequest) String() string { return proto.CompactTextString(m) } func (*CreateJobRequest) ProtoMessage() {} func (*CreateJobRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{0} + return fileDescriptor_job_073b88ec5135fd11, []int{0} } func (m *CreateJobRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_CreateJobRequest.Unmarshal(m, b) @@ -116,7 +116,7 @@ func (m *GetJobRequest) Reset() { *m = GetJobRequest{} } func (m *GetJobRequest) String() string { return proto.CompactTextString(m) } func (*GetJobRequest) ProtoMessage() {} func (*GetJobRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{1} + return fileDescriptor_job_073b88ec5135fd11, []int{1} } func (m *GetJobRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetJobRequest.Unmarshal(m, b) @@ -158,7 +158,7 @@ func (m *ListJobsRequest) Reset() { *m = ListJobsRequest{} } func (m *ListJobsRequest) String() string { return proto.CompactTextString(m) } func (*ListJobsRequest) ProtoMessage() {} func (*ListJobsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{2} + return fileDescriptor_job_073b88ec5135fd11, []int{2} } func (m *ListJobsRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListJobsRequest.Unmarshal(m, b) @@ -215,6 +215,7 @@ func (m *ListJobsRequest) GetFilter() string { type ListJobsResponse struct { Jobs []*Job `protobuf:"bytes,1,rep,name=jobs,proto3" json:"jobs,omitempty"` + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -225,7 +226,7 @@ func (m *ListJobsResponse) Reset() { *m = ListJobsResponse{} } func (m *ListJobsResponse) String() string { return proto.CompactTextString(m) } func (*ListJobsResponse) ProtoMessage() {} func (*ListJobsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{3} + return fileDescriptor_job_073b88ec5135fd11, []int{3} } func (m *ListJobsResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListJobsResponse.Unmarshal(m, b) @@ -252,6 +253,13 @@ func (m *ListJobsResponse) GetJobs() []*Job { return nil } +func (m *ListJobsResponse) GetTotalSize() int32 { + if m != nil { + return m.TotalSize + } + return 0 +} + func (m *ListJobsResponse) GetNextPageToken() string { if m != nil { return m.NextPageToken @@ -270,7 +278,7 @@ func (m *DeleteJobRequest) Reset() { *m = DeleteJobRequest{} } func (m *DeleteJobRequest) String() string { return proto.CompactTextString(m) } func (*DeleteJobRequest) ProtoMessage() {} func (*DeleteJobRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{4} + return fileDescriptor_job_073b88ec5135fd11, []int{4} } func (m *DeleteJobRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DeleteJobRequest.Unmarshal(m, b) @@ -308,7 +316,7 @@ func (m *EnableJobRequest) Reset() { *m = EnableJobRequest{} } func (m *EnableJobRequest) String() string { return proto.CompactTextString(m) } func (*EnableJobRequest) ProtoMessage() {} func (*EnableJobRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{5} + return fileDescriptor_job_073b88ec5135fd11, []int{5} } func (m *EnableJobRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_EnableJobRequest.Unmarshal(m, b) @@ -346,7 +354,7 @@ func (m *DisableJobRequest) Reset() { *m = DisableJobRequest{} } func (m *DisableJobRequest) String() string { return proto.CompactTextString(m) } func (*DisableJobRequest) ProtoMessage() {} func (*DisableJobRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{6} + return fileDescriptor_job_073b88ec5135fd11, []int{6} } func (m *DisableJobRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DisableJobRequest.Unmarshal(m, b) @@ -386,7 +394,7 @@ func (m *CronSchedule) Reset() { *m = CronSchedule{} } func (m *CronSchedule) String() string { return proto.CompactTextString(m) } func (*CronSchedule) ProtoMessage() {} func (*CronSchedule) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{7} + return fileDescriptor_job_073b88ec5135fd11, []int{7} } func (m *CronSchedule) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_CronSchedule.Unmarshal(m, b) @@ -440,7 +448,7 @@ func (m *PeriodicSchedule) Reset() { *m = PeriodicSchedule{} } func (m *PeriodicSchedule) String() string { return proto.CompactTextString(m) } func (*PeriodicSchedule) ProtoMessage() {} func (*PeriodicSchedule) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{8} + return fileDescriptor_job_073b88ec5135fd11, []int{8} } func (m *PeriodicSchedule) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PeriodicSchedule.Unmarshal(m, b) @@ -495,7 +503,7 @@ func (m *Trigger) Reset() { *m = Trigger{} } func (m *Trigger) String() string { return proto.CompactTextString(m) } func (*Trigger) ProtoMessage() {} func (*Trigger) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{9} + return fileDescriptor_job_073b88ec5135fd11, []int{9} } func (m *Trigger) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Trigger.Unmarshal(m, b) @@ -649,7 +657,7 @@ func (m *Job) Reset() { *m = Job{} } func (m *Job) String() string { return proto.CompactTextString(m) } func (*Job) ProtoMessage() {} func (*Job) Descriptor() ([]byte, []int) { - return fileDescriptor_job_ba819acef2ed9144, []int{10} + return fileDescriptor_job_073b88ec5135fd11, []int{10} } func (m *Job) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Job.Unmarshal(m, b) @@ -1012,76 +1020,77 @@ var _JobService_serviceDesc = grpc.ServiceDesc{ Metadata: "backend/api/job.proto", } -func init() { proto.RegisterFile("backend/api/job.proto", fileDescriptor_job_ba819acef2ed9144) } +func init() { proto.RegisterFile("backend/api/job.proto", fileDescriptor_job_073b88ec5135fd11) } -var fileDescriptor_job_ba819acef2ed9144 = []byte{ - // 1075 bytes of a gzipped FileDescriptorProto +var fileDescriptor_job_073b88ec5135fd11 = []byte{ + // 1090 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x56, 0xdd, 0x52, 0x23, 0xc5, 0x17, 0x27, 0x1f, 0xe4, 0xe3, 0x90, 0x40, 0xe8, 0xe5, 0x63, 0xfe, 0x59, 0xf6, 0x4f, 0x76, 0xb4, 0x58, 0xca, 0x72, 0x93, 0x5a, 0xb6, 0xb4, 0xd4, 0x3b, 0x20, 0xc8, 0x0a, 0x0b, 0x4b, 0x4d, 0xb0, - 0xb4, 0xf4, 0x62, 0xaa, 0x67, 0xe6, 0x90, 0x1d, 0x48, 0xa6, 0xc7, 0xee, 0x0e, 0x12, 0x2c, 0x6f, - 0x7c, 0x04, 0xf5, 0x05, 0x7c, 0x00, 0xaf, 0x7c, 0x07, 0x5f, 0xc0, 0x57, 0xf0, 0x41, 0xac, 0xee, - 0xe9, 0x09, 0xf9, 0x58, 0xe4, 0xd2, 0xab, 0xcc, 0x39, 0xe7, 0x77, 0x4e, 0x9f, 0xef, 0x13, 0x58, - 0xf5, 0xa8, 0x7f, 0x85, 0x51, 0xd0, 0xa2, 0x71, 0xd8, 0xba, 0x64, 0x5e, 0x33, 0xe6, 0x4c, 0x32, - 0x92, 0xa3, 0x71, 0x58, 0xdf, 0xe8, 0x32, 0xd6, 0xed, 0xa1, 0x16, 0xd1, 0x28, 0x62, 0x92, 0xca, - 0x90, 0x45, 0x22, 0x81, 0xd4, 0x37, 0x8d, 0x54, 0x53, 0xde, 0xe0, 0xa2, 0x25, 0xc3, 0x3e, 0x0a, - 0x49, 0xfb, 0xb1, 0x01, 0x3c, 0x9e, 0x06, 0x60, 0x3f, 0x96, 0xc3, 0x54, 0x38, 0xfe, 0x6e, 0x4c, - 0x39, 0xed, 0xa3, 0x44, 0x9e, 0x9a, 0x9e, 0x10, 0x86, 0x31, 0xf6, 0xc2, 0x08, 0x5d, 0x11, 0xa3, - 0x6f, 0x00, 0xef, 0x8f, 0x03, 0x38, 0x0a, 0x36, 0xe0, 0x3e, 0xba, 0x1c, 0x2f, 0x90, 0x63, 0xe4, - 0xa3, 0x41, 0x4d, 0xc4, 0xc6, 0x07, 0x91, 0x61, 0x7f, 0xa8, 0x7f, 0xfc, 0xe7, 0x5d, 0x8c, 0x9e, - 0x8b, 0xef, 0x69, 0xb7, 0x8b, 0xbc, 0xc5, 0x62, 0x1d, 0xda, 0x3b, 0xc2, 0x5c, 0x1f, 0x37, 0x82, - 0x9c, 0x33, 0xe3, 0xa4, 0xdd, 0x84, 0xda, 0x3e, 0x47, 0x2a, 0xf1, 0x88, 0x79, 0x0e, 0x7e, 0x37, - 0x40, 0x21, 0x49, 0x1d, 0x72, 0x97, 0xcc, 0xb3, 0x32, 0x8d, 0xcc, 0xf6, 0xc2, 0x4e, 0xa9, 0x49, - 0xe3, 0xb0, 0xa9, 0xa4, 0x8a, 0x69, 0x6f, 0x42, 0xf5, 0x10, 0xe5, 0x18, 0x78, 0x11, 0xb2, 0x61, - 0xa0, 0xb1, 0x65, 0x27, 0x1b, 0x06, 0xf6, 0x9f, 0x19, 0x58, 0x7a, 0x1d, 0x0a, 0x05, 0x11, 0x29, - 0xe6, 0x09, 0x40, 0x4c, 0xbb, 0xe8, 0x4a, 0x76, 0x85, 0x91, 0xc1, 0x96, 0x15, 0xe7, 0x5c, 0x31, - 0xc8, 0x63, 0xd0, 0x84, 0x2b, 0xc2, 0x5b, 0xb4, 0xb2, 0x8d, 0xcc, 0xf6, 0xbc, 0x53, 0x52, 0x8c, - 0x4e, 0x78, 0x8b, 0x64, 0x1d, 0x8a, 0x82, 0x71, 0xe9, 0x7a, 0x43, 0x2b, 0xa7, 0x15, 0x0b, 0x8a, - 0xdc, 0x1b, 0x92, 0xcf, 0x61, 0x6d, 0x36, 0x67, 0xee, 0x15, 0x0e, 0xad, 0xbc, 0x76, 0xbc, 0xa6, - 0x1d, 0x77, 0x0c, 0xe4, 0x18, 0x87, 0xce, 0x4a, 0x8a, 0x77, 0x52, 0xf8, 0x31, 0x0e, 0xc9, 0x1a, - 0x14, 0x2e, 0xc2, 0x9e, 0x44, 0x6e, 0xcd, 0x27, 0xf6, 0x13, 0xca, 0xfe, 0x1a, 0x6a, 0x77, 0x71, - 0x88, 0x98, 0x45, 0x02, 0xc9, 0x06, 0xe4, 0x2f, 0x99, 0x27, 0xac, 0x4c, 0x23, 0x37, 0x91, 0x1a, - 0xcd, 0x25, 0x5b, 0xb0, 0x14, 0xe1, 0x8d, 0x74, 0xc7, 0x62, 0xcd, 0x6a, 0x93, 0x55, 0xc5, 0x3e, - 0x4b, 0xe3, 0xb5, 0x6d, 0xa8, 0xb5, 0xb1, 0x87, 0x13, 0x39, 0x9f, 0x4e, 0xa3, 0x0d, 0xb5, 0x83, - 0x88, 0x7a, 0xbd, 0x7f, 0xc3, 0xbc, 0x07, 0xcb, 0xed, 0x50, 0x3c, 0x00, 0xfa, 0x35, 0x03, 0x95, - 0x7d, 0xce, 0xa2, 0x8e, 0xff, 0x16, 0x83, 0x41, 0x0f, 0xc9, 0xa7, 0x00, 0x42, 0x52, 0x2e, 0x5d, - 0xd5, 0xe9, 0xa6, 0xc8, 0xf5, 0x66, 0xd2, 0xe5, 0xcd, 0xb4, 0xcb, 0x9b, 0xe7, 0xe9, 0x18, 0x38, - 0x65, 0x8d, 0x56, 0x34, 0xf9, 0x08, 0x4a, 0x18, 0x05, 0x89, 0x62, 0xf6, 0x41, 0xc5, 0x22, 0x46, - 0x81, 0x56, 0x23, 0x90, 0xf7, 0x39, 0x8b, 0x4c, 0xfd, 0xf4, 0xb7, 0xfd, 0x7b, 0x06, 0x6a, 0x67, - 0xc8, 0x43, 0x16, 0x84, 0xfe, 0x7f, 0xe8, 0xda, 0x33, 0x58, 0x0a, 0x23, 0x89, 0xfc, 0x9a, 0xf6, - 0x5c, 0x81, 0x3e, 0x8b, 0x02, 0xed, 0x65, 0xce, 0x59, 0x4c, 0xd9, 0x1d, 0xcd, 0x55, 0x69, 0x2c, - 0x9e, 0xf3, 0x50, 0x8d, 0x19, 0xf9, 0x04, 0xaa, 0x2a, 0x06, 0x57, 0x18, 0xbf, 0x8d, 0xa7, 0xcb, - 0xba, 0x1d, 0xc6, 0x73, 0xfd, 0x6a, 0xce, 0xa9, 0xf8, 0xe3, 0xb9, 0x6f, 0xc3, 0x72, 0x6c, 0x82, - 0xbe, 0xd3, 0x4e, 0xdc, 0x5d, 0xd5, 0xda, 0xd3, 0x29, 0x79, 0x35, 0xe7, 0xd4, 0xe2, 0x29, 0xde, - 0x5e, 0x19, 0x8a, 0x32, 0x71, 0xc5, 0xfe, 0x23, 0x0f, 0xb9, 0x23, 0xe6, 0x4d, 0x57, 0x5d, 0xa5, - 0x3c, 0xa2, 0x26, 0x15, 0x65, 0x47, 0x7f, 0x93, 0x06, 0x2c, 0x04, 0x28, 0x7c, 0x1e, 0xea, 0x2d, - 0x61, 0xaa, 0x31, 0xce, 0x22, 0x1f, 0x43, 0x75, 0x62, 0x4f, 0x99, 0x49, 0x4a, 0x02, 0x3b, 0x33, - 0x92, 0x4e, 0x8c, 0xbe, 0x53, 0x89, 0xc7, 0x28, 0x72, 0x08, 0x8f, 0x66, 0x47, 0x51, 0x58, 0xf3, - 0x7a, 0x4a, 0xd6, 0x26, 0xe6, 0x70, 0x34, 0x7a, 0x0e, 0x99, 0x99, 0x46, 0xa1, 0xca, 0xd1, 0xa7, - 0x37, 0xae, 0xcf, 0x22, 0x7f, 0xc0, 0x15, 0x6f, 0x68, 0x15, 0x92, 0x72, 0xf4, 0xe9, 0xcd, 0xfe, - 0x1d, 0x97, 0x6c, 0x8d, 0x52, 0x60, 0x15, 0xb5, 0x8f, 0x15, 0xfd, 0x8a, 0xa9, 0x90, 0x93, 0x0a, - 0xc9, 0x53, 0xc8, 0xf7, 0x59, 0x80, 0x56, 0xa9, 0x91, 0xd9, 0x5e, 0xdc, 0xa9, 0xa6, 0x03, 0xdb, - 0x3c, 0x61, 0x01, 0x3a, 0x5a, 0xa4, 0x9a, 0xce, 0xd7, 0x1b, 0x30, 0x70, 0xa9, 0xb4, 0xca, 0x0f, - 0x37, 0x9d, 0x41, 0xef, 0x4a, 0xa5, 0x3a, 0x88, 0x83, 0x54, 0x15, 0x1e, 0x56, 0x35, 0xe8, 0x5d, - 0xa9, 0xb6, 0x8e, 0x90, 0x54, 0x0e, 0x84, 0xb5, 0x60, 0xb6, 0x9a, 0xa6, 0xc8, 0x0a, 0xcc, 0xeb, - 0xf5, 0x6c, 0x55, 0x34, 0x3b, 0x21, 0x88, 0x05, 0x45, 0xd4, 0xdb, 0x20, 0xb0, 0x6a, 0x8d, 0xcc, - 0x76, 0xc9, 0x49, 0x49, 0xfb, 0x25, 0xe4, 0x55, 0x2c, 0xa4, 0x06, 0x95, 0x2f, 0x4f, 0x8f, 0x4f, - 0xdf, 0x7c, 0x75, 0xea, 0x9e, 0xbc, 0x69, 0x1f, 0xd4, 0xe6, 0xc8, 0x02, 0x14, 0x0f, 0x4e, 0x77, - 0xf7, 0x5e, 0x1f, 0xb4, 0x6b, 0x19, 0x52, 0x81, 0x52, 0xfb, 0x8b, 0x4e, 0x42, 0x65, 0x77, 0x7e, - 0xcb, 0x03, 0x1c, 0x31, 0xaf, 0x83, 0xfc, 0x3a, 0xf4, 0x91, 0x9c, 0x40, 0x79, 0x74, 0x03, 0xc8, - 0xaa, 0xe9, 0xe2, 0xc9, 0x9b, 0x50, 0x1f, 0xed, 0x3a, 0x7b, 0xf3, 0xa7, 0xbf, 0xfe, 0xfe, 0x25, - 0xfb, 0x3f, 0x9b, 0xa8, 0x5b, 0x22, 0x5a, 0xd7, 0x2f, 0x3c, 0x94, 0xf4, 0x85, 0xba, 0xba, 0xe2, - 0x33, 0x75, 0x22, 0xc8, 0x21, 0x14, 0x92, 0x13, 0x41, 0x88, 0x56, 0x9a, 0xb8, 0x17, 0xb3, 0x86, - 0xc8, 0xfa, 0xac, 0xa1, 0xd6, 0x0f, 0x61, 0xf0, 0x23, 0xe9, 0x40, 0x29, 0xdd, 0xc0, 0x64, 0x45, - 0xab, 0x4d, 0x1d, 0x96, 0xfa, 0xea, 0x14, 0x37, 0x59, 0xd3, 0x76, 0x5d, 0x5b, 0x5e, 0x21, 0xef, - 0x70, 0x91, 0x78, 0x50, 0x1e, 0x2d, 0x56, 0x13, 0xec, 0xf4, 0xa2, 0xad, 0xaf, 0xcd, 0xd4, 0xf0, - 0x40, 0x1d, 0x7d, 0x7b, 0x4b, 0xdb, 0x6d, 0xd8, 0xff, 0xbf, 0xc7, 0xe3, 0x56, 0x52, 0x15, 0x82, - 0x00, 0x77, 0x8b, 0x99, 0x24, 0x03, 0x30, 0xb3, 0xa9, 0xef, 0x7d, 0xe5, 0x99, 0x7e, 0xe5, 0xa9, - 0xbd, 0x79, 0xdf, 0x2b, 0x41, 0x62, 0x8a, 0x7c, 0x0b, 0xe5, 0xd1, 0x1d, 0x31, 0xa1, 0x4c, 0xdf, - 0x95, 0x7b, 0x1f, 0x31, 0xc9, 0xff, 0xe0, 0xbe, 0xe4, 0xef, 0x9d, 0xfd, 0xbc, 0x7b, 0xe2, 0x6c, - 0x40, 0x31, 0xc0, 0x0b, 0x3a, 0xe8, 0x49, 0xb2, 0x4c, 0x96, 0xa0, 0x5a, 0x5f, 0xd0, 0xaf, 0x74, - 0x74, 0xaf, 0x7e, 0xb3, 0x09, 0x4f, 0xa0, 0xb0, 0x87, 0x94, 0x23, 0x27, 0x8f, 0x4a, 0xd9, 0x7a, - 0x95, 0x0e, 0xe4, 0x5b, 0xc6, 0xc3, 0x5b, 0xfd, 0xd7, 0xa3, 0x91, 0xf5, 0x2a, 0x00, 0x23, 0xc0, - 0x9c, 0x57, 0xd0, 0x2e, 0xbc, 0xfc, 0x27, 0x00, 0x00, 0xff, 0xff, 0x4c, 0x07, 0x6a, 0x5f, 0xad, - 0x09, 0x00, 0x00, + 0xac, 0xd2, 0x8b, 0xa9, 0x9e, 0x99, 0x43, 0x76, 0x20, 0x99, 0x1e, 0xbb, 0x3b, 0x2c, 0xc1, 0xf2, + 0xc6, 0x47, 0x50, 0x5f, 0xc0, 0x07, 0xf0, 0xca, 0x77, 0xf0, 0x05, 0x7c, 0x05, 0x1f, 0xc4, 0xea, + 0x9e, 0x9e, 0x90, 0x8f, 0x45, 0x2e, 0xbd, 0x4a, 0xce, 0xaf, 0x7f, 0xa7, 0xcf, 0x57, 0x9f, 0x73, + 0x06, 0x56, 0x3d, 0xea, 0x5f, 0x61, 0x14, 0xb4, 0x68, 0x1c, 0xb6, 0x2e, 0x99, 0xd7, 0x8c, 0x39, + 0x93, 0x8c, 0xe4, 0x68, 0x1c, 0xd6, 0x37, 0xba, 0x8c, 0x75, 0x7b, 0xa8, 0x8f, 0x68, 0x14, 0x31, + 0x49, 0x65, 0xc8, 0x22, 0x91, 0x50, 0xea, 0x9b, 0xe6, 0x54, 0x4b, 0xde, 0xe0, 0xa2, 0x25, 0xc3, + 0x3e, 0x0a, 0x49, 0xfb, 0xb1, 0x21, 0x3c, 0x9e, 0x26, 0x60, 0x3f, 0x96, 0xc3, 0xf4, 0x70, 0xdc, + 0x6e, 0x4c, 0x39, 0xed, 0xa3, 0x44, 0x9e, 0x5e, 0x3d, 0x71, 0x18, 0xc6, 0xd8, 0x0b, 0x23, 0x74, + 0x45, 0x8c, 0xbe, 0x21, 0x7c, 0x38, 0x4e, 0xe0, 0x28, 0xd8, 0x80, 0xfb, 0xe8, 0x72, 0xbc, 0x40, + 0x8e, 0x91, 0x8f, 0x86, 0x35, 0x11, 0x1b, 0x1f, 0x44, 0x06, 0xfe, 0x58, 0xff, 0xf8, 0xcf, 0xbb, + 0x18, 0x3d, 0x17, 0xef, 0x68, 0xb7, 0x8b, 0xbc, 0xc5, 0x62, 0x1d, 0xda, 0x7b, 0xc2, 0x5c, 0x1f, + 0xbf, 0x04, 0x39, 0x67, 0xc6, 0x49, 0xbb, 0x09, 0xb5, 0x7d, 0x8e, 0x54, 0xe2, 0x11, 0xf3, 0x1c, + 0xfc, 0x7e, 0x80, 0x42, 0x92, 0x3a, 0xe4, 0x2e, 0x99, 0x67, 0x65, 0x1a, 0x99, 0xed, 0x85, 0x9d, + 0x52, 0x93, 0xc6, 0x61, 0x53, 0x9d, 0x2a, 0xd0, 0xde, 0x84, 0xea, 0x21, 0xca, 0x31, 0xf2, 0x22, + 0x64, 0xc3, 0x40, 0x73, 0xcb, 0x4e, 0x36, 0x0c, 0xec, 0x3f, 0x33, 0xb0, 0xf4, 0x3a, 0x14, 0x8a, + 0x22, 0x52, 0xce, 0x13, 0x80, 0x98, 0x76, 0xd1, 0x95, 0xec, 0x0a, 0x23, 0xc3, 0x2d, 0x2b, 0xe4, + 0x5c, 0x01, 0xe4, 0x31, 0x68, 0xc1, 0x15, 0xe1, 0x2d, 0x5a, 0xd9, 0x46, 0x66, 0x7b, 0xde, 0x29, + 0x29, 0xa0, 0x13, 0xde, 0x22, 0x59, 0x87, 0xa2, 0x60, 0x5c, 0xba, 0xde, 0xd0, 0xca, 0x69, 0xc5, + 0x82, 0x12, 0xf7, 0x86, 0xe4, 0x4b, 0x58, 0x9b, 0xcd, 0x99, 0x7b, 0x85, 0x43, 0x2b, 0xaf, 0x1d, + 0xaf, 0x69, 0xc7, 0x1d, 0x43, 0x39, 0xc6, 0xa1, 0xb3, 0x92, 0xf2, 0x9d, 0x94, 0x7e, 0x8c, 0x43, + 0xb2, 0x06, 0x85, 0x8b, 0xb0, 0x27, 0x91, 0x5b, 0xf3, 0xc9, 0xfd, 0x89, 0x64, 0xbf, 0x83, 0xda, + 0x5d, 0x1c, 0x22, 0x66, 0x91, 0x40, 0xb2, 0x01, 0xf9, 0x4b, 0xe6, 0x09, 0x2b, 0xd3, 0xc8, 0x4d, + 0xa4, 0x46, 0xa3, 0x2a, 0x4c, 0xc9, 0x24, 0xed, 0x25, 0x81, 0xe4, 0x74, 0x20, 0x65, 0x8d, 0xe8, + 0x48, 0xb6, 0x60, 0x29, 0xc2, 0x1b, 0xe9, 0x8e, 0xa5, 0x22, 0xab, 0x2d, 0x56, 0x15, 0x7c, 0x96, + 0xa6, 0xc3, 0xb6, 0xa1, 0xd6, 0xc6, 0x1e, 0x4e, 0x94, 0x64, 0x3a, 0xcb, 0x36, 0xd4, 0x0e, 0x22, + 0xea, 0xf5, 0xfe, 0x8d, 0xf3, 0x01, 0x2c, 0xb7, 0x43, 0xf1, 0x00, 0xe9, 0xd7, 0x0c, 0x54, 0xf6, + 0x39, 0x8b, 0x3a, 0xfe, 0x5b, 0x0c, 0x06, 0x3d, 0x24, 0x9f, 0x03, 0x08, 0x49, 0xb9, 0x74, 0x55, + 0x23, 0x98, 0x37, 0x50, 0x6f, 0x26, 0x4d, 0xd0, 0x4c, 0x9b, 0xa0, 0x79, 0x9e, 0x76, 0x89, 0x53, + 0xd6, 0x6c, 0x25, 0x93, 0x4f, 0xa0, 0x84, 0x51, 0x90, 0x28, 0x66, 0x1f, 0x54, 0x2c, 0x62, 0x14, + 0x68, 0x35, 0x02, 0x79, 0x9f, 0xb3, 0xc8, 0x94, 0x57, 0xff, 0xb7, 0x7f, 0xcf, 0x40, 0xed, 0x0c, + 0x79, 0xc8, 0x82, 0xd0, 0xff, 0x0f, 0x5d, 0x7b, 0x06, 0x4b, 0x61, 0x24, 0x91, 0x5f, 0xab, 0xa2, + 0xa2, 0xcf, 0xa2, 0x40, 0x7b, 0x99, 0x73, 0x16, 0x53, 0xb8, 0xa3, 0x51, 0x95, 0xc6, 0xe2, 0x39, + 0x0f, 0x55, 0x17, 0x92, 0xcf, 0xa0, 0xaa, 0x62, 0x70, 0x85, 0xf1, 0xdb, 0x78, 0xba, 0xac, 0x5f, + 0xcb, 0x78, 0xae, 0x5f, 0xcd, 0x39, 0x15, 0x7f, 0x3c, 0xf7, 0x6d, 0x58, 0x8e, 0x4d, 0xd0, 0x77, + 0xda, 0x89, 0xbb, 0xab, 0x5a, 0x7b, 0x3a, 0x25, 0xaf, 0xe6, 0x9c, 0x5a, 0x3c, 0x85, 0xed, 0x95, + 0xa1, 0x28, 0x13, 0x57, 0xec, 0x3f, 0xf2, 0x90, 0x3b, 0x62, 0xde, 0x74, 0xd5, 0x55, 0xca, 0x23, + 0x6a, 0x52, 0x51, 0x76, 0xf4, 0x7f, 0xd2, 0x80, 0x85, 0x00, 0x85, 0xcf, 0x43, 0x3d, 0x44, 0x4c, + 0x35, 0xc6, 0x21, 0xf2, 0x29, 0x54, 0x27, 0xc6, 0x98, 0x69, 0xb4, 0x24, 0xb0, 0x33, 0x73, 0xd2, + 0x89, 0xd1, 0x77, 0x2a, 0xf1, 0x98, 0x44, 0x0e, 0xe1, 0xd1, 0x6c, 0xa7, 0x0a, 0x6b, 0x5e, 0x37, + 0xd1, 0xda, 0x44, 0x9b, 0x8e, 0x3a, 0xd3, 0x21, 0x33, 0xcd, 0x2a, 0x54, 0x39, 0xfa, 0xf4, 0xc6, + 0xf5, 0x59, 0xe4, 0x0f, 0xb8, 0xc2, 0x86, 0x56, 0x21, 0x29, 0x47, 0x9f, 0xde, 0xec, 0xdf, 0xa1, + 0x64, 0x6b, 0x94, 0x02, 0xab, 0xa8, 0x7d, 0xac, 0x68, 0x2b, 0xa6, 0x42, 0x4e, 0x7a, 0x48, 0x9e, + 0x42, 0xbe, 0xcf, 0x02, 0xb4, 0x4a, 0x8d, 0xcc, 0xf6, 0xe2, 0x4e, 0x35, 0xed, 0xe7, 0xe6, 0x09, + 0x0b, 0xd0, 0xd1, 0x47, 0xea, 0xd1, 0xf9, 0x7a, 0x40, 0x06, 0x2e, 0x95, 0x56, 0xf9, 0xe1, 0x47, + 0x67, 0xd8, 0xbb, 0x52, 0xa9, 0x0e, 0xe2, 0x20, 0x55, 0x85, 0x87, 0x55, 0x0d, 0x7b, 0x57, 0xaa, + 0xa1, 0x24, 0x24, 0x95, 0x03, 0x61, 0x2d, 0x98, 0xa1, 0xa7, 0x25, 0xb2, 0x02, 0xf3, 0x7a, 0x7a, + 0x5b, 0x15, 0x0d, 0x27, 0x02, 0xb1, 0xa0, 0x88, 0x7a, 0x1a, 0x04, 0x56, 0xad, 0x91, 0xd9, 0x2e, + 0x39, 0xa9, 0x68, 0xbf, 0x84, 0xbc, 0x8a, 0x85, 0xd4, 0xa0, 0xf2, 0xf5, 0xe9, 0xf1, 0xe9, 0x9b, + 0x6f, 0x4e, 0xdd, 0x93, 0x37, 0xed, 0x83, 0xda, 0x1c, 0x59, 0x80, 0xe2, 0xc1, 0xe9, 0xee, 0xde, + 0xeb, 0x83, 0x76, 0x2d, 0x43, 0x2a, 0x50, 0x6a, 0x7f, 0xd5, 0x49, 0xa4, 0xec, 0xce, 0x6f, 0x79, + 0x80, 0x23, 0xe6, 0x75, 0x90, 0x5f, 0x87, 0x3e, 0x92, 0x13, 0x28, 0x8f, 0x56, 0x04, 0x59, 0x35, + 0xaf, 0x78, 0x72, 0x65, 0xd4, 0x47, 0xa3, 0xd0, 0xde, 0xfc, 0xe9, 0xaf, 0xbf, 0x7f, 0xc9, 0xfe, + 0xcf, 0x26, 0x6a, 0xd5, 0x88, 0xd6, 0xf5, 0x0b, 0x0f, 0x25, 0x7d, 0xa1, 0x96, 0xb2, 0xf8, 0x42, + 0x6d, 0x10, 0x72, 0x08, 0x85, 0x64, 0x83, 0x10, 0xa2, 0x95, 0x26, 0xd6, 0xc9, 0xec, 0x45, 0x64, + 0x7d, 0xf6, 0xa2, 0xd6, 0x0f, 0x61, 0xf0, 0x23, 0xe9, 0x40, 0x29, 0x1d, 0xd0, 0x64, 0x45, 0xab, + 0x4d, 0xed, 0x9d, 0xfa, 0xea, 0x14, 0x9a, 0x4c, 0x71, 0xbb, 0xae, 0x6f, 0x5e, 0x21, 0xef, 0x71, + 0x91, 0x78, 0x50, 0x1e, 0x0d, 0x56, 0x13, 0xec, 0xf4, 0xa0, 0xad, 0xaf, 0xcd, 0xd4, 0xf0, 0x40, + 0x7d, 0x13, 0xd8, 0x5b, 0xfa, 0xde, 0x86, 0xfd, 0xff, 0x7b, 0x3c, 0x6e, 0x25, 0x55, 0x21, 0x08, + 0x70, 0x37, 0x98, 0x49, 0xd2, 0x00, 0x33, 0x93, 0xfa, 0x5e, 0x2b, 0xcf, 0xb4, 0x95, 0xa7, 0xf6, + 0xe6, 0x7d, 0x56, 0x82, 0xe4, 0x2a, 0xf2, 0x1d, 0x94, 0x47, 0x7b, 0xc4, 0x84, 0x32, 0xbd, 0x57, + 0xee, 0x35, 0x62, 0x92, 0xff, 0xd1, 0x7d, 0xc9, 0xdf, 0x3b, 0xfb, 0x79, 0xf7, 0xc4, 0xd9, 0x80, + 0x62, 0x80, 0x17, 0x74, 0xd0, 0x93, 0x64, 0x99, 0x2c, 0x41, 0xb5, 0xbe, 0xa0, 0xad, 0x74, 0xf4, + 0x5b, 0xfd, 0x76, 0x13, 0x9e, 0x40, 0x61, 0x0f, 0x29, 0x47, 0x4e, 0x1e, 0x95, 0xb2, 0xf5, 0x2a, + 0x1d, 0xc8, 0xb7, 0x8c, 0x87, 0xb7, 0xfa, 0xcb, 0xa4, 0x91, 0xf5, 0x2a, 0x00, 0x23, 0xc2, 0x9c, + 0x57, 0xd0, 0x2e, 0xbc, 0xfc, 0x27, 0x00, 0x00, 0xff, 0xff, 0x03, 0x21, 0xd3, 0x01, 0xcc, 0x09, + 0x00, 0x00, } diff --git a/backend/api/go_client/pipeline.pb.go b/backend/api/go_client/pipeline.pb.go index f18131f7d29..36f8947b954 100755 --- a/backend/api/go_client/pipeline.pb.go +++ b/backend/api/go_client/pipeline.pb.go @@ -52,7 +52,7 @@ func (m *Url) Reset() { *m = Url{} } func (m *Url) String() string { return proto.CompactTextString(m) } func (*Url) ProtoMessage() {} func (*Url) Descriptor() ([]byte, []int) { - return fileDescriptor_pipeline_a836a461d06dc62b, []int{0} + return fileDescriptor_pipeline_38b26ddca62efc84, []int{0} } func (m *Url) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Url.Unmarshal(m, b) @@ -90,7 +90,7 @@ func (m *CreatePipelineRequest) Reset() { *m = CreatePipelineRequest{} } func (m *CreatePipelineRequest) String() string { return proto.CompactTextString(m) } func (*CreatePipelineRequest) ProtoMessage() {} func (*CreatePipelineRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_pipeline_a836a461d06dc62b, []int{1} + return fileDescriptor_pipeline_38b26ddca62efc84, []int{1} } func (m *CreatePipelineRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_CreatePipelineRequest.Unmarshal(m, b) @@ -128,7 +128,7 @@ func (m *GetPipelineRequest) Reset() { *m = GetPipelineRequest{} } func (m *GetPipelineRequest) String() string { return proto.CompactTextString(m) } func (*GetPipelineRequest) ProtoMessage() {} func (*GetPipelineRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_pipeline_a836a461d06dc62b, []int{2} + return fileDescriptor_pipeline_38b26ddca62efc84, []int{2} } func (m *GetPipelineRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetPipelineRequest.Unmarshal(m, b) @@ -169,7 +169,7 @@ func (m *ListPipelinesRequest) Reset() { *m = ListPipelinesRequest{} } func (m *ListPipelinesRequest) String() string { return proto.CompactTextString(m) } func (*ListPipelinesRequest) ProtoMessage() {} func (*ListPipelinesRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_pipeline_a836a461d06dc62b, []int{3} + return fileDescriptor_pipeline_38b26ddca62efc84, []int{3} } func (m *ListPipelinesRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListPipelinesRequest.Unmarshal(m, b) @@ -219,6 +219,7 @@ func (m *ListPipelinesRequest) GetFilter() string { type ListPipelinesResponse struct { Pipelines []*Pipeline `protobuf:"bytes,1,rep,name=pipelines,proto3" json:"pipelines,omitempty"` + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -229,7 +230,7 @@ func (m *ListPipelinesResponse) Reset() { *m = ListPipelinesResponse{} } func (m *ListPipelinesResponse) String() string { return proto.CompactTextString(m) } func (*ListPipelinesResponse) ProtoMessage() {} func (*ListPipelinesResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_pipeline_a836a461d06dc62b, []int{4} + return fileDescriptor_pipeline_38b26ddca62efc84, []int{4} } func (m *ListPipelinesResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListPipelinesResponse.Unmarshal(m, b) @@ -256,6 +257,13 @@ func (m *ListPipelinesResponse) GetPipelines() []*Pipeline { return nil } +func (m *ListPipelinesResponse) GetTotalSize() int32 { + if m != nil { + return m.TotalSize + } + return 0 +} + func (m *ListPipelinesResponse) GetNextPageToken() string { if m != nil { return m.NextPageToken @@ -274,7 +282,7 @@ func (m *DeletePipelineRequest) Reset() { *m = DeletePipelineRequest{} } func (m *DeletePipelineRequest) String() string { return proto.CompactTextString(m) } func (*DeletePipelineRequest) ProtoMessage() {} func (*DeletePipelineRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_pipeline_a836a461d06dc62b, []int{5} + return fileDescriptor_pipeline_38b26ddca62efc84, []int{5} } func (m *DeletePipelineRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DeletePipelineRequest.Unmarshal(m, b) @@ -312,7 +320,7 @@ func (m *GetTemplateRequest) Reset() { *m = GetTemplateRequest{} } func (m *GetTemplateRequest) String() string { return proto.CompactTextString(m) } func (*GetTemplateRequest) ProtoMessage() {} func (*GetTemplateRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_pipeline_a836a461d06dc62b, []int{6} + return fileDescriptor_pipeline_38b26ddca62efc84, []int{6} } func (m *GetTemplateRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetTemplateRequest.Unmarshal(m, b) @@ -350,7 +358,7 @@ func (m *GetTemplateResponse) Reset() { *m = GetTemplateResponse{} } func (m *GetTemplateResponse) String() string { return proto.CompactTextString(m) } func (*GetTemplateResponse) ProtoMessage() {} func (*GetTemplateResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_pipeline_a836a461d06dc62b, []int{7} + return fileDescriptor_pipeline_38b26ddca62efc84, []int{7} } func (m *GetTemplateResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetTemplateResponse.Unmarshal(m, b) @@ -394,7 +402,7 @@ func (m *Pipeline) Reset() { *m = Pipeline{} } func (m *Pipeline) String() string { return proto.CompactTextString(m) } func (*Pipeline) ProtoMessage() {} func (*Pipeline) Descriptor() ([]byte, []int) { - return fileDescriptor_pipeline_a836a461d06dc62b, []int{8} + return fileDescriptor_pipeline_38b26ddca62efc84, []int{8} } func (m *Pipeline) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Pipeline.Unmarshal(m, b) @@ -680,55 +688,56 @@ var _PipelineService_serviceDesc = grpc.ServiceDesc{ } func init() { - proto.RegisterFile("backend/api/pipeline.proto", fileDescriptor_pipeline_a836a461d06dc62b) -} - -var fileDescriptor_pipeline_a836a461d06dc62b = []byte{ - // 722 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x54, 0xcd, 0x52, 0x13, 0x41, - 0x10, 0x36, 0x09, 0x84, 0xa4, 0x43, 0x42, 0xd9, 0xfc, 0x64, 0x59, 0x40, 0xe2, 0x4a, 0x61, 0x50, - 0xd9, 0x14, 0x78, 0xd2, 0x1b, 0x51, 0xcb, 0x8b, 0x56, 0x51, 0x01, 0x2e, 0x7a, 0x48, 0x4d, 0x92, - 0x4e, 0x1c, 0xd9, 0xec, 0xae, 0x33, 0x13, 0x14, 0x2c, 0x2f, 0xfa, 0x06, 0x7a, 0xf6, 0xa9, 0x7c, - 0x05, 0xaf, 0xbe, 0x83, 0xb5, 0xb3, 0x3b, 0x21, 0x7f, 0x70, 0x4a, 0xfa, 0xeb, 0x6f, 0xfb, 0xeb, - 0xee, 0xfd, 0x7a, 0xc1, 0x6e, 0xb1, 0xf6, 0x39, 0xf9, 0x9d, 0x1a, 0x0b, 0x79, 0x2d, 0xe4, 0x21, - 0x79, 0xdc, 0x27, 0x37, 0x14, 0x81, 0x0a, 0x30, 0xc3, 0x42, 0x6e, 0x6f, 0xf6, 0x82, 0xa0, 0xe7, - 0x91, 0xce, 0x33, 0xdf, 0x0f, 0x14, 0x53, 0x3c, 0xf0, 0x65, 0x4c, 0xb1, 0xb7, 0x93, 0xac, 0x8e, - 0x5a, 0x83, 0x6e, 0x4d, 0xf1, 0x3e, 0x49, 0xc5, 0xfa, 0x61, 0x42, 0xd8, 0x98, 0x24, 0x50, 0x3f, - 0x54, 0x97, 0x26, 0x39, 0x26, 0xce, 0x04, 0xeb, 0x93, 0x22, 0x91, 0x24, 0xcb, 0xa3, 0x49, 0x12, - 0x22, 0x30, 0x89, 0x27, 0xfa, 0xa7, 0xbd, 0xdf, 0x23, 0x7f, 0x5f, 0x7e, 0x66, 0xbd, 0x1e, 0x89, - 0x5a, 0x10, 0xea, 0xae, 0xa6, 0x3b, 0x74, 0xaa, 0x90, 0x39, 0x13, 0x1e, 0xde, 0x87, 0x45, 0x33, - 0x5d, 0x73, 0x20, 0x3c, 0x2b, 0x55, 0x49, 0x55, 0xf3, 0x8d, 0x82, 0xc1, 0xce, 0x84, 0xe7, 0xd4, - 0x61, 0xf5, 0x85, 0x20, 0xa6, 0xe8, 0x38, 0x01, 0x1b, 0xf4, 0x69, 0x40, 0x52, 0xe1, 0x1e, 0xe4, - 0x0c, 0x4f, 0x3f, 0x57, 0x38, 0x2c, 0xba, 0x2c, 0xe4, 0xee, 0x90, 0x37, 0x4c, 0x3b, 0x3b, 0x80, - 0xaf, 0x49, 0x4d, 0x16, 0x28, 0x41, 0x9a, 0x77, 0x12, 0xc9, 0x34, 0xef, 0x38, 0x3f, 0x52, 0xb0, - 0xf2, 0x86, 0xcb, 0x21, 0x4f, 0x1a, 0xe2, 0x16, 0x40, 0xc8, 0x7a, 0xd4, 0x54, 0xc1, 0x39, 0xf9, - 0xc9, 0x03, 0xf9, 0x08, 0x39, 0x8d, 0x00, 0xdc, 0x00, 0x1d, 0x34, 0x25, 0xbf, 0x22, 0x2b, 0x5d, - 0x49, 0x55, 0xe7, 0x1b, 0xb9, 0x08, 0x38, 0xe1, 0x57, 0x84, 0x65, 0x58, 0x90, 0x81, 0x50, 0xcd, - 0xd6, 0xa5, 0x95, 0xd1, 0x0f, 0x66, 0xa3, 0xb0, 0x7e, 0x89, 0x6b, 0x90, 0xed, 0x72, 0x4f, 0x91, - 0xb0, 0xe6, 0x62, 0x3c, 0x8e, 0x1c, 0x0f, 0x56, 0x27, 0x9a, 0x90, 0x61, 0xe0, 0x4b, 0xc2, 0xc7, - 0x90, 0x37, 0x03, 0x49, 0x2b, 0x55, 0xc9, 0x4c, 0x0f, 0x7c, 0x9d, 0xc7, 0x5d, 0x58, 0xf2, 0xe9, - 0x8b, 0x6a, 0x8e, 0xf4, 0x9d, 0xd6, 0x32, 0xc5, 0x08, 0x3e, 0x36, 0xbd, 0x3b, 0x0f, 0x61, 0xf5, - 0x25, 0x79, 0x34, 0xbd, 0xdd, 0xc9, 0xe5, 0xc4, 0x2b, 0x3c, 0xa5, 0x7e, 0xe8, 0x31, 0x75, 0x23, - 0xeb, 0x00, 0x96, 0xc7, 0x58, 0x49, 0xeb, 0x36, 0xe4, 0x54, 0x82, 0x25, 0xe4, 0x61, 0xec, 0xfc, - 0x4b, 0x41, 0xce, 0x88, 0x4f, 0xd6, 0xc3, 0x67, 0x00, 0x6d, 0xfd, 0xf2, 0x3b, 0x4d, 0xa6, 0xf4, - 0x04, 0x85, 0x43, 0xdb, 0x8d, 0xcd, 0xeb, 0x1a, 0xf3, 0xba, 0xa7, 0xc6, 0xdd, 0x8d, 0x7c, 0xc2, - 0x3e, 0x52, 0x88, 0x30, 0xe7, 0xb3, 0x3e, 0x25, 0x5b, 0xd7, 0xff, 0xb1, 0x02, 0x85, 0x0e, 0xc9, - 0xb6, 0xe0, 0xda, 0x97, 0xc9, 0xe2, 0x47, 0x21, 0x74, 0xa3, 0x57, 0x9d, 0x38, 0x5e, 0x5a, 0xf3, - 0x7a, 0xcb, 0xa5, 0x78, 0xcb, 0x06, 0x6e, 0x8c, 0x30, 0xd0, 0x86, 0x4c, 0xe4, 0xdb, 0x05, 0xdd, - 0x59, 0x4e, 0x13, 0xcf, 0x84, 0xd7, 0x88, 0x40, 0x5c, 0x81, 0x79, 0x7d, 0x20, 0x56, 0x56, 0xeb, - 0xc4, 0xc1, 0xe1, 0xef, 0x39, 0x58, 0x32, 0xf3, 0x9e, 0x90, 0xb8, 0xe0, 0x6d, 0xc2, 0x2e, 0x94, - 0xc6, 0x3d, 0x8e, 0xb6, 0x2e, 0x35, 0xd3, 0xf8, 0xf6, 0xf8, 0x5b, 0x77, 0xf6, 0xbe, 0xff, 0xf9, - 0xfb, 0x2b, 0xfd, 0xc0, 0x29, 0x47, 0x27, 0x29, 0x6b, 0x17, 0x07, 0x2d, 0x52, 0xec, 0x60, 0xf8, - 0xd5, 0x90, 0xcf, 0x87, 0x77, 0x80, 0xef, 0xa1, 0x30, 0x72, 0x07, 0x58, 0xd6, 0x85, 0xa6, 0x2f, - 0x63, 0x52, 0x61, 0x47, 0x2b, 0xdc, 0xc3, 0xcd, 0x1b, 0x14, 0x6a, 0x5f, 0x79, 0xe7, 0x1b, 0xf6, - 0xa0, 0x38, 0x66, 0x5c, 0x5c, 0xd7, 0x55, 0x66, 0x5d, 0x94, 0x6d, 0xcf, 0x4a, 0xc5, 0x66, 0x71, - 0xb6, 0xb5, 0xda, 0x3a, 0xde, 0x34, 0x0f, 0x7e, 0x84, 0xd2, 0xb8, 0x67, 0x93, 0x6d, 0xcd, 0x34, - 0xb2, 0xbd, 0x36, 0x65, 0x97, 0x57, 0xd1, 0xb7, 0xce, 0x0c, 0xf5, 0xe8, 0xf6, 0xa1, 0x42, 0xbd, - 0x31, 0x63, 0xe8, 0xeb, 0x8d, 0x4d, 0x1c, 0x82, 0x6d, 0x4d, 0x27, 0x92, 0x71, 0x5c, 0xad, 0x53, - 0xc5, 0xdd, 0xdb, 0x74, 0x6a, 0xe6, 0x1c, 0x64, 0xfd, 0xf8, 0xe7, 0xd1, 0xdb, 0xc6, 0x26, 0x2c, - 0x74, 0xa8, 0xcb, 0x06, 0x9e, 0xc2, 0xbb, 0xb8, 0x04, 0x45, 0xbb, 0xa0, 0xeb, 0x9f, 0x28, 0xa6, - 0x06, 0xf2, 0xdd, 0x36, 0x6c, 0x41, 0xb6, 0x4e, 0x4c, 0x90, 0xc0, 0xe5, 0x5c, 0xda, 0x2e, 0xb2, - 0x81, 0xfa, 0x10, 0x08, 0x7e, 0xa5, 0x3f, 0xb1, 0x95, 0x74, 0x6b, 0x11, 0x60, 0x48, 0xb8, 0xd3, - 0xca, 0xea, 0xc9, 0x9f, 0xfe, 0x0f, 0x00, 0x00, 0xff, 0xff, 0xf4, 0xea, 0x27, 0x6c, 0x55, 0x06, - 0x00, 0x00, + proto.RegisterFile("backend/api/pipeline.proto", fileDescriptor_pipeline_38b26ddca62efc84) +} + +var fileDescriptor_pipeline_38b26ddca62efc84 = []byte{ + // 739 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x54, 0x4f, 0x53, 0x13, 0x4f, + 0x10, 0xfd, 0x25, 0x81, 0x90, 0x74, 0x48, 0xa8, 0xdf, 0xf0, 0x27, 0xcb, 0x02, 0x12, 0x57, 0x0a, + 0x83, 0xca, 0xa6, 0xc0, 0x93, 0xde, 0x88, 0x5a, 0x5e, 0xb4, 0x8a, 0x0a, 0x70, 0xd1, 0x43, 0x6a, + 0x92, 0x74, 0xe2, 0xc8, 0x66, 0x77, 0x9d, 0x99, 0xa0, 0x60, 0x79, 0xd1, 0xa3, 0x37, 0x3d, 0xfb, + 0xa9, 0xfc, 0x0a, 0x5e, 0xfd, 0x0e, 0xd6, 0xf6, 0xee, 0x84, 0xfc, 0x83, 0x53, 0x32, 0xaf, 0xdf, + 0x76, 0xf7, 0xeb, 0x79, 0x3d, 0x60, 0xb7, 0x78, 0xfb, 0x1c, 0xfd, 0x4e, 0x8d, 0x87, 0xa2, 0x16, + 0x8a, 0x10, 0x3d, 0xe1, 0xa3, 0x1b, 0xca, 0x40, 0x07, 0x2c, 0xc3, 0x43, 0x61, 0x6f, 0xf6, 0x82, + 0xa0, 0xe7, 0x21, 0xc5, 0xb9, 0xef, 0x07, 0x9a, 0x6b, 0x11, 0xf8, 0x2a, 0xa6, 0xd8, 0xdb, 0x49, + 0x94, 0x4e, 0xad, 0x41, 0xb7, 0xa6, 0x45, 0x1f, 0x95, 0xe6, 0xfd, 0x30, 0x21, 0x6c, 0x4c, 0x12, + 0xb0, 0x1f, 0xea, 0x4b, 0x13, 0x1c, 0x2b, 0xce, 0x25, 0xef, 0xa3, 0x46, 0x99, 0x04, 0xcb, 0xa3, + 0x41, 0x94, 0x32, 0x30, 0x81, 0x47, 0xf4, 0xd3, 0xde, 0xef, 0xa1, 0xbf, 0xaf, 0x3e, 0xf2, 0x5e, + 0x0f, 0x65, 0x2d, 0x08, 0xa9, 0xab, 0xe9, 0x0e, 0x9d, 0x2a, 0x64, 0xce, 0xa4, 0xc7, 0xee, 0xc2, + 0xa2, 0x51, 0xd7, 0x1c, 0x48, 0xcf, 0x4a, 0x55, 0x52, 0xd5, 0x7c, 0xa3, 0x60, 0xb0, 0x33, 0xe9, + 0x39, 0x75, 0x58, 0x7d, 0x26, 0x91, 0x6b, 0x3c, 0x4e, 0xc0, 0x06, 0x7e, 0x18, 0xa0, 0xd2, 0x6c, + 0x0f, 0x72, 0x86, 0x47, 0xdf, 0x15, 0x0e, 0x8b, 0x2e, 0x0f, 0x85, 0x3b, 0xe4, 0x0d, 0xc3, 0xce, + 0x0e, 0xb0, 0x97, 0xa8, 0x27, 0x13, 0x94, 0x20, 0x2d, 0x3a, 0x49, 0xc9, 0xb4, 0xe8, 0x38, 0xdf, + 0x52, 0xb0, 0xf2, 0x4a, 0xa8, 0x21, 0x4f, 0x19, 0xe2, 0x16, 0x40, 0xc8, 0x7b, 0xd8, 0xd4, 0xc1, + 0x39, 0xfa, 0xc9, 0x07, 0xf9, 0x08, 0x39, 0x8d, 0x00, 0xb6, 0x01, 0x74, 0x68, 0x2a, 0x71, 0x85, + 0x56, 0xba, 0x92, 0xaa, 0xce, 0x37, 0x72, 0x11, 0x70, 0x22, 0xae, 0x90, 0x95, 0x61, 0x41, 0x05, + 0x52, 0x37, 0x5b, 0x97, 0x56, 0x86, 0x3e, 0xcc, 0x46, 0xc7, 0xfa, 0x25, 0x5b, 0x83, 0x6c, 0x57, + 0x78, 0x1a, 0xa5, 0x35, 0x17, 0xe3, 0xf1, 0xc9, 0xf9, 0x9e, 0x82, 0xd5, 0x89, 0x2e, 0x54, 0x18, + 0xf8, 0x0a, 0xd9, 0x43, 0xc8, 0x1b, 0x45, 0xca, 0x4a, 0x55, 0x32, 0xd3, 0x8a, 0xaf, 0xe3, 0x51, + 0xcf, 0x3a, 0xd0, 0xdc, 0x8b, 0xbb, 0xca, 0x50, 0x57, 0x79, 0x42, 0xa8, 0xad, 0x5d, 0x58, 0xf2, + 0xf1, 0x93, 0x6e, 0x8e, 0xe8, 0x4a, 0x53, 0x1b, 0xc5, 0x08, 0x3e, 0x36, 0xda, 0x9c, 0xfb, 0xb0, + 0xfa, 0x1c, 0x3d, 0x9c, 0x9e, 0xfe, 0xe4, 0xf0, 0xe2, 0x11, 0x9f, 0x62, 0x3f, 0xf4, 0xb8, 0xbe, + 0x91, 0x75, 0x00, 0xcb, 0x63, 0xac, 0x44, 0x99, 0x0d, 0x39, 0x9d, 0x60, 0x09, 0x79, 0x78, 0x76, + 0xfe, 0xa6, 0x20, 0x67, 0x8a, 0x4f, 0xe6, 0x63, 0x4f, 0x00, 0xda, 0x64, 0x8e, 0x4e, 0x93, 0x6b, + 0x52, 0x50, 0x38, 0xb4, 0xdd, 0xd8, 0xdc, 0xae, 0x31, 0xb7, 0x7b, 0x6a, 0xdc, 0xdf, 0xc8, 0x27, + 0xec, 0x23, 0xcd, 0x18, 0xcc, 0xf9, 0xbc, 0x8f, 0xc9, 0xad, 0xd0, 0x7f, 0x56, 0x81, 0x42, 0x07, + 0x55, 0x5b, 0x0a, 0xf2, 0x6d, 0x72, 0x31, 0xa3, 0x10, 0x73, 0x23, 0x2b, 0x24, 0x1b, 0xa1, 0xac, + 0x79, 0xba, 0x84, 0x52, 0x7c, 0x09, 0x06, 0x6e, 0x8c, 0x30, 0x98, 0x0d, 0x99, 0xc8, 0xd7, 0x0b, + 0xd4, 0x59, 0x8e, 0x88, 0x67, 0xd2, 0x6b, 0x44, 0x20, 0x5b, 0x81, 0x79, 0x5a, 0x20, 0x2b, 0x4b, + 0x75, 0xe2, 0xc3, 0xe1, 0xaf, 0x39, 0x58, 0x32, 0x7a, 0x4f, 0x50, 0x5e, 0x88, 0x36, 0xb2, 0x2e, + 0x94, 0xc6, 0x77, 0x80, 0xd9, 0x94, 0x6a, 0xe6, 0x62, 0xd8, 0xe3, 0xa6, 0x70, 0xf6, 0xbe, 0xfe, + 0xfe, 0xf3, 0x33, 0x7d, 0xcf, 0x29, 0x47, 0x2b, 0xab, 0x6a, 0x17, 0x07, 0x2d, 0xd4, 0xfc, 0x60, + 0xf8, 0xaa, 0xa8, 0xa7, 0xc3, 0x3d, 0x61, 0x6f, 0xa1, 0x30, 0xb2, 0x27, 0xac, 0x4c, 0x89, 0xa6, + 0x37, 0x67, 0xb2, 0xc2, 0x0e, 0x55, 0xb8, 0xc3, 0x36, 0x6f, 0xa8, 0x50, 0xfb, 0x2c, 0x3a, 0x5f, + 0x58, 0x0f, 0x8a, 0x63, 0xbe, 0x66, 0xeb, 0x94, 0x65, 0xd6, 0xc6, 0xd9, 0xf6, 0xac, 0x50, 0x6c, + 0x16, 0x67, 0x9b, 0xaa, 0xad, 0xb3, 0x9b, 0xf4, 0xb0, 0xf7, 0x50, 0x1a, 0xf7, 0x6c, 0x32, 0xad, + 0x99, 0x46, 0xb6, 0xd7, 0xa6, 0xec, 0xf2, 0x22, 0x7a, 0x0b, 0x8d, 0xa8, 0x07, 0xb7, 0x8b, 0x0a, + 0x69, 0x62, 0xc6, 0xd0, 0xd7, 0x13, 0x9b, 0x58, 0x04, 0xdb, 0x9a, 0x0e, 0x24, 0x72, 0x5c, 0xaa, + 0x53, 0x65, 0xbb, 0xb7, 0xd5, 0xa9, 0x99, 0x75, 0x50, 0xf5, 0xe3, 0x1f, 0x47, 0xaf, 0x1b, 0x9b, + 0xb0, 0xd0, 0xc1, 0x2e, 0x1f, 0x78, 0x9a, 0xfd, 0xcf, 0x96, 0xa0, 0x68, 0x17, 0x28, 0xff, 0x89, + 0xe6, 0x7a, 0xa0, 0xde, 0x6c, 0xc3, 0x16, 0x64, 0xeb, 0xc8, 0x25, 0x4a, 0xb6, 0x9c, 0x4b, 0xdb, + 0x45, 0x3e, 0xd0, 0xef, 0x02, 0x29, 0xae, 0xe8, 0x09, 0xae, 0xa4, 0x5b, 0x8b, 0x00, 0x43, 0xc2, + 0x7f, 0xad, 0x2c, 0x29, 0x7f, 0xfc, 0x2f, 0x00, 0x00, 0xff, 0xff, 0xec, 0xd8, 0xa0, 0x22, 0x75, + 0x06, 0x00, 0x00, } diff --git a/backend/api/go_client/run.pb.go b/backend/api/go_client/run.pb.go index dc14db66d1e..c796e2369c2 100755 --- a/backend/api/go_client/run.pb.go +++ b/backend/api/go_client/run.pb.go @@ -61,7 +61,7 @@ func (x Run_StorageState) String() string { return proto.EnumName(Run_StorageState_name, int32(x)) } func (Run_StorageState) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{7, 0} + return fileDescriptor_run_49d294eb7256008c, []int{7, 0} } type RunMetric_Format int32 @@ -87,7 +87,7 @@ func (x RunMetric_Format) String() string { return proto.EnumName(RunMetric_Format_name, int32(x)) } func (RunMetric_Format) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{10, 0} + return fileDescriptor_run_49d294eb7256008c, []int{10, 0} } type ReportRunMetricsResponse_ReportRunMetricResult_Status int32 @@ -119,7 +119,7 @@ func (x ReportRunMetricsResponse_ReportRunMetricResult_Status) String() string { return proto.EnumName(ReportRunMetricsResponse_ReportRunMetricResult_Status_name, int32(x)) } func (ReportRunMetricsResponse_ReportRunMetricResult_Status) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{12, 0, 0} + return fileDescriptor_run_49d294eb7256008c, []int{12, 0, 0} } type CreateRunRequest struct { @@ -133,7 +133,7 @@ func (m *CreateRunRequest) Reset() { *m = CreateRunRequest{} } func (m *CreateRunRequest) String() string { return proto.CompactTextString(m) } func (*CreateRunRequest) ProtoMessage() {} func (*CreateRunRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{0} + return fileDescriptor_run_49d294eb7256008c, []int{0} } func (m *CreateRunRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_CreateRunRequest.Unmarshal(m, b) @@ -171,7 +171,7 @@ func (m *GetRunRequest) Reset() { *m = GetRunRequest{} } func (m *GetRunRequest) String() string { return proto.CompactTextString(m) } func (*GetRunRequest) ProtoMessage() {} func (*GetRunRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{1} + return fileDescriptor_run_49d294eb7256008c, []int{1} } func (m *GetRunRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetRunRequest.Unmarshal(m, b) @@ -213,7 +213,7 @@ func (m *ListRunsRequest) Reset() { *m = ListRunsRequest{} } func (m *ListRunsRequest) String() string { return proto.CompactTextString(m) } func (*ListRunsRequest) ProtoMessage() {} func (*ListRunsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{2} + return fileDescriptor_run_49d294eb7256008c, []int{2} } func (m *ListRunsRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListRunsRequest.Unmarshal(m, b) @@ -270,6 +270,7 @@ func (m *ListRunsRequest) GetFilter() string { type ListRunsResponse struct { Runs []*Run `protobuf:"bytes,1,rep,name=runs,proto3" json:"runs,omitempty"` + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -280,7 +281,7 @@ func (m *ListRunsResponse) Reset() { *m = ListRunsResponse{} } func (m *ListRunsResponse) String() string { return proto.CompactTextString(m) } func (*ListRunsResponse) ProtoMessage() {} func (*ListRunsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{3} + return fileDescriptor_run_49d294eb7256008c, []int{3} } func (m *ListRunsResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListRunsResponse.Unmarshal(m, b) @@ -307,6 +308,13 @@ func (m *ListRunsResponse) GetRuns() []*Run { return nil } +func (m *ListRunsResponse) GetTotalSize() int32 { + if m != nil { + return m.TotalSize + } + return 0 +} + func (m *ListRunsResponse) GetNextPageToken() string { if m != nil { return m.NextPageToken @@ -325,7 +333,7 @@ func (m *ArchiveRunRequest) Reset() { *m = ArchiveRunRequest{} } func (m *ArchiveRunRequest) String() string { return proto.CompactTextString(m) } func (*ArchiveRunRequest) ProtoMessage() {} func (*ArchiveRunRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{4} + return fileDescriptor_run_49d294eb7256008c, []int{4} } func (m *ArchiveRunRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ArchiveRunRequest.Unmarshal(m, b) @@ -363,7 +371,7 @@ func (m *UnarchiveRunRequest) Reset() { *m = UnarchiveRunRequest{} } func (m *UnarchiveRunRequest) String() string { return proto.CompactTextString(m) } func (*UnarchiveRunRequest) ProtoMessage() {} func (*UnarchiveRunRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{5} + return fileDescriptor_run_49d294eb7256008c, []int{5} } func (m *UnarchiveRunRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_UnarchiveRunRequest.Unmarshal(m, b) @@ -401,7 +409,7 @@ func (m *DeleteRunRequest) Reset() { *m = DeleteRunRequest{} } func (m *DeleteRunRequest) String() string { return proto.CompactTextString(m) } func (*DeleteRunRequest) ProtoMessage() {} func (*DeleteRunRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{6} + return fileDescriptor_run_49d294eb7256008c, []int{6} } func (m *DeleteRunRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DeleteRunRequest.Unmarshal(m, b) @@ -449,7 +457,7 @@ func (m *Run) Reset() { *m = Run{} } func (m *Run) String() string { return proto.CompactTextString(m) } func (*Run) ProtoMessage() {} func (*Run) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{7} + return fileDescriptor_run_49d294eb7256008c, []int{7} } func (m *Run) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Run.Unmarshal(m, b) @@ -558,7 +566,7 @@ func (m *PipelineRuntime) Reset() { *m = PipelineRuntime{} } func (m *PipelineRuntime) String() string { return proto.CompactTextString(m) } func (*PipelineRuntime) ProtoMessage() {} func (*PipelineRuntime) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{8} + return fileDescriptor_run_49d294eb7256008c, []int{8} } func (m *PipelineRuntime) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PipelineRuntime.Unmarshal(m, b) @@ -604,7 +612,7 @@ func (m *RunDetail) Reset() { *m = RunDetail{} } func (m *RunDetail) String() string { return proto.CompactTextString(m) } func (*RunDetail) ProtoMessage() {} func (*RunDetail) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{9} + return fileDescriptor_run_49d294eb7256008c, []int{9} } func (m *RunDetail) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_RunDetail.Unmarshal(m, b) @@ -654,7 +662,7 @@ func (m *RunMetric) Reset() { *m = RunMetric{} } func (m *RunMetric) String() string { return proto.CompactTextString(m) } func (*RunMetric) ProtoMessage() {} func (*RunMetric) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{10} + return fileDescriptor_run_49d294eb7256008c, []int{10} } func (m *RunMetric) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_RunMetric.Unmarshal(m, b) @@ -781,7 +789,7 @@ func (m *ReportRunMetricsRequest) Reset() { *m = ReportRunMetricsRequest func (m *ReportRunMetricsRequest) String() string { return proto.CompactTextString(m) } func (*ReportRunMetricsRequest) ProtoMessage() {} func (*ReportRunMetricsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{11} + return fileDescriptor_run_49d294eb7256008c, []int{11} } func (m *ReportRunMetricsRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReportRunMetricsRequest.Unmarshal(m, b) @@ -826,7 +834,7 @@ func (m *ReportRunMetricsResponse) Reset() { *m = ReportRunMetricsRespon func (m *ReportRunMetricsResponse) String() string { return proto.CompactTextString(m) } func (*ReportRunMetricsResponse) ProtoMessage() {} func (*ReportRunMetricsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{12} + return fileDescriptor_run_49d294eb7256008c, []int{12} } func (m *ReportRunMetricsResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReportRunMetricsResponse.Unmarshal(m, b) @@ -871,7 +879,7 @@ func (m *ReportRunMetricsResponse_ReportRunMetricResult) String() string { } func (*ReportRunMetricsResponse_ReportRunMetricResult) ProtoMessage() {} func (*ReportRunMetricsResponse_ReportRunMetricResult) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{12, 0} + return fileDescriptor_run_49d294eb7256008c, []int{12, 0} } func (m *ReportRunMetricsResponse_ReportRunMetricResult) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReportRunMetricsResponse_ReportRunMetricResult.Unmarshal(m, b) @@ -932,7 +940,7 @@ func (m *ReadArtifactRequest) Reset() { *m = ReadArtifactRequest{} } func (m *ReadArtifactRequest) String() string { return proto.CompactTextString(m) } func (*ReadArtifactRequest) ProtoMessage() {} func (*ReadArtifactRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{13} + return fileDescriptor_run_49d294eb7256008c, []int{13} } func (m *ReadArtifactRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReadArtifactRequest.Unmarshal(m, b) @@ -984,7 +992,7 @@ func (m *ReadArtifactResponse) Reset() { *m = ReadArtifactResponse{} } func (m *ReadArtifactResponse) String() string { return proto.CompactTextString(m) } func (*ReadArtifactResponse) ProtoMessage() {} func (*ReadArtifactResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_run_87a629795b79c81e, []int{14} + return fileDescriptor_run_49d294eb7256008c, []int{14} } func (m *ReadArtifactResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReadArtifactResponse.Unmarshal(m, b) @@ -1336,98 +1344,99 @@ var _RunService_serviceDesc = grpc.ServiceDesc{ Metadata: "backend/api/run.proto", } -func init() { proto.RegisterFile("backend/api/run.proto", fileDescriptor_run_87a629795b79c81e) } - -var fileDescriptor_run_87a629795b79c81e = []byte{ - // 1432 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0xcd, 0x6e, 0xdb, 0x46, - 0x10, 0x36, 0x25, 0x5b, 0xb2, 0x46, 0xb2, 0x4d, 0xaf, 0xff, 0x18, 0xc5, 0x86, 0x1d, 0x3a, 0x4d, - 0x9d, 0xb4, 0x91, 0x10, 0xa7, 0x28, 0x50, 0x03, 0x45, 0x41, 0xdb, 0x8c, 0xa3, 0xc6, 0x96, 0xdd, - 0x95, 0xec, 0x16, 0xe9, 0x81, 0xa0, 0xc5, 0x95, 0xcd, 0x5a, 0x22, 0xd9, 0xdd, 0xa5, 0x53, 0x27, - 0xc8, 0xa5, 0x40, 0x5f, 0xa0, 0x3d, 0xf4, 0x56, 0xa0, 0xaf, 0xd0, 0x87, 0x28, 0xd0, 0x73, 0xaf, - 0x3d, 0xf6, 0x41, 0x0a, 0xee, 0x92, 0x0a, 0xf5, 0x63, 0x05, 0xe8, 0x49, 0xda, 0x99, 0x6f, 0x67, - 0x86, 0xf3, 0xcd, 0xcc, 0x0e, 0x2c, 0x9d, 0xdb, 0xad, 0x2b, 0xe2, 0x39, 0x55, 0x3b, 0x70, 0xab, - 0x34, 0xf4, 0x2a, 0x01, 0xf5, 0xb9, 0x8f, 0xb2, 0x76, 0xe0, 0x96, 0x57, 0xd2, 0x3a, 0x42, 0xa9, - 0x4f, 0xa5, 0xb6, 0x7c, 0xf7, 0xc2, 0xf7, 0x2f, 0x3a, 0xa4, 0x2a, 0x4e, 0xe7, 0x61, 0xbb, 0x4a, - 0xba, 0x01, 0xbf, 0x89, 0x95, 0xab, 0xb1, 0x32, 0xba, 0x64, 0x7b, 0x9e, 0xcf, 0x6d, 0xee, 0xfa, - 0x1e, 0x8b, 0xb5, 0xeb, 0x83, 0x57, 0xb9, 0xdb, 0x25, 0x8c, 0xdb, 0xdd, 0x20, 0x01, 0xa4, 0x9d, - 0x06, 0x6e, 0x40, 0x3a, 0xae, 0x47, 0x2c, 0x16, 0x90, 0x56, 0x0c, 0xb8, 0xdf, 0x17, 0x31, 0x61, - 0x7e, 0x48, 0x5b, 0xc4, 0xa2, 0xa4, 0x4d, 0x28, 0xf1, 0x5a, 0x24, 0x46, 0x7d, 0x2c, 0x7e, 0x5a, - 0x8f, 0x2f, 0x88, 0xf7, 0x98, 0xbd, 0xb2, 0x2f, 0x2e, 0x08, 0xad, 0xfa, 0x81, 0x88, 0x64, 0x38, - 0x2a, 0xbd, 0x02, 0xea, 0x1e, 0x25, 0x36, 0x27, 0x38, 0xf4, 0x30, 0xf9, 0x3e, 0x24, 0x8c, 0xa3, - 0x32, 0x64, 0x69, 0xe8, 0x69, 0xca, 0x86, 0xb2, 0x55, 0xdc, 0x9e, 0xae, 0xd8, 0x81, 0x5b, 0x89, - 0xb4, 0x91, 0x50, 0x7f, 0x00, 0x33, 0x07, 0x84, 0xa7, 0xc0, 0x4b, 0x90, 0xa3, 0xa1, 0x67, 0xb9, - 0x8e, 0xc0, 0x17, 0xf0, 0x14, 0x0d, 0xbd, 0x9a, 0xa3, 0xff, 0xa9, 0xc0, 0xdc, 0xa1, 0xcb, 0x22, - 0x24, 0x4b, 0xa0, 0x6b, 0x00, 0x81, 0x7d, 0x41, 0x2c, 0xee, 0x5f, 0x11, 0x2f, 0x86, 0x17, 0x22, - 0x49, 0x33, 0x12, 0xa0, 0xbb, 0x20, 0x0e, 0x16, 0x73, 0x5f, 0x13, 0x2d, 0xb3, 0xa1, 0x6c, 0x4d, - 0xe1, 0xe9, 0x48, 0xd0, 0x70, 0x5f, 0x13, 0xb4, 0x02, 0x79, 0xe6, 0x53, 0x6e, 0x9d, 0xdf, 0x68, - 0x59, 0x71, 0x31, 0x17, 0x1d, 0x77, 0x6f, 0xd0, 0x33, 0x58, 0x1e, 0x4e, 0x85, 0x75, 0x45, 0x6e, - 0xb4, 0x49, 0x11, 0xbf, 0x2a, 0xe3, 0x8f, 0x21, 0x2f, 0xc8, 0x0d, 0x5e, 0x4c, 0xf0, 0x38, 0x81, - 0xbf, 0x20, 0x37, 0x68, 0x19, 0x72, 0x6d, 0xb7, 0xc3, 0x09, 0xd5, 0xa6, 0xa4, 0x7d, 0x79, 0xd2, - 0xbf, 0x01, 0xf5, 0xdd, 0x77, 0xb0, 0xc0, 0xf7, 0x18, 0x41, 0xab, 0x30, 0x49, 0x43, 0x8f, 0x69, - 0xca, 0x46, 0xb6, 0x2f, 0x43, 0x42, 0x8a, 0x1e, 0xc0, 0x9c, 0x47, 0x7e, 0xe0, 0x56, 0xea, 0x5b, - 0x33, 0xc2, 0xe4, 0x4c, 0x24, 0x3e, 0x49, 0xbe, 0x57, 0xdf, 0x84, 0x79, 0x83, 0xb6, 0x2e, 0xdd, - 0xeb, 0x74, 0xee, 0x67, 0x21, 0xd3, 0x4b, 0x65, 0xc6, 0x75, 0xf4, 0x0f, 0x60, 0xe1, 0xd4, 0xb3, - 0xdf, 0x0b, 0xd3, 0x41, 0xdd, 0x27, 0x1d, 0xc2, 0xc7, 0x61, 0x7e, 0x9f, 0x84, 0x2c, 0x0e, 0xbd, - 0x41, 0x39, 0x42, 0x30, 0xe9, 0xd9, 0x5d, 0x12, 0x07, 0x29, 0xfe, 0xa3, 0x1d, 0x98, 0x61, 0xdc, - 0xa7, 0x82, 0x0e, 0x6e, 0x73, 0xa2, 0xc1, 0x86, 0xb2, 0x35, 0xbb, 0xbd, 0x94, 0x7c, 0x6a, 0xa5, - 0x21, 0xb5, 0x8d, 0x48, 0x89, 0x4b, 0x2c, 0x75, 0x42, 0x1b, 0x50, 0x74, 0x08, 0x6b, 0x51, 0x57, - 0x14, 0x5d, 0x4c, 0x57, 0x5a, 0x84, 0x3e, 0x85, 0x99, 0xbe, 0xfa, 0x8e, 0xa9, 0x9a, 0x17, 0xd6, - 0x4f, 0x62, 0x4d, 0x23, 0x20, 0x2d, 0x5c, 0x0a, 0x52, 0x27, 0x74, 0x00, 0x0b, 0xc3, 0x5c, 0x33, - 0x6d, 0x4a, 0xd0, 0xb0, 0xdc, 0x47, 0x74, 0x8f, 0x5b, 0x8c, 0x86, 0xe8, 0x66, 0xe8, 0x33, 0x80, - 0x96, 0xa8, 0x7a, 0xc7, 0xb2, 0xb9, 0x96, 0x13, 0xde, 0xcb, 0x15, 0xd9, 0xa0, 0x95, 0xa4, 0x41, - 0x2b, 0xcd, 0xa4, 0x41, 0x71, 0x21, 0x46, 0x1b, 0x1c, 0x7d, 0x0e, 0x25, 0xd6, 0xba, 0x24, 0x4e, - 0xd8, 0x91, 0x97, 0xf3, 0xef, 0xbd, 0x5c, 0xec, 0xe1, 0x0d, 0x1e, 0x95, 0x59, 0x94, 0xd0, 0x90, - 0x69, 0xd3, 0x71, 0x19, 0x8b, 0x13, 0x5a, 0x84, 0x29, 0x31, 0x67, 0xb4, 0x92, 0xec, 0x22, 0x71, - 0x40, 0x5b, 0x90, 0xef, 0x12, 0x4e, 0xdd, 0x16, 0xd3, 0x0a, 0xe2, 0x23, 0x67, 0x13, 0x02, 0x8e, - 0x84, 0x18, 0x27, 0x6a, 0xdd, 0x84, 0x52, 0x9a, 0x12, 0x54, 0x86, 0xe5, 0x46, 0xf3, 0x18, 0x1b, - 0x07, 0x66, 0xa3, 0x69, 0x34, 0x4d, 0xcb, 0x38, 0x33, 0x6a, 0x87, 0xc6, 0xee, 0xa1, 0xa9, 0x4e, - 0xa0, 0x3b, 0xb0, 0xd4, 0xaf, 0xc3, 0x7b, 0xcf, 0x6b, 0x67, 0xe6, 0xbe, 0xaa, 0xe8, 0x57, 0x30, - 0x97, 0xe4, 0x1f, 0x87, 0x5e, 0x34, 0xa1, 0xd0, 0x47, 0x30, 0xdf, 0x23, 0xab, 0x6b, 0x7b, 0x6e, - 0x9b, 0x30, 0x2e, 0xca, 0xa1, 0x80, 0xd5, 0x44, 0x71, 0x14, 0xcb, 0x23, 0xf0, 0x2b, 0x9f, 0x5e, - 0xb5, 0x3b, 0xfe, 0xab, 0x77, 0xe0, 0xa2, 0x04, 0x27, 0x8a, 0x04, 0xac, 0x5f, 0x42, 0x01, 0x87, - 0xde, 0x3e, 0xe1, 0xb6, 0xdb, 0x19, 0x37, 0x74, 0xd0, 0x17, 0xd0, 0xf3, 0x64, 0x51, 0x19, 0x96, - 0xa8, 0xd6, 0xe2, 0xf6, 0x62, 0x5f, 0xc9, 0xc4, 0x21, 0xe3, 0xb9, 0xa0, 0x5f, 0xa0, 0xff, 0xa5, - 0x08, 0x57, 0x32, 0x69, 0xbd, 0x82, 0x57, 0x52, 0x05, 0xbf, 0x02, 0x79, 0xcf, 0x77, 0x48, 0x34, - 0xc7, 0x64, 0x1f, 0xe4, 0xa2, 0x63, 0xcd, 0x41, 0x9b, 0x50, 0xf2, 0xc2, 0xee, 0x39, 0xa1, 0xd6, - 0xb5, 0xdd, 0x09, 0x89, 0x28, 0x67, 0xe5, 0xf9, 0x04, 0x2e, 0x4a, 0xe9, 0x59, 0x24, 0x44, 0x8f, - 0x21, 0xd7, 0xf6, 0x69, 0xd7, 0xe6, 0xa2, 0x92, 0x53, 0x7d, 0x22, 0x3d, 0x56, 0x9e, 0x09, 0x25, - 0x8e, 0x41, 0xfa, 0x36, 0xe4, 0xa4, 0x04, 0xcd, 0x41, 0xf1, 0xb4, 0xde, 0x38, 0x31, 0xf7, 0x6a, - 0xcf, 0x6a, 0xe6, 0xbe, 0x3a, 0x81, 0xf2, 0x90, 0xc5, 0xc6, 0xd7, 0xaa, 0x82, 0x66, 0x01, 0x4e, - 0x4c, 0xbc, 0x67, 0xd6, 0x9b, 0xc6, 0x81, 0xa9, 0x66, 0x76, 0xf3, 0x30, 0x25, 0x02, 0xd0, 0x5f, - 0xc2, 0x0a, 0x26, 0x81, 0x4f, 0x79, 0xcf, 0x3c, 0x1b, 0x3f, 0x8b, 0xd3, 0x55, 0x94, 0x19, 0x5f, - 0x45, 0xbf, 0x65, 0x41, 0x1b, 0x36, 0x1e, 0x4f, 0xbd, 0x23, 0xc8, 0x53, 0xc2, 0xc2, 0x0e, 0x4f, - 0x06, 0xdf, 0xd3, 0xb8, 0xe3, 0x46, 0xe3, 0x07, 0x15, 0x58, 0xdc, 0xc5, 0x89, 0x8d, 0xf2, 0x1f, - 0x19, 0x58, 0x1a, 0x09, 0x41, 0xeb, 0x50, 0x94, 0x01, 0x59, 0x29, 0x9a, 0x40, 0x8a, 0xea, 0x11, - 0x59, 0xf7, 0x61, 0x36, 0x01, 0xf4, 0x71, 0x56, 0x8a, 0x31, 0x92, 0x39, 0xdc, 0x6b, 0xb5, 0xac, - 0x20, 0x65, 0xe7, 0x7f, 0x84, 0x5b, 0x69, 0x08, 0x0b, 0xbd, 0x36, 0xd5, 0xa2, 0x54, 0x32, 0x66, - 0x5f, 0x10, 0xc1, 0x74, 0x01, 0x27, 0x47, 0xdd, 0x81, 0x9c, 0xc4, 0x0e, 0x73, 0x9a, 0x83, 0xcc, - 0xf1, 0x0b, 0x55, 0x41, 0x8b, 0xa0, 0xd6, 0xea, 0x67, 0xc6, 0x61, 0x6d, 0xdf, 0x32, 0xf0, 0xc1, - 0xe9, 0x91, 0x59, 0x6f, 0xaa, 0x19, 0xb4, 0x02, 0x0b, 0xfb, 0xa7, 0x27, 0x87, 0xb5, 0xbd, 0xa8, - 0x15, 0xb1, 0x79, 0x72, 0x8c, 0x9b, 0xb5, 0xfa, 0x81, 0x9a, 0x45, 0x08, 0x66, 0x6b, 0xf5, 0xa6, - 0x89, 0xeb, 0xc6, 0xa1, 0x65, 0x62, 0x7c, 0x8c, 0xd5, 0x49, 0xfd, 0x3b, 0x58, 0xc0, 0xc4, 0x76, - 0x0c, 0xca, 0xdd, 0xb6, 0xdd, 0xe2, 0xef, 0x21, 0x7e, 0x4c, 0x51, 0xcf, 0xd8, 0xb1, 0x09, 0x99, - 0x63, 0x39, 0xa4, 0x4b, 0x89, 0x30, 0xca, 0xb2, 0xfe, 0x08, 0x16, 0xfb, 0x7d, 0xc5, 0x75, 0x80, - 0x60, 0xd2, 0xb1, 0xb9, 0x2d, 0x5c, 0x95, 0xb0, 0xf8, 0xbf, 0xfd, 0x4f, 0x0e, 0x00, 0x87, 0x5e, - 0x83, 0xd0, 0x6b, 0xb7, 0x45, 0x50, 0x03, 0x0a, 0xbd, 0xad, 0x02, 0xc9, 0x66, 0x18, 0xdc, 0x32, - 0xca, 0xbd, 0x22, 0x94, 0x03, 0x40, 0x5f, 0xff, 0xf1, 0xef, 0x7f, 0x7f, 0xc9, 0xdc, 0xd1, 0x51, - 0xb4, 0xde, 0xb0, 0xea, 0xf5, 0x93, 0x73, 0xc2, 0xed, 0x27, 0xd1, 0x66, 0xc6, 0x76, 0xc4, 0x14, - 0xf8, 0x0a, 0x72, 0x72, 0xf5, 0x40, 0x48, 0x5c, 0xed, 0xdb, 0x43, 0x86, 0xcc, 0x6d, 0x0a, 0x73, - 0x6b, 0xe8, 0xee, 0xb0, 0xb9, 0xea, 0x1b, 0x99, 0xac, 0xb7, 0xa8, 0x01, 0xd3, 0xc9, 0xe3, 0x8e, - 0xe4, 0x28, 0x19, 0xd8, 0x59, 0xca, 0x4b, 0x03, 0x52, 0x99, 0x03, 0xbd, 0x2c, 0xac, 0x2f, 0xa2, - 0x11, 0xc1, 0x22, 0x02, 0xf0, 0xee, 0x5d, 0x47, 0xf2, 0x59, 0x1a, 0x7a, 0xe8, 0xcb, 0xcb, 0x43, - 0x2f, 0x86, 0x19, 0xad, 0x92, 0xfa, 0x87, 0xc2, 0xf2, 0x3d, 0x7d, 0x7d, 0x54, 0xdc, 0xae, 0xf3, - 0x76, 0x27, 0x5e, 0x06, 0xd0, 0x15, 0x94, 0xd2, 0x9b, 0x01, 0xd2, 0x84, 0xa3, 0x11, 0xcb, 0xc2, - 0xad, 0xae, 0x1e, 0x0a, 0x57, 0x9b, 0xfa, 0xbd, 0xdb, 0x5c, 0x85, 0x89, 0x31, 0xf4, 0x2d, 0x14, - 0x7a, 0xfb, 0x45, 0x4c, 0xe8, 0xe0, 0xbe, 0x71, 0xab, 0x9b, 0x98, 0xd8, 0x47, 0x2b, 0xb7, 0xb8, - 0x41, 0x3f, 0x29, 0xa0, 0x0e, 0xb6, 0x25, 0x5a, 0xbd, 0xa5, 0x5b, 0xa5, 0xaf, 0xb5, 0xb1, 0xbd, - 0xac, 0x7f, 0x22, 0x5c, 0x56, 0xf4, 0x87, 0x63, 0xc8, 0xdf, 0xa1, 0xe2, 0x76, 0x7c, 0x75, 0x47, - 0x79, 0x84, 0x7e, 0x55, 0xa0, 0x94, 0xae, 0xf8, 0x38, 0xa5, 0x23, 0x1a, 0xae, 0x7c, 0x67, 0x84, - 0x26, 0xf6, 0x8d, 0x85, 0xef, 0x43, 0xf4, 0xe5, 0x18, 0xdf, 0xd5, 0xa8, 0x0f, 0x59, 0xf5, 0x4d, - 0xdc, 0x9d, 0x6f, 0xab, 0x49, 0xe3, 0xb1, 0xea, 0x9b, 0xbe, 0xc6, 0x8c, 0xa2, 0xb4, 0x9d, 0xdd, - 0x93, 0x9f, 0x8d, 0x23, 0xbc, 0x0a, 0x79, 0x87, 0xb4, 0xed, 0x68, 0x42, 0xce, 0xa3, 0x39, 0x98, - 0x29, 0x17, 0x45, 0x10, 0x72, 0xea, 0xbc, 0x5c, 0x87, 0x35, 0xc8, 0xed, 0x12, 0x9b, 0x12, 0x8a, - 0x16, 0xa6, 0x33, 0xe5, 0x19, 0x3b, 0xe4, 0x97, 0x3e, 0x75, 0x5f, 0x8b, 0x75, 0x7f, 0x23, 0x73, - 0x5e, 0x02, 0xe8, 0x01, 0x26, 0xce, 0x73, 0x82, 0xa4, 0xa7, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, - 0x6a, 0x32, 0x7d, 0x10, 0x06, 0x0d, 0x00, 0x00, +func init() { proto.RegisterFile("backend/api/run.proto", fileDescriptor_run_49d294eb7256008c) } + +var fileDescriptor_run_49d294eb7256008c = []byte{ + // 1450 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x57, 0x5f, 0x4f, 0x23, 0x47, + 0x12, 0xc7, 0x36, 0xd8, 0xb8, 0x6c, 0x60, 0x68, 0xfe, 0xcd, 0x7a, 0x41, 0xb0, 0xc3, 0xde, 0x1e, + 0xbb, 0x77, 0x6b, 0x6b, 0xd9, 0xd3, 0x49, 0x87, 0x74, 0x3a, 0x0d, 0x30, 0xcb, 0xfa, 0x16, 0x0c, + 0x69, 0x1b, 0x22, 0x6d, 0x1e, 0x46, 0x8d, 0xa7, 0x0d, 0x13, 0xec, 0x99, 0x49, 0x77, 0x0f, 0x84, + 0x5d, 0xed, 0x4b, 0xa4, 0x7c, 0x81, 0xe4, 0x21, 0x6f, 0x91, 0xf2, 0x15, 0xf2, 0x21, 0x22, 0xe5, + 0x39, 0xaf, 0x79, 0xcc, 0x07, 0x89, 0xa6, 0x7b, 0xc6, 0x3b, 0xb6, 0xc1, 0x2b, 0xe5, 0x09, 0x77, + 0xd5, 0xaf, 0xab, 0xaa, 0xab, 0xea, 0x57, 0x53, 0xc0, 0xd2, 0x39, 0x69, 0x5f, 0x51, 0xcf, 0xa9, + 0x91, 0xc0, 0xad, 0xb1, 0xd0, 0xab, 0x06, 0xcc, 0x17, 0x3e, 0xca, 0x91, 0xc0, 0xad, 0xac, 0xa4, + 0x75, 0x94, 0x31, 0x9f, 0x29, 0x6d, 0xe5, 0xe1, 0x85, 0xef, 0x5f, 0x74, 0x69, 0x4d, 0x9e, 0xce, + 0xc3, 0x4e, 0x8d, 0xf6, 0x02, 0x71, 0x1b, 0x2b, 0x57, 0x63, 0x65, 0x74, 0x89, 0x78, 0x9e, 0x2f, + 0x88, 0x70, 0x7d, 0x8f, 0xc7, 0xda, 0xf5, 0xe1, 0xab, 0xc2, 0xed, 0x51, 0x2e, 0x48, 0x2f, 0x48, + 0x00, 0x69, 0xa7, 0x81, 0x1b, 0xd0, 0xae, 0xeb, 0x51, 0x9b, 0x07, 0xb4, 0x1d, 0x03, 0x1e, 0x0f, + 0x44, 0x4c, 0xb9, 0x1f, 0xb2, 0x36, 0xb5, 0x19, 0xed, 0x50, 0x46, 0xbd, 0x36, 0x8d, 0x51, 0xff, + 0x94, 0x7f, 0xda, 0xcf, 0x2f, 0xa8, 0xf7, 0x9c, 0xdf, 0x90, 0x8b, 0x0b, 0xca, 0x6a, 0x7e, 0x20, + 0x23, 0x19, 0x8d, 0xca, 0xa8, 0x82, 0xb6, 0xc7, 0x28, 0x11, 0x14, 0x87, 0x1e, 0xa6, 0x5f, 0x85, + 0x94, 0x0b, 0x54, 0x81, 0x1c, 0x0b, 0x3d, 0x3d, 0xb3, 0x91, 0xd9, 0x2a, 0x6d, 0x4f, 0x57, 0x49, + 0xe0, 0x56, 0x23, 0x6d, 0x24, 0x34, 0x9e, 0xc0, 0xcc, 0x01, 0x15, 0x29, 0xf0, 0x12, 0xe4, 0x59, + 0xe8, 0xd9, 0xae, 0x23, 0xf1, 0x45, 0x3c, 0xc5, 0x42, 0xaf, 0xee, 0x18, 0xbf, 0x64, 0x60, 0xee, + 0xd0, 0xe5, 0x11, 0x92, 0x27, 0xd0, 0x35, 0x80, 0x80, 0x5c, 0x50, 0x5b, 0xf8, 0x57, 0xd4, 0x8b, + 0xe1, 0xc5, 0x48, 0xd2, 0x8a, 0x04, 0xe8, 0x21, 0xc8, 0x83, 0xcd, 0xdd, 0x77, 0x54, 0xcf, 0x6e, + 0x64, 0xb6, 0xa6, 0xf0, 0x74, 0x24, 0x68, 0xba, 0xef, 0x28, 0x5a, 0x81, 0x02, 0xf7, 0x99, 0xb0, + 0xcf, 0x6f, 0xf5, 0x9c, 0xbc, 0x98, 0x8f, 0x8e, 0xbb, 0xb7, 0xe8, 0x15, 0x2c, 0x8f, 0xa6, 0xc2, + 0xbe, 0xa2, 0xb7, 0xfa, 0xa4, 0x8c, 0x5f, 0x53, 0xf1, 0xc7, 0x90, 0x37, 0xf4, 0x16, 0x2f, 0x26, + 0x78, 0x9c, 0xc0, 0xdf, 0xd0, 0x5b, 0xb4, 0x0c, 0xf9, 0x8e, 0xdb, 0x15, 0x94, 0xe9, 0x53, 0xca, + 0xbe, 0x3a, 0x19, 0x37, 0xa0, 0x7d, 0x7c, 0x07, 0x0f, 0x7c, 0x8f, 0x53, 0xb4, 0x0a, 0x93, 0x2c, + 0xf4, 0xb8, 0x9e, 0xd9, 0xc8, 0x0d, 0x64, 0x48, 0x4a, 0xa3, 0x67, 0x0a, 0x5f, 0x90, 0xae, 0x7a, + 0x48, 0x4e, 0x3e, 0xa4, 0x28, 0x25, 0xf2, 0x25, 0x4f, 0x60, 0xce, 0xa3, 0x5f, 0x0b, 0x3b, 0x95, + 0x8a, 0xac, 0xf4, 0x38, 0x13, 0x89, 0x4f, 0x92, 0x74, 0x18, 0x9b, 0x30, 0x6f, 0xb2, 0xf6, 0xa5, + 0x7b, 0x9d, 0x2e, 0xcd, 0x2c, 0x64, 0xfb, 0x99, 0xce, 0xba, 0x8e, 0xf1, 0x37, 0x58, 0x38, 0xf5, + 0xc8, 0x27, 0x61, 0x06, 0x68, 0xfb, 0xb4, 0x4b, 0xc5, 0x38, 0xcc, 0x4f, 0x93, 0x90, 0xc3, 0xa1, + 0x37, 0x2c, 0x47, 0x08, 0x26, 0x3d, 0xd2, 0xa3, 0x71, 0x90, 0xf2, 0x37, 0xda, 0x81, 0x19, 0x2e, + 0x7c, 0x26, 0xab, 0x25, 0x88, 0xa0, 0x3a, 0x6c, 0x64, 0xb6, 0x66, 0xb7, 0x97, 0x92, 0x4c, 0x54, + 0x9b, 0x4a, 0xdb, 0x8c, 0x94, 0xb8, 0xcc, 0x53, 0x27, 0xb4, 0x01, 0x25, 0x87, 0xf2, 0x36, 0x73, + 0x65, 0x4f, 0xc6, 0xd5, 0x4c, 0x8b, 0xd0, 0xbf, 0x61, 0x66, 0xa0, 0xfd, 0xe3, 0x4a, 0xce, 0x4b, + 0xeb, 0x27, 0xb1, 0xa6, 0x19, 0xd0, 0x36, 0x2e, 0x07, 0xa9, 0x13, 0x3a, 0x80, 0x85, 0xd1, 0x56, + 0xe0, 0xfa, 0x94, 0xac, 0xd2, 0xf2, 0x40, 0x1f, 0xf4, 0x4b, 0x8f, 0xd1, 0x48, 0x37, 0x70, 0xf4, + 0x1f, 0x80, 0xb6, 0x24, 0x85, 0x63, 0x13, 0xa1, 0xe7, 0xa5, 0xf7, 0x4a, 0x55, 0xf1, 0xb7, 0x9a, + 0xf0, 0xb7, 0xda, 0x4a, 0xf8, 0x8b, 0x8b, 0x31, 0xda, 0x14, 0xe8, 0xbf, 0x50, 0xe6, 0xed, 0x4b, + 0xea, 0x84, 0x5d, 0x75, 0xb9, 0xf0, 0xc9, 0xcb, 0xa5, 0x3e, 0xde, 0x14, 0x51, 0x17, 0x46, 0x09, + 0x0d, 0xb9, 0x3e, 0x1d, 0x77, 0xb9, 0x3c, 0xa1, 0x45, 0x98, 0x92, 0x63, 0x48, 0x2f, 0x2b, 0x92, + 0xc9, 0x03, 0xda, 0x82, 0x42, 0x8f, 0x0a, 0xe6, 0xb6, 0xb9, 0x5e, 0x94, 0x8f, 0x9c, 0x4d, 0x0a, + 0x70, 0x24, 0xc5, 0x38, 0x51, 0x1b, 0x16, 0x94, 0xd3, 0x25, 0x41, 0x15, 0x58, 0x6e, 0xb6, 0x8e, + 0xb1, 0x79, 0x60, 0x35, 0x5b, 0x66, 0xcb, 0xb2, 0xcd, 0x33, 0xb3, 0x7e, 0x68, 0xee, 0x1e, 0x5a, + 0xda, 0x04, 0x7a, 0x00, 0x4b, 0x83, 0x3a, 0xbc, 0xf7, 0xba, 0x7e, 0x66, 0xed, 0x6b, 0x19, 0xe3, + 0x0a, 0xe6, 0x92, 0xfc, 0xe3, 0xd0, 0x8b, 0x06, 0x18, 0xfa, 0x07, 0xcc, 0xf7, 0x8b, 0xd5, 0x23, + 0x9e, 0xdb, 0xa1, 0x5c, 0xc8, 0x76, 0x28, 0x62, 0x2d, 0x51, 0x1c, 0xc5, 0xf2, 0x08, 0x7c, 0xe3, + 0xb3, 0xab, 0x4e, 0xd7, 0xbf, 0xf9, 0x08, 0x2e, 0x29, 0x70, 0xa2, 0x48, 0xc0, 0xc6, 0x25, 0x14, + 0x71, 0xe8, 0xed, 0x53, 0x41, 0xdc, 0xee, 0xb8, 0x99, 0x84, 0xfe, 0x07, 0x7d, 0x4f, 0x36, 0x53, + 0x61, 0xc9, 0x6e, 0x2d, 0x6d, 0x2f, 0x0e, 0xb4, 0x4c, 0x1c, 0x32, 0x9e, 0x0b, 0x06, 0x05, 0xc6, + 0xaf, 0x19, 0xe9, 0x4a, 0x25, 0xad, 0xdf, 0xf0, 0x99, 0x54, 0xc3, 0xaf, 0x40, 0xc1, 0xf3, 0x1d, + 0x1a, 0x8d, 0x39, 0xc5, 0x83, 0x7c, 0x74, 0xac, 0x3b, 0x68, 0x13, 0xca, 0x5e, 0xd8, 0x3b, 0xa7, + 0xcc, 0xbe, 0x26, 0xdd, 0x50, 0xd1, 0x3d, 0xf3, 0x7a, 0x02, 0x97, 0x94, 0xf4, 0x2c, 0x12, 0xa2, + 0xe7, 0x90, 0xef, 0xf8, 0xac, 0x47, 0x84, 0xec, 0xe4, 0x14, 0x4f, 0x94, 0xc7, 0xea, 0x2b, 0xa9, + 0xc4, 0x31, 0xc8, 0xd8, 0x86, 0xbc, 0x92, 0xa0, 0x39, 0x28, 0x9d, 0x36, 0x9a, 0x27, 0xd6, 0x5e, + 0xfd, 0x55, 0xdd, 0xda, 0xd7, 0x26, 0x50, 0x01, 0x72, 0xd8, 0xfc, 0x5c, 0xcb, 0xa0, 0x59, 0x80, + 0x13, 0x0b, 0xef, 0x59, 0x8d, 0x96, 0x79, 0x60, 0x69, 0xd9, 0xdd, 0x02, 0x4c, 0xc9, 0x00, 0x8c, + 0xb7, 0xb0, 0x82, 0x69, 0xe0, 0x33, 0xd1, 0x37, 0xcf, 0xc7, 0x8f, 0xea, 0x74, 0x17, 0x65, 0xc7, + 0x77, 0xd1, 0x8f, 0x39, 0xd0, 0x47, 0x8d, 0xc7, 0x43, 0xf1, 0x08, 0x0a, 0x8c, 0xf2, 0xb0, 0x2b, + 0x92, 0xb9, 0xf8, 0x32, 0x66, 0xdc, 0xdd, 0xf8, 0x61, 0x05, 0x96, 0x77, 0x71, 0x62, 0xa3, 0xf2, + 0x73, 0x16, 0x96, 0xee, 0x84, 0xa0, 0x75, 0x28, 0xa9, 0x80, 0xec, 0x54, 0x99, 0x40, 0x89, 0x1a, + 0x51, 0xb1, 0x1e, 0xc3, 0x6c, 0x02, 0x18, 0xa8, 0x59, 0x39, 0xc6, 0xa8, 0xca, 0xe1, 0x3e, 0xd5, + 0x72, 0xb2, 0x28, 0x3b, 0x7f, 0x21, 0xdc, 0x6a, 0x53, 0x5a, 0xe8, 0xd3, 0x54, 0x8f, 0x52, 0xc9, + 0x39, 0xb9, 0xa0, 0xb2, 0xd2, 0x45, 0x9c, 0x1c, 0x0d, 0x07, 0xf2, 0x0a, 0x3b, 0x5a, 0xd3, 0x3c, + 0x64, 0x8f, 0xdf, 0x68, 0x19, 0xb4, 0x08, 0x5a, 0xbd, 0x71, 0x66, 0x1e, 0xd6, 0xf7, 0x6d, 0x13, + 0x1f, 0x9c, 0x1e, 0x59, 0x8d, 0x96, 0x96, 0x45, 0x2b, 0xb0, 0xb0, 0x7f, 0x7a, 0x72, 0x58, 0xdf, + 0x8b, 0xa8, 0x88, 0xad, 0x93, 0x63, 0xdc, 0xaa, 0x37, 0x0e, 0xb4, 0x1c, 0x42, 0x30, 0x5b, 0x6f, + 0xb4, 0x2c, 0xdc, 0x30, 0x0f, 0x6d, 0x0b, 0xe3, 0x63, 0xac, 0x4d, 0x1a, 0x5f, 0xc2, 0x02, 0xa6, + 0xc4, 0x31, 0x99, 0x70, 0x3b, 0xa4, 0x2d, 0x3e, 0x51, 0xf8, 0x31, 0x4d, 0x3d, 0x43, 0x62, 0x13, + 0x2a, 0xc7, 0x6a, 0x48, 0x97, 0x13, 0x61, 0x94, 0x65, 0xe3, 0x19, 0x2c, 0x0e, 0xfa, 0x8a, 0xfb, + 0x00, 0xc1, 0xa4, 0x43, 0x04, 0x91, 0xae, 0xca, 0x58, 0xfe, 0xde, 0xfe, 0x3d, 0x0f, 0x80, 0x43, + 0xaf, 0x49, 0xd9, 0xb5, 0xdb, 0xa6, 0xa8, 0x09, 0xc5, 0xfe, 0xd2, 0x81, 0x14, 0x19, 0x86, 0x97, + 0x90, 0x4a, 0xbf, 0x09, 0xd5, 0x00, 0x30, 0xd6, 0xbf, 0xf9, 0xed, 0x8f, 0xef, 0xb3, 0x0f, 0x0c, + 0x14, 0x6d, 0x3f, 0xbc, 0x76, 0xfd, 0xe2, 0x9c, 0x0a, 0xf2, 0x22, 0x5a, 0xdc, 0xf8, 0x8e, 0x9c, + 0x02, 0x9f, 0x41, 0x5e, 0x6d, 0x26, 0x08, 0xc9, 0xab, 0x03, 0x6b, 0xca, 0x88, 0xb9, 0x4d, 0x69, + 0x6e, 0x0d, 0x3d, 0x1c, 0x35, 0x57, 0x7b, 0xaf, 0x92, 0xf5, 0x01, 0x35, 0x61, 0x3a, 0xf9, 0xf6, + 0x23, 0x35, 0x4a, 0x86, 0x56, 0x9a, 0xca, 0xd2, 0x90, 0x54, 0xe5, 0xc0, 0xa8, 0x48, 0xeb, 0x8b, + 0xe8, 0x8e, 0x60, 0x11, 0x05, 0xf8, 0xf8, 0x5d, 0x47, 0xea, 0xb3, 0x34, 0xf2, 0xa1, 0xaf, 0x2c, + 0x8f, 0x7c, 0x31, 0xac, 0x68, 0xd3, 0x34, 0xfe, 0x2e, 0x2d, 0x3f, 0x32, 0xd6, 0xef, 0x8a, 0xdb, + 0x75, 0x3e, 0xec, 0xc4, 0xcb, 0x00, 0xba, 0x82, 0x72, 0x7a, 0x33, 0x40, 0xba, 0x74, 0x74, 0xc7, + 0xb2, 0x70, 0xaf, 0xab, 0xa7, 0xd2, 0xd5, 0xa6, 0xf1, 0xe8, 0x3e, 0x57, 0x61, 0x62, 0x0c, 0x7d, + 0x01, 0xc5, 0xfe, 0x7e, 0x11, 0x17, 0x74, 0x78, 0xdf, 0xb8, 0xd7, 0x4d, 0x5c, 0xd8, 0x67, 0x2b, + 0xf7, 0xb8, 0x41, 0xdf, 0x66, 0x40, 0x1b, 0xa6, 0x25, 0x5a, 0xbd, 0x87, 0xad, 0xca, 0xd7, 0xda, + 0x58, 0x2e, 0x1b, 0xff, 0x92, 0x2e, 0xab, 0xc6, 0xd3, 0x31, 0xc5, 0xdf, 0x61, 0xf2, 0x76, 0x7c, + 0x75, 0x27, 0xf3, 0x0c, 0xfd, 0x90, 0x81, 0x72, 0xba, 0xe3, 0xe3, 0x94, 0xde, 0x41, 0xb8, 0xca, + 0x83, 0x3b, 0x34, 0xb1, 0x6f, 0x2c, 0x7d, 0x1f, 0xa2, 0xff, 0x8f, 0xf1, 0x5d, 0x8b, 0x78, 0xc8, + 0x6b, 0xef, 0x63, 0x76, 0x7e, 0xa8, 0x25, 0xc4, 0xe3, 0xb5, 0xf7, 0x03, 0xc4, 0x8c, 0xa2, 0x24, + 0xce, 0xee, 0xc9, 0x77, 0xe6, 0x11, 0x5e, 0x85, 0x82, 0x43, 0x3b, 0x24, 0x9a, 0x90, 0xf3, 0x68, + 0x0e, 0x66, 0x2a, 0x25, 0x19, 0x84, 0x9a, 0x3a, 0x6f, 0xd7, 0x61, 0x0d, 0xf2, 0xbb, 0x94, 0x30, + 0xca, 0xd0, 0xc2, 0x74, 0xb6, 0x32, 0x43, 0x42, 0x71, 0xe9, 0x33, 0xf7, 0x9d, 0xfc, 0x6f, 0x60, + 0x23, 0x7b, 0x5e, 0x06, 0xe8, 0x03, 0x26, 0xce, 0xf3, 0xb2, 0x48, 0x2f, 0xff, 0x0c, 0x00, 0x00, + 0xff, 0xff, 0x18, 0xf0, 0x15, 0x1d, 0x25, 0x0d, 0x00, 0x00, } diff --git a/backend/api/go_http_client/experiment_model/api_list_experiments_response.go b/backend/api/go_http_client/experiment_model/api_list_experiments_response.go index 5fb67f55839..3cd9f28e0df 100644 --- a/backend/api/go_http_client/experiment_model/api_list_experiments_response.go +++ b/backend/api/go_http_client/experiment_model/api_list_experiments_response.go @@ -37,6 +37,9 @@ type APIListExperimentsResponse struct { // next page token NextPageToken string `json:"next_page_token,omitempty"` + + // total size + TotalSize int32 `json:"total_size,omitempty"` } // Validate validates this api list experiments response diff --git a/backend/api/go_http_client/job_model/api_list_jobs_response.go b/backend/api/go_http_client/job_model/api_list_jobs_response.go index 6378c085e1b..402bcc7d11d 100644 --- a/backend/api/go_http_client/job_model/api_list_jobs_response.go +++ b/backend/api/go_http_client/job_model/api_list_jobs_response.go @@ -37,6 +37,9 @@ type APIListJobsResponse struct { // next page token NextPageToken string `json:"next_page_token,omitempty"` + + // total size + TotalSize int32 `json:"total_size,omitempty"` } // Validate validates this api list jobs response diff --git a/backend/api/go_http_client/pipeline_model/api_list_pipelines_response.go b/backend/api/go_http_client/pipeline_model/api_list_pipelines_response.go index b879d56deb0..71060362794 100644 --- a/backend/api/go_http_client/pipeline_model/api_list_pipelines_response.go +++ b/backend/api/go_http_client/pipeline_model/api_list_pipelines_response.go @@ -37,6 +37,9 @@ type APIListPipelinesResponse struct { // pipelines Pipelines []*APIPipeline `json:"pipelines"` + + // total size + TotalSize int32 `json:"total_size,omitempty"` } // Validate validates this api list pipelines response diff --git a/backend/api/go_http_client/run_model/api_list_runs_response.go b/backend/api/go_http_client/run_model/api_list_runs_response.go index 1f47291a071..d0d53e10f7d 100644 --- a/backend/api/go_http_client/run_model/api_list_runs_response.go +++ b/backend/api/go_http_client/run_model/api_list_runs_response.go @@ -37,6 +37,9 @@ type APIListRunsResponse struct { // runs Runs []*APIRun `json:"runs"` + + // total size + TotalSize int32 `json:"total_size,omitempty"` } // Validate validates this api list runs response diff --git a/backend/api/job.proto b/backend/api/job.proto index 9009009c94d..4a7e771562f 100644 --- a/backend/api/job.proto +++ b/backend/api/job.proto @@ -126,6 +126,7 @@ message ListJobsRequest { message ListJobsResponse { repeated Job jobs = 1; + int32 total_size = 3; string next_page_token = 2; } diff --git a/backend/api/pipeline.proto b/backend/api/pipeline.proto index b19af2acba4..83e693077b2 100644 --- a/backend/api/pipeline.proto +++ b/backend/api/pipeline.proto @@ -119,6 +119,7 @@ message ListPipelinesRequest { message ListPipelinesResponse { repeated Pipeline pipelines = 1; + int32 total_size = 3; string next_page_token = 2; } diff --git a/backend/api/run.proto b/backend/api/run.proto index bd66d80e548..3cabd3b6c3e 100644 --- a/backend/api/run.proto +++ b/backend/api/run.proto @@ -141,6 +141,7 @@ message ListRunsRequest { message ListRunsResponse { repeated Run runs = 1; + int32 total_size = 3; string next_page_token = 2; } diff --git a/backend/api/swagger/experiment.swagger.json b/backend/api/swagger/experiment.swagger.json index 3b8cc2924eb..93bc180ac6e 100644 --- a/backend/api/swagger/experiment.swagger.json +++ b/backend/api/swagger/experiment.swagger.json @@ -187,6 +187,10 @@ "$ref": "#/definitions/apiExperiment" } }, + "total_size": { + "type": "integer", + "format": "int32" + }, "next_page_token": { "type": "string" } diff --git a/backend/api/swagger/job.swagger.json b/backend/api/swagger/job.swagger.json index b5653c15ade..9fd1fe1bdab 100644 --- a/backend/api/swagger/job.swagger.json +++ b/backend/api/swagger/job.swagger.json @@ -333,6 +333,10 @@ "$ref": "#/definitions/apiJob" } }, + "total_size": { + "type": "integer", + "format": "int32" + }, "next_page_token": { "type": "string" } diff --git a/backend/api/swagger/pipeline.swagger.json b/backend/api/swagger/pipeline.swagger.json index 2d0a548bd9b..426732cf806 100644 --- a/backend/api/swagger/pipeline.swagger.json +++ b/backend/api/swagger/pipeline.swagger.json @@ -203,6 +203,10 @@ "$ref": "#/definitions/apiPipeline" } }, + "total_size": { + "type": "integer", + "format": "int32" + }, "next_page_token": { "type": "string" } diff --git a/backend/api/swagger/run.swagger.json b/backend/api/swagger/run.swagger.json index 28c5f745fd6..a8980f350ca 100644 --- a/backend/api/swagger/run.swagger.json +++ b/backend/api/swagger/run.swagger.json @@ -383,6 +383,10 @@ "$ref": "#/definitions/apiRun" } }, + "total_size": { + "type": "integer", + "format": "int32" + }, "next_page_token": { "type": "string" } diff --git a/backend/src/apiserver/list/BUILD.bazel b/backend/src/apiserver/list/BUILD.bazel index bdd4834d20e..3a26548af88 100644 --- a/backend/src/apiserver/list/BUILD.bazel +++ b/backend/src/apiserver/list/BUILD.bazel @@ -7,6 +7,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//backend/api:go_default_library", + "//backend/src/apiserver/common:go_default_library", "//backend/src/apiserver/filter:go_default_library", "//backend/src/common/util:go_default_library", "@com_github_masterminds_squirrel//:go_default_library", @@ -19,10 +20,12 @@ go_test( embed = [":go_default_library"], deps = [ "//backend/api:go_default_library", + "//backend/src/apiserver/common:go_default_library", "//backend/src/apiserver/filter:go_default_library", "//backend/src/common/util:go_default_library", "@com_github_google_go_cmp//cmp:go_default_library", "@com_github_google_go_cmp//cmp/cmpopts:go_default_library", "@com_github_masterminds_squirrel//:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", ], ) diff --git a/backend/src/apiserver/list/list.go b/backend/src/apiserver/list/list.go index 0ec11d02ae8..8a0578a9cb8 100644 --- a/backend/src/apiserver/list/list.go +++ b/backend/src/apiserver/list/list.go @@ -18,6 +18,7 @@ package list import ( + "database/sql" "encoding/base64" "encoding/json" "fmt" @@ -26,6 +27,7 @@ import ( sq "github.com/Masterminds/squirrel" api "github.com/kubeflow/pipelines/backend/api/go_client" + "github.com/kubeflow/pipelines/backend/src/apiserver/common" "github.com/kubeflow/pipelines/backend/src/apiserver/filter" "github.com/kubeflow/pipelines/backend/src/common/util" ) @@ -158,10 +160,10 @@ func NewOptions(listable Listable, pageSize int, sortBy string, filterProto *api return &Options{PageSize: pageSize, token: token}, nil } -// AddToSelect adds WHERE clauses with the sorting and filtering criteria in the +// AddPaginationToSelect adds WHERE clauses with the sorting and pagination criteria in the // Options o to the supplied SelectBuilder, and returns the new SelectBuilder // containing these. -func (o *Options) AddToSelect(sqlBuilder sq.SelectBuilder) sq.SelectBuilder { +func (o *Options) AddPaginationToSelect(sqlBuilder sq.SelectBuilder) sq.SelectBuilder { // If next row's value is specified, set those values in the clause. if o.SortByFieldValue != nil && o.KeyFieldValue != nil { if o.IsDesc { @@ -186,6 +188,13 @@ func (o *Options) AddToSelect(sqlBuilder sq.SelectBuilder) sq.SelectBuilder { // Add one more item than what is requested. sqlBuilder = sqlBuilder.Limit(uint64(o.PageSize + 1)) + return sqlBuilder +} + +// AddFilterToSelect adds WHERE clauses with the filtering criteria in the +// Options o to the supplied SelectBuilder, and returns the new SelectBuilder +// containing these. +func (o *Options) AddFilterToSelect(sqlBuilder sq.SelectBuilder) sq.SelectBuilder { if o.Filter != nil { sqlBuilder = o.Filter.AddToSelect(sqlBuilder) } @@ -193,6 +202,42 @@ func (o *Options) AddToSelect(sqlBuilder sq.SelectBuilder) sq.SelectBuilder { return sqlBuilder } +// FilterOnResourceReference filters the given resource's table by rows from the ResourceReferences +// table that match an optional given filter, and returns the rebuilt SelectBuilder +func FilterOnResourceReference(tableName string, resourceType common.ResourceType, selectCount bool, + filterContext *common.FilterContext) (sq.SelectBuilder, error) { + selectBuilder := sq.Select("*") + if selectCount { + selectBuilder = sq.Select("count(*)") + } + selectBuilder = selectBuilder.From(tableName) + if filterContext.ReferenceKey != nil { + resourceReferenceFilter, args, err := sq.Select("ResourceUUID"). + From("resource_references as rf"). + Where(sq.And{ + sq.Eq{"rf.ResourceType": resourceType}, + sq.Eq{"rf.ReferenceUUID": filterContext.ID}, + sq.Eq{"rf.ReferenceType": filterContext.Type}}).ToSql() + if err != nil { + return selectBuilder, util.NewInternalServerError( + err, "Failed to create subquery to filter by resource reference: %v", err.Error()) + } + return selectBuilder.Where(fmt.Sprintf("UUID in (%s)", resourceReferenceFilter), args...), nil + } + return selectBuilder, nil +} + +// Scans the one given row into a number, and returns the number +func ScanRowToTotalSize(rows *sql.Rows) (int, error) { + var total_size int + rows.Next() + err := rows.Scan(&total_size) + if err != nil { + return 0, util.NewInternalServerError(err, "Failed to scan row total_size") + } + return total_size, nil +} + // Listable is an interface that should be implemented by any resource/model // that wants to support listing queries. type Listable interface { diff --git a/backend/src/apiserver/list/list_test.go b/backend/src/apiserver/list/list_test.go index 9d15274e099..11d370f25db 100644 --- a/backend/src/apiserver/list/list_test.go +++ b/backend/src/apiserver/list/list_test.go @@ -4,8 +4,10 @@ import ( "reflect" "testing" + "github.com/kubeflow/pipelines/backend/src/apiserver/common" "github.com/kubeflow/pipelines/backend/src/apiserver/filter" "github.com/kubeflow/pipelines/backend/src/common/util" + "github.com/stretchr/testify/assert" sq "github.com/Masterminds/squirrel" "github.com/google/go-cmp/cmp" @@ -365,7 +367,7 @@ func TestNewOptions_InvalidFilter(t *testing.T) { } } -func TestAddToSelect(t *testing.T) { +func TestAddPaginationAndFilterToSelect(t *testing.T) { protoFilter := &api.Filter{ Predicates: []*api.Predicate{ &api.Predicate{ @@ -472,7 +474,7 @@ func TestAddToSelect(t *testing.T) { for _, test := range tests { sql := sq.Select("*").From("MyTable") - gotSQL, gotArgs, err := test.in.AddToSelect(sql).ToSql() + gotSQL, gotArgs, err := test.in.AddFilterToSelect(test.in.AddPaginationToSelect(sql)).ToSql() if gotSQL != test.wantSQL || !reflect.DeepEqual(gotArgs, test.wantArgs) || err != nil { t.Errorf("BuildListSQLQuery(%+v) =\nGot: %q, %v, %v\nWant: %q, %v, nil", @@ -605,3 +607,70 @@ func TestMatches(t *testing.T) { } } } + +func TestFilterOnResourceReference(t *testing.T) { + + type testIn struct { + table string + resourceType common.ResourceType + count bool + filter *common.FilterContext + } + tests := []struct { + in *testIn + wantSql string + wantErr error + }{ + { + in: &testIn{ + table: "testTable", + resourceType: common.Run, + count: false, + filter: &common.FilterContext{}, + }, + wantSql: "SELECT * FROM testTable", + wantErr: nil, + }, + { + in: &testIn{ + table: "testTable", + resourceType: common.Run, + count: true, + filter: &common.FilterContext{}, + }, + wantSql: "SELECT count(*) FROM testTable", + wantErr: nil, + }, + { + in: &testIn{ + table: "testTable", + resourceType: common.Run, + count: false, + filter: &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Run}}, + }, + wantSql: "SELECT * FROM testTable WHERE UUID in (SELECT ResourceUUID FROM resource_references as rf WHERE (rf.ResourceType = ? AND rf.ReferenceUUID = ? AND rf.ReferenceType = ?))", + wantErr: nil, + }, + { + in: &testIn{ + table: "testTable", + resourceType: common.Run, + count: true, + filter: &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Run}}, + }, + wantSql: "SELECT count(*) FROM testTable WHERE UUID in (SELECT ResourceUUID FROM resource_references as rf WHERE (rf.ResourceType = ? AND rf.ReferenceUUID = ? AND rf.ReferenceType = ?))", + wantErr: nil, + }, + } + + for _, test := range tests { + sqlBuilder, gotErr := FilterOnResourceReference(test.in.table, test.in.resourceType, test.in.count, test.in.filter) + gotSql, _, err := sqlBuilder.ToSql() + assert.Nil(t, err) + + if gotSql != test.wantSql || gotErr != test.wantErr { + t.Errorf("FilterOnResourceReference(%+v) =\nGot: %q, %v\nWant: %q, %v", + test.in, gotSql, gotErr, test.wantSql, test.wantErr) + } + } +} diff --git a/backend/src/apiserver/resource/resource_manager.go b/backend/src/apiserver/resource/resource_manager.go index f4302cbc9ef..c171607d8bc 100644 --- a/backend/src/apiserver/resource/resource_manager.go +++ b/backend/src/apiserver/resource/resource_manager.go @@ -92,7 +92,7 @@ func (r *ResourceManager) GetExperiment(experimentId string) (*model.Experiment, } func (r *ResourceManager) ListExperiments(opts *list.Options) ( - experiments []*model.Experiment, nextPageToken string, err error) { + experiments []*model.Experiment, total_size int, nextPageToken string, err error) { return r.experimentStore.ListExperiments(opts) } @@ -105,7 +105,7 @@ func (r *ResourceManager) DeleteExperiment(experimentID string) error { } func (r *ResourceManager) ListPipelines(opts *list.Options) ( - pipelines []*model.Pipeline, nextPageToken string, err error) { + pipelines []*model.Pipeline, total_size int, nextPageToken string, err error) { return r.pipelineStore.ListPipelines(opts) } @@ -228,7 +228,8 @@ func (r *ResourceManager) GetRun(runId string) (*model.RunDetail, error) { return r.runStore.GetRun(runId) } -func (r *ResourceManager) ListRuns(filterContext *common.FilterContext, opts *list.Options) (runs []*model.Run, nextPageToken string, err error) { +func (r *ResourceManager) ListRuns(filterContext *common.FilterContext, + opts *list.Options) (runs []*model.Run, total_size int, nextPageToken string, err error) { return r.runStore.ListRuns(filterContext, opts) } @@ -258,7 +259,8 @@ func (r *ResourceManager) DeleteRun(runID string) error { return nil } -func (r *ResourceManager) ListJobs(filterContext *common.FilterContext, opts *list.Options) (jobs []*model.Job, nextPageToken string, err error) { +func (r *ResourceManager) ListJobs(filterContext *common.FilterContext, + opts *list.Options) (jobs []*model.Job, total_size int, nextPageToken string, err error) { return r.jobStore.ListJobs(filterContext, opts) } diff --git a/backend/src/apiserver/server/experiment_server.go b/backend/src/apiserver/server/experiment_server.go index 6b742de1523..61ac9bfd49a 100644 --- a/backend/src/apiserver/server/experiment_server.go +++ b/backend/src/apiserver/server/experiment_server.go @@ -44,12 +44,13 @@ func (s *ExperimentServer) ListExperiment(ctx context.Context, request *api.List return nil, util.Wrap(err, "Failed to create list options") } - experiments, nextPageToken, err := s.resourceManager.ListExperiments(opts) + experiments, total_size, nextPageToken, err := s.resourceManager.ListExperiments(opts) if err != nil { return nil, util.Wrap(err, "List experiments failed.") } return &api.ListExperimentsResponse{ Experiments: ToApiExperiments(experiments), + TotalSize: int32(total_size), NextPageToken: nextPageToken}, nil } diff --git a/backend/src/apiserver/server/job_server.go b/backend/src/apiserver/server/job_server.go index 95bd2b6fb08..9cd5331c143 100644 --- a/backend/src/apiserver/server/job_server.go +++ b/backend/src/apiserver/server/job_server.go @@ -60,11 +60,11 @@ func (s *JobServer) ListJobs(ctx context.Context, request *api.ListJobsRequest) if err != nil { return nil, util.Wrap(err, "Validating filter failed.") } - jobs, nextPageToken, err := s.resourceManager.ListJobs(filterContext, opts) + jobs, total_size, nextPageToken, err := s.resourceManager.ListJobs(filterContext, opts) if err != nil { return nil, util.Wrap(err, "Failed to list jobs.") } - return &api.ListJobsResponse{Jobs: ToApiJobs(jobs), NextPageToken: nextPageToken}, nil + return &api.ListJobsResponse{Jobs: ToApiJobs(jobs), TotalSize: int32(total_size), NextPageToken: nextPageToken}, nil } func (s *JobServer) EnableJob(ctx context.Context, request *api.EnableJobRequest) (*empty.Empty, error) { diff --git a/backend/src/apiserver/server/pipeline_server.go b/backend/src/apiserver/server/pipeline_server.go index 0c95c801026..8c3641440e6 100644 --- a/backend/src/apiserver/server/pipeline_server.go +++ b/backend/src/apiserver/server/pipeline_server.go @@ -77,12 +77,12 @@ func (s *PipelineServer) ListPipelines(ctx context.Context, request *api.ListPip return nil, util.Wrap(err, "Failed to create list options") } - pipelines, nextPageToken, err := s.resourceManager.ListPipelines(opts) + pipelines, total_size, nextPageToken, err := s.resourceManager.ListPipelines(opts) if err != nil { return nil, util.Wrap(err, "List pipelines failed.") } apiPipelines := ToApiPipelines(pipelines) - return &api.ListPipelinesResponse{Pipelines: apiPipelines, NextPageToken: nextPageToken}, nil + return &api.ListPipelinesResponse{Pipelines: apiPipelines, TotalSize: int32(total_size), NextPageToken: nextPageToken}, nil } func (s *PipelineServer) DeletePipeline(ctx context.Context, request *api.DeletePipelineRequest) (*empty.Empty, error) { diff --git a/backend/src/apiserver/server/pipeline_upload_server_test.go b/backend/src/apiserver/server/pipeline_upload_server_test.go index 07797b99fd2..dfd9a531e56 100644 --- a/backend/src/apiserver/server/pipeline_upload_server_test.go +++ b/backend/src/apiserver/server/pipeline_upload_server_test.go @@ -70,9 +70,10 @@ func TestUploadPipeline_YAML(t *testing.T) { Name: "hello-world.yaml", Parameters: "[]", Status: model.PipelineReady}} - pkg, str, err := clientManager.PipelineStore().ListPipelines(opts) + pkg, total_size, str, err := clientManager.PipelineStore().ListPipelines(opts) assert.Nil(t, err) assert.Equal(t, str, "") + assert.Equal(t, 1, total_size) assert.Equal(t, pkgsExpect, pkg) } @@ -111,9 +112,10 @@ func TestUploadPipeline_Tarball(t *testing.T) { Name: "arguments.tar.gz", Parameters: "[{\"name\":\"param1\",\"value\":\"hello\"},{\"name\":\"param2\"}]", Status: model.PipelineReady}} - pkg, str, err := clientManager.PipelineStore().ListPipelines(opts) + pkg, total_size, str, err := clientManager.PipelineStore().ListPipelines(opts) assert.Nil(t, err) assert.Equal(t, str, "") + assert.Equal(t, 1, total_size) assert.Equal(t, pkgsExpect, pkg) } @@ -169,8 +171,9 @@ func TestUploadPipeline_SpecifyFileName(t *testing.T) { Name: "foo bar", Parameters: "[]", Status: model.PipelineReady}} - pkg, str, err := clientManager.PipelineStore().ListPipelines(opts) + pkg, total_size, str, err := clientManager.PipelineStore().ListPipelines(opts) assert.Nil(t, err) + assert.Equal(t, 1, total_size) assert.Equal(t, str, "") assert.Equal(t, pkgsExpect, pkg) } diff --git a/backend/src/apiserver/server/run_server.go b/backend/src/apiserver/server/run_server.go index 23d9a85f130..567813f923c 100644 --- a/backend/src/apiserver/server/run_server.go +++ b/backend/src/apiserver/server/run_server.go @@ -59,11 +59,11 @@ func (s *RunServer) ListRuns(ctx context.Context, request *api.ListRunsRequest) if err != nil { return nil, util.Wrap(err, "Validating filter failed.") } - runs, nextPageToken, err := s.resourceManager.ListRuns(filterContext, opts) + runs, total_size, nextPageToken, err := s.resourceManager.ListRuns(filterContext, opts) if err != nil { return nil, util.Wrap(err, "Failed to list runs.") } - return &api.ListRunsResponse{Runs: ToApiRuns(runs), NextPageToken: nextPageToken}, nil + return &api.ListRunsResponse{Runs: ToApiRuns(runs), TotalSize: int32(total_size), NextPageToken: nextPageToken}, nil } func (s *RunServer) ArchiveRun(ctx context.Context, request *api.ArchiveRunRequest) (*empty.Empty, error) { diff --git a/backend/src/apiserver/storage/BUILD.bazel b/backend/src/apiserver/storage/BUILD.bazel index 5daef16c02f..e8c627ca808 100644 --- a/backend/src/apiserver/storage/BUILD.bazel +++ b/backend/src/apiserver/storage/BUILD.bazel @@ -8,7 +8,6 @@ go_library( "db_status_store.go", "experiment_store.go", "job_store.go", - "list_util.go", "minio_client.go", "minio_client_fake.go", "object_store.go", @@ -47,7 +46,6 @@ go_test( "db_test.go", "experiment_store_test.go", "job_store_test.go", - "list_util_test.go", "object_store_test.go", "pipeline_store_test.go", "resource_reference_store_test.go", @@ -61,7 +59,6 @@ go_test( "//backend/src/apiserver/model:go_default_library", "//backend/src/common/util:go_default_library", "//backend/src/crd/pkg/apis/scheduledworkflow/v1beta1:go_default_library", - "@com_github_masterminds_squirrel//:go_default_library", "@com_github_minio_minio_go//:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", diff --git a/backend/src/apiserver/storage/db_status_store.go b/backend/src/apiserver/storage/db_status_store.go index 261cc1c5b3b..33cbfa1d09f 100644 --- a/backend/src/apiserver/storage/db_status_store.go +++ b/backend/src/apiserver/storage/db_status_store.go @@ -16,6 +16,7 @@ package storage import ( sq "github.com/Masterminds/squirrel" + "github.com/golang/glog" "github.com/kubeflow/pipelines/backend/src/common/util" ) @@ -70,7 +71,7 @@ func (s *DBStatusStore) InitializeDBStatusTable() error { } err = tx.Commit() if err != nil { - tx.Rollback() + glog.Error("Failed to commit transaction to initialize database status table") return util.NewInternalServerError(err, "Failed to initializing the database status table.") } return nil diff --git a/backend/src/apiserver/storage/experiment_store.go b/backend/src/apiserver/storage/experiment_store.go index 32864cdabae..527266ba8ae 100644 --- a/backend/src/apiserver/storage/experiment_store.go +++ b/backend/src/apiserver/storage/experiment_store.go @@ -6,6 +6,7 @@ import ( "fmt" sq "github.com/Masterminds/squirrel" + "github.com/golang/glog" "github.com/kubeflow/pipelines/backend/src/apiserver/common" "github.com/kubeflow/pipelines/backend/src/apiserver/list" "github.com/kubeflow/pipelines/backend/src/apiserver/model" @@ -13,7 +14,7 @@ import ( ) type ExperimentStoreInterface interface { - ListExperiments(opts *list.Options) ([]*model.Experiment, string, error) + ListExperiments(opts *list.Options) ([]*model.Experiment, int, string, error) GetExperiment(uuid string) (*model.Experiment, error) CreateExperiment(*model.Experiment) (*model.Experiment, error) DeleteExperiment(uuid string) error @@ -26,33 +27,70 @@ type ExperimentStore struct { resourceReferenceStore *ResourceReferenceStore } -func (s *ExperimentStore) ListExperiments(opts *list.Options) ([]*model.Experiment, string, error) { - errorF := func(err error) ([]*model.Experiment, string, error) { - return nil, "", util.NewInternalServerError(err, "Failed to list experiments: %v", err) +// Runs two SQL queries in a transaction to return a list of matching experiments, as well as their +// total_size. The total_size does not reflect the page size. +func (s *ExperimentStore) ListExperiments(opts *list.Options) ([]*model.Experiment, int, string, error) { + errorF := func(err error) ([]*model.Experiment, int, string, error) { + return nil, 0, "", util.NewInternalServerError(err, "Failed to list experiments: %v", err) } - sql, args, err := opts.AddToSelect(sq.Select("*").From("experiments")).ToSql() + // SQL for getting the filtered and paginated rows + sqlBuilder := opts.AddFilterToSelect(sq.Select("*").From("experiments")) + rowsSql, rowsArgs, err := opts.AddPaginationToSelect(sqlBuilder).ToSql() if err != nil { return errorF(err) } - rows, err := s.db.Query(sql, args...) + // SQL for getting total size. This matches the query to get all the rows above, in order + // to do the same filter, but counts instead of scanning the rows. + sizeSql, sizeArgs, err := opts.AddFilterToSelect(sq.Select("count(*)").From("experiments")).ToSql() if err != nil { return errorF(err) } - defer rows.Close() + // Use a transaction to make sure we're returning the total_size of the same rows queried + tx, err := s.db.Begin() + if err != nil { + glog.Errorf("Failed to start transaction to list jobs") + return errorF(err) + } + + rows, err := tx.Query(rowsSql, rowsArgs...) + if err != nil { + tx.Rollback() + return errorF(err) + } exps, err := s.scanRows(rows) if err != nil { + tx.Rollback() + return errorF(err) + } + rows.Close() + + sizeRow, err := tx.Query(sizeSql, sizeArgs...) + if err != nil { + tx.Rollback() + return errorF(err) + } + total_size, err := list.ScanRowToTotalSize(sizeRow) + if err != nil { + tx.Rollback() + return errorF(err) + } + sizeRow.Close() + + err = tx.Commit() + if err != nil { + glog.Errorf("Failed to commit transaction to list experiments") return errorF(err) } if len(exps) <= opts.PageSize { - return exps, "", nil + return exps, total_size, "", nil } npt, err := opts.NextPageToken(exps[opts.PageSize]) - return exps[:opts.PageSize], npt, err + return exps[:opts.PageSize], total_size, npt, err } func (s *ExperimentStore) GetExperiment(uuid string) (*model.Experiment, error) { @@ -157,7 +195,6 @@ func (s *ExperimentStore) DeleteExperiment(id string) error { } err = tx.Commit() if err != nil { - tx.Rollback() return util.NewInternalServerError(err, "Failed to delete experiment %v and its resource references from table", id) } return nil diff --git a/backend/src/apiserver/storage/experiment_store_test.go b/backend/src/apiserver/storage/experiment_store_test.go index f9ae206a87c..9a8f381c25f 100644 --- a/backend/src/apiserver/storage/experiment_store_test.go +++ b/backend/src/apiserver/storage/experiment_store_test.go @@ -52,11 +52,12 @@ func TestListExperiments_Pagination(t *testing.T) { opts, err := list.NewOptions(&model.Experiment{}, 2, "name", nil) assert.Nil(t, err) - experiments, nextPageToken, err := experimentStore.ListExperiments(opts) + experiments, total_size, nextPageToken, err := experimentStore.ListExperiments(opts) assert.Nil(t, err) assert.NotEmpty(t, nextPageToken) assert.Equal(t, experimentsExpected, experiments) + assert.Equal(t, 4, total_size) expectedExperiment2 := &model.Experiment{ UUID: fakeIDTwo, @@ -75,9 +76,10 @@ func TestListExperiments_Pagination(t *testing.T) { opts, err = list.NewOptionsFromToken(nextPageToken, 2) assert.Nil(t, err) - experiments, nextPageToken, err = experimentStore.ListExperiments(opts) + experiments, total_size, nextPageToken, err = experimentStore.ListExperiments(opts) assert.Nil(t, err) assert.Empty(t, nextPageToken) + assert.Equal(t, 4, total_size) assert.Equal(t, experimentsExpected2, experiments) } @@ -109,10 +111,11 @@ func TestListExperiments_Pagination_Descend(t *testing.T) { opts, err := list.NewOptions(&model.Experiment{}, 2, "name desc", nil) assert.Nil(t, err) - experiments, nextPageToken, err := experimentStore.ListExperiments(opts) + experiments, total_size, nextPageToken, err := experimentStore.ListExperiments(opts) assert.Nil(t, err) assert.NotEmpty(t, nextPageToken) + assert.Equal(t, 4, total_size) assert.Equal(t, experimentsExpected, experiments) expectedExperiment1 := &model.Experiment{ @@ -132,9 +135,10 @@ func TestListExperiments_Pagination_Descend(t *testing.T) { opts, err = list.NewOptionsFromToken(nextPageToken, 2) assert.Nil(t, err) - experiments, nextPageToken, err = experimentStore.ListExperiments(opts) + experiments, total_size, nextPageToken, err = experimentStore.ListExperiments(opts) assert.Nil(t, err) assert.Empty(t, nextPageToken) + assert.Equal(t, 4, total_size) assert.Equal(t, experimentsExpected2, experiments) } @@ -154,9 +158,10 @@ func TestListExperiments_Pagination_LessThanPageSize(t *testing.T) { opts, err := list.NewOptions(&model.Experiment{}, 2, "", nil) assert.Nil(t, err) - experiments, nextPageToken, err := experimentStore.ListExperiments(opts) + experiments, total_size, nextPageToken, err := experimentStore.ListExperiments(opts) assert.Nil(t, err) assert.Equal(t, "", nextPageToken) + assert.Equal(t, 1, total_size) assert.Equal(t, experimentsExpected, experiments) } @@ -168,7 +173,7 @@ func TestListExperimentsError(t *testing.T) { opts, err := list.NewOptions(&model.Experiment{}, 2, "", nil) assert.Nil(t, err) - _, _, err = experimentStore.ListExperiments(opts) + _, _, _, err = experimentStore.ListExperiments(opts) assert.Equal(t, codes.Internal, err.(*util.UserError).ExternalStatusCode()) } @@ -323,7 +328,7 @@ func TestListExperiments_Filtering(t *testing.T) { opts, err := list.NewOptions(&model.Experiment{}, 2, "id", filterProto) assert.Nil(t, err) - experiments, nextPageToken, err := experimentStore.ListExperiments(opts) + experiments, total_size, nextPageToken, err := experimentStore.ListExperiments(opts) expected := []*model.Experiment{ &model.Experiment{ @@ -343,12 +348,13 @@ func TestListExperiments_Filtering(t *testing.T) { assert.Nil(t, err) assert.NotEqual(t, "", nextPageToken) assert.Equal(t, expected, experiments) + assert.Equal(t, 3, total_size) // Next page should give experiment4. opts, err = list.NewOptionsFromToken(nextPageToken, 2) assert.Nil(t, err) - experiments, nextPageToken, err = experimentStore.ListExperiments(opts) + experiments, total_size, nextPageToken, err = experimentStore.ListExperiments(opts) expected = []*model.Experiment{ &model.Experiment{ @@ -363,4 +369,5 @@ func TestListExperiments_Filtering(t *testing.T) { // No more pages. assert.Equal(t, "", nextPageToken) assert.Equal(t, expected, experiments) + assert.Equal(t, 4, total_size) } diff --git a/backend/src/apiserver/storage/job_store.go b/backend/src/apiserver/storage/job_store.go index 7d897a57ee4..fca74870496 100644 --- a/backend/src/apiserver/storage/job_store.go +++ b/backend/src/apiserver/storage/job_store.go @@ -19,6 +19,7 @@ import ( "fmt" sq "github.com/Masterminds/squirrel" + "github.com/golang/glog" "github.com/kubeflow/pipelines/backend/src/apiserver/common" "github.com/kubeflow/pipelines/backend/src/apiserver/list" "github.com/kubeflow/pipelines/backend/src/apiserver/model" @@ -26,7 +27,7 @@ import ( ) type JobStoreInterface interface { - ListJobs(filterContext *common.FilterContext, opts *list.Options) ([]*model.Job, string, error) + ListJobs(filterContext *common.FilterContext, opts *list.Options) ([]*model.Job, int, string, error) GetJob(id string) (*model.Job, error) CreateJob(*model.Job) (*model.Job, error) DeleteJob(id string) error @@ -40,67 +41,97 @@ type JobStore struct { time util.TimeInterface } +// Runs two SQL queries in a transaction to return a list of matching jobs, as well as their +// total_size. The total_size does not reflect the page size, but it does reflect the number of jobs +// matching the supplied filters and resource references. func (s *JobStore) ListJobs( - filterContext *common.FilterContext, opts *list.Options) ([]*model.Job, string, error) { - errorF := func(err error) ([]*model.Job, string, error) { - return nil, "", util.NewInternalServerError(err, "Failed to list jobs: %v", err) + filterContext *common.FilterContext, opts *list.Options) ([]*model.Job, int, string, error) { + errorF := func(err error) ([]*model.Job, int, string, error) { + return nil, 0, "", util.NewInternalServerError(err, "Failed to list jobs: %v", err) } - // Add filter condition - filteredSelectBuilder, err := s.toFilteredQuery(filterContext) + rowsSql, rowsArgs, err := s.buildSelectJobsQuery(false, opts, filterContext) if err != nil { return errorF(err) } - sqlBuilder := s.selectJob(filteredSelectBuilder) + sizeSql, sizeArgs, err := s.buildSelectJobsQuery(true, opts, filterContext) if err != nil { return errorF(err) } - sql, args, err := opts.AddToSelect(sqlBuilder).ToSql() + // Use a transaction to make sure we're returning the total_size of the same rows queried + tx, err := s.db.Begin() if err != nil { + glog.Errorf("Failed to start transaction to list jobs") return errorF(err) } - rows, err := s.db.Query(sql, args...) + rows, err := tx.Query(rowsSql, rowsArgs...) if err != nil { return errorF(err) } - defer rows.Close() - jobs, err := s.scanRows(rows) if err != nil { + tx.Rollback() + return errorF(err) + } + rows.Close() + + sizeRow, err := tx.Query(sizeSql, sizeArgs...) + if err != nil { + tx.Rollback() + return errorF(err) + } + total_size, err := list.ScanRowToTotalSize(sizeRow) + if err != nil { + tx.Rollback() + return errorF(err) + } + sizeRow.Close() + + err = tx.Commit() + if err != nil { + glog.Errorf("Failed to commit transaction to list jobs") return errorF(err) } if len(jobs) <= opts.PageSize { - return jobs, "", nil + return jobs, total_size, "", nil } npt, err := opts.NextPageToken(jobs[opts.PageSize]) - return jobs[:opts.PageSize], npt, err + return jobs[:opts.PageSize], total_size, npt, err } -func (s *JobStore) toFilteredQuery(filterContext *common.FilterContext) (sq.SelectBuilder, error) { - selectBuilder := sq.Select("*").From("jobs") - if filterContext.ReferenceKey != nil { - resourceReferenceFilter, args, err := sq.Select("ResourceUUID"). - From("resource_references as rf"). - Where(sq.And{ - sq.Eq{"rf.ResourceType": common.Job}, - sq.Eq{"rf.ReferenceUUID": filterContext.ID}, - sq.Eq{"rf.ReferenceType": filterContext.Type}}).ToSql() - if err != nil { - return selectBuilder, util.NewInternalServerError( - err, "Failed to create subquery to filter by resource reference: %v", err.Error()) - } - return selectBuilder.Where(fmt.Sprintf("UUID in (%s)", resourceReferenceFilter), args...), nil +func (s *JobStore) buildSelectJobsQuery(selectCount bool, opts *list.Options, + filterContext *common.FilterContext) (string, []interface{}, error) { + filteredSelectBuilder, err := list.FilterOnResourceReference("jobs", common.Job, selectCount, filterContext) + if err != nil { + return "", nil, util.NewInternalServerError(err, "Failed to list jobs: %v", err) + } + + sqlBuilder := opts.AddFilterToSelect(filteredSelectBuilder) + if err != nil { + return "", nil, util.NewInternalServerError(err, "Failed to list jobs: %v", err) + } + + // If we're not just counting, then also add select columns and perform a left join + // to get resource reference information. Also add pagination. + if !selectCount { + sqlBuilder = s.addResourceReferences(sqlBuilder) + sqlBuilder = opts.AddPaginationToSelect(sqlBuilder) } - return selectBuilder, nil + sql, args, err := sqlBuilder.ToSql() + if err != nil { + return "", nil, util.NewInternalServerError(err, "Failed to list jobs: %v", err) + } + + return sql, args, err } func (s *JobStore) GetJob(id string) (*model.Job, error) { - sql, args, err := s.selectJob(sq.Select("*").From("jobs")). + sql, args, err := s.addResourceReferences(sq.Select("*").From("jobs")). Where(sq.Eq{"uuid": id}). Limit(1). ToSql() @@ -124,7 +155,7 @@ func (s *JobStore) GetJob(id string) (*model.Job, error) { return jobs[0], nil } -func (s *JobStore) selectJob(filteredSelectBuilder sq.SelectBuilder) sq.SelectBuilder { +func (s *JobStore) addResourceReferences(filteredSelectBuilder sq.SelectBuilder) sq.SelectBuilder { resourceRefConcatQuery := s.db.Concat([]string{`"["`, s.db.GroupConcat("r.Payload", ","), `"]"`}, "") return sq. Select("jobs.*", resourceRefConcatQuery+" AS refs"). diff --git a/backend/src/apiserver/storage/job_store_test.go b/backend/src/apiserver/storage/job_store_test.go index a5f1c2cf9e0..9718e303491 100644 --- a/backend/src/apiserver/storage/job_store_test.go +++ b/backend/src/apiserver/storage/job_store_test.go @@ -18,6 +18,7 @@ import ( "testing" "time" + api "github.com/kubeflow/pipelines/backend/api/go_client" "github.com/kubeflow/pipelines/backend/src/apiserver/common" "github.com/kubeflow/pipelines/backend/src/apiserver/list" "github.com/kubeflow/pipelines/backend/src/apiserver/model" @@ -135,10 +136,11 @@ func TestListJobs_Pagination(t *testing.T) { opts, err := list.NewOptions(&model.Job{}, 1, "name", nil) assert.Nil(t, err) - jobs, nextPageToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) + jobs, total_size, nextPageToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) assert.Nil(t, err) assert.NotEmpty(t, nextPageToken) + assert.Equal(t, 2, total_size) assert.Equal(t, jobsExpected, jobs) jobsExpected2 := []*model.Job{ { @@ -171,12 +173,50 @@ func TestListJobs_Pagination(t *testing.T) { opts, err = list.NewOptionsFromToken(nextPageToken, 1) assert.Nil(t, err) - jobs, newToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) + jobs, total_size, newToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) assert.Nil(t, err) assert.Equal(t, "", newToken) + assert.Equal(t, 2, total_size) assert.Equal(t, jobsExpected2, jobs) } +func TestListJobs_TotalSizeWithNoFilter(t *testing.T) { + db, jobStore := initializeDbAndStore() + defer db.Close() + + opts, _ := list.NewOptions(&model.Job{}, 1, "name", nil) + + // No filter + jobs, total_size, _, err := jobStore.ListJobs(&common.FilterContext{}, opts) + assert.Nil(t, err) + assert.Equal(t, 1, len(jobs)) + assert.Equal(t, 2, total_size) +} + +func TestListJobs_TotalSizeWithFilter(t *testing.T) { + db, jobStore := initializeDbAndStore() + defer db.Close() + + // Add a filter + opts, _ := list.NewOptions(&model.Job{}, 1, "name", &api.Filter{ + Predicates: []*api.Predicate{ + &api.Predicate{ + Key: "name", + Op: api.Predicate_IN, + Value: &api.Predicate_StringValues{ + StringValues: &api.StringValues{ + Values: []string{"pp 1"}, + }, + }, + }, + }, + }) + jobs, total_size, _, err := jobStore.ListJobs(&common.FilterContext{}, opts) + assert.Nil(t, err) + assert.Equal(t, 1, len(jobs)) + assert.Equal(t, 1, total_size) +} + func TestListJobs_Pagination_Descent(t *testing.T) { db, jobStore := initializeDbAndStore() defer db.Close() @@ -211,9 +251,10 @@ func TestListJobs_Pagination_Descent(t *testing.T) { }} opts, err := list.NewOptions(&model.Job{}, 1, "name desc", nil) assert.Nil(t, err) - jobs, nextPageToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) + jobs, total_size, nextPageToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) assert.Nil(t, err) assert.NotEmpty(t, nextPageToken) + assert.Equal(t, 2, total_size) assert.Equal(t, jobsExpected, jobs) jobsExpected2 := []*model.Job{ @@ -247,9 +288,10 @@ func TestListJobs_Pagination_Descent(t *testing.T) { opts, err = list.NewOptionsFromToken(nextPageToken, 2) assert.Nil(t, err) - jobs, newToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) + jobs, total_size, newToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) assert.Nil(t, err) assert.Equal(t, "", newToken) + assert.Equal(t, 2, total_size) assert.Equal(t, jobsExpected2, jobs) } @@ -315,9 +357,10 @@ func TestListJobs_Pagination_LessThanPageSize(t *testing.T) { opts, err := list.NewOptions(&model.Job{}, 2, "name", nil) assert.Nil(t, err) - jobs, nextPageToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) + jobs, total_size, nextPageToken, err := jobStore.ListJobs(&common.FilterContext{}, opts) assert.Nil(t, err) assert.Equal(t, "", nextPageToken) + assert.Equal(t, 2, total_size) assert.Equal(t, jobsExpected, jobs) } @@ -356,10 +399,11 @@ func TestListJobs_FilterByReferenceKey(t *testing.T) { opts, err := list.NewOptions(&model.Job{}, 2, "name", nil) assert.Nil(t, err) - jobs, nextPageToken, err := jobStore.ListJobs( + jobs, total_size, nextPageToken, err := jobStore.ListJobs( &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Experiment, ID: defaultFakeExpId}}, opts) assert.Nil(t, err) assert.Equal(t, "", nextPageToken) + assert.Equal(t, 1, total_size) assert.Equal(t, jobsExpected, jobs) } @@ -370,7 +414,7 @@ func TestListJobsError(t *testing.T) { db.Close() opts, err := list.NewOptions(&model.Job{}, 2, "", nil) assert.Nil(t, err) - _, _, err = jobStore.ListJobs( + _, _, _, err = jobStore.ListJobs( &common.FilterContext{}, opts) assert.Equal(t, codes.Internal, err.(*util.UserError).ExternalStatusCode(), "Expected to list job to return error") diff --git a/backend/src/apiserver/storage/list_util.go b/backend/src/apiserver/storage/list_util.go deleted file mode 100644 index 73e9df64e2f..00000000000 --- a/backend/src/apiserver/storage/list_util.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2018 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. - -package storage - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "reflect" - - sq "github.com/Masterminds/squirrel" - "github.com/kubeflow/pipelines/backend/src/apiserver/common" - "github.com/kubeflow/pipelines/backend/src/apiserver/model" - "github.com/kubeflow/pipelines/backend/src/common/util" -) - -// Delegate to query the model table for a list of models. -type QueryListableModelTable func(request *common.PaginationContext) ([]model.ListableDataModel, error) - -// Logic for listing ListableModels. This would attempt to query one more model than requested, -// in order to generate the page token. If the result is less than request+1, the nextPageToken -// will be empty -func listModel(context *common.PaginationContext, queryTable QueryListableModelTable) (models []model.ListableDataModel, nextPageToken string, err error) { - newContext := *context - // List one more item to generate next page token. - newContext.PageSize = context.PageSize + 1 - results, err := queryTable(&newContext) - if err != nil { - return nil, "", util.Wrap(err, "List data model failed.") - } - if len(results) < newContext.PageSize { - return results, "", nil - } - tokenString, err := toNextPageToken(context.SortByFieldName, results[context.PageSize]) - if err != nil { - return nil, "", util.Wrap(err, "Failed to create page token") - } - return results[:len(results)-1], tokenString, nil -} - -// Generate page token given the first model to be listed in the next page. -func toNextPageToken(sortByFieldName string, model model.ListableDataModel) (string, error) { - newToken := common.Token{ - SortByFieldValue: fmt.Sprint(reflect.ValueOf(model).FieldByName(sortByFieldName)), - KeyFieldValue: model.GetValueOfPrimaryKey(), - } - - tokenBytes, err := json.Marshal(newToken) - if err != nil { - return "", util.NewInternalServerError(err, "Failed to serialize page token.") - } - return base64.StdEncoding.EncodeToString(tokenBytes), nil -} - -// If the PaginationContext is -// {sortByFieldName "name", keyFieldName:"id", token: {SortByFieldValue: "foo", KeyFieldValue: "2"}} -// This function construct query as something like -// select * from table where (name, id)>=("foo","2") order by name, id -func toPaginationQuery(selectBuilder sq.SelectBuilder, context *common.PaginationContext) sq.SelectBuilder { - if token := context.Token; token != nil { - if context.IsDesc { - selectBuilder = selectBuilder. - Where(sq.Or{sq.Lt{context.SortByFieldName: token.SortByFieldValue}, - sq.And{sq.Eq{context.SortByFieldName: token.SortByFieldValue}, sq.LtOrEq{context.KeyFieldName: token.KeyFieldValue}}}) - } else { - selectBuilder = selectBuilder. - Where(sq.Or{sq.Gt{context.SortByFieldName: token.SortByFieldValue}, - sq.And{sq.Eq{context.SortByFieldName: token.SortByFieldValue}, sq.GtOrEq{context.KeyFieldName: token.KeyFieldValue}}}) - } - } - order := "ASC" - if context.IsDesc { - order = "DESC" - } - selectBuilder = selectBuilder. - OrderBy(fmt.Sprintf("%v %v", context.SortByFieldName, order)). - OrderBy(fmt.Sprintf("%v %v", context.KeyFieldName, order)) - return selectBuilder -} diff --git a/backend/src/apiserver/storage/list_util_test.go b/backend/src/apiserver/storage/list_util_test.go deleted file mode 100644 index 25d80984221..00000000000 --- a/backend/src/apiserver/storage/list_util_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2018 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. - -package storage - -import ( - "encoding/base64" - "encoding/json" - "testing" - - "github.com/kubeflow/pipelines/backend/src/apiserver/common" - "github.com/kubeflow/pipelines/backend/src/apiserver/model" - "github.com/kubeflow/pipelines/backend/src/common/util" - "github.com/stretchr/testify/assert" - "google.golang.org/grpc/codes" -) - -type FakeListableModel struct { - Name string - Author string - Description string -} - -func (FakeListableModel) GetKeyName() string { - return "name" -} - -func (m FakeListableModel) GetValueOfPrimaryKey() string { - return m.Name -} - -func fooListInternal(request *common.PaginationContext) ([]model.ListableDataModel, error) { - return []model.ListableDataModel{ - FakeListableModel{Name: "a_name", Author: "a_author"}, - FakeListableModel{Name: "b_name", Author: "b_author"}}, nil -} - -func fooBadListInternal(request *common.PaginationContext) ([]model.ListableDataModel, error) { - return nil, util.NewInvalidInputError("some error") -} - -func TestList(t *testing.T) { - request := &common.PaginationContext{PageSize: 1, SortByFieldName: "name", KeyFieldName: "name", Token: nil} - models, token, err := listModel(request, fooListInternal) - assert.Nil(t, err) - assert.Equal(t, []model.ListableDataModel{FakeListableModel{Name: "a_name", Author: "a_author"}}, models) - expectedToken, err := toNextPageToken("name", FakeListableModel{Name: "b_name", Author: "b_author"}) - assert.Nil(t, err) - assert.Equal(t, expectedToken, token) -} - -func TestList_ListedAll(t *testing.T) { - request := &common.PaginationContext{PageSize: 2, SortByFieldName: "name", Token: nil} - models, token, err := listModel(request, fooListInternal) - assert.Nil(t, err) - assert.Equal(t, []model.ListableDataModel{ - FakeListableModel{Name: "a_name", Author: "a_author"}, - FakeListableModel{Name: "b_name", Author: "b_author"}}, models) - assert.Equal(t, "", token) -} - -func TestList_ListInternalError(t *testing.T) { - request := &common.PaginationContext{PageSize: 2, SortByFieldName: "name", Token: nil} - _, _, err := listModel(request, fooBadListInternal) - assert.Equal(t, codes.InvalidArgument, err.(*util.UserError).ExternalStatusCode()) -} - -func TestToNextPageToken(t *testing.T) { - model := FakeListableModel{Name: "foo", Author: "bar"} - token, err := toNextPageToken("Author", model) - assert.Nil(t, err) - expectedJson, _ := json.Marshal(common.Token{ - SortByFieldValue: "bar", - KeyFieldValue: "foo", - }) - assert.Equal(t, base64.StdEncoding.EncodeToString(expectedJson), token) -} diff --git a/backend/src/apiserver/storage/pipeline_store.go b/backend/src/apiserver/storage/pipeline_store.go index 28a0052911e..7367abdaa89 100644 --- a/backend/src/apiserver/storage/pipeline_store.go +++ b/backend/src/apiserver/storage/pipeline_store.go @@ -19,13 +19,14 @@ import ( "fmt" sq "github.com/Masterminds/squirrel" + "github.com/golang/glog" "github.com/kubeflow/pipelines/backend/src/apiserver/list" "github.com/kubeflow/pipelines/backend/src/apiserver/model" "github.com/kubeflow/pipelines/backend/src/common/util" ) type PipelineStoreInterface interface { - ListPipelines(opts *list.Options) ([]*model.Pipeline, string, error) + ListPipelines(opts *list.Options) ([]*model.Pipeline, int, string, error) GetPipeline(pipelineId string) (*model.Pipeline, error) GetPipelineWithStatus(id string, status model.PipelineStatus) (*model.Pipeline, error) DeletePipeline(pipelineId string) error @@ -39,34 +40,75 @@ type PipelineStore struct { uuid util.UUIDGeneratorInterface } -func (s *PipelineStore) ListPipelines(opts *list.Options) ([]*model.Pipeline, string, error) { - errorF := func(err error) ([]*model.Pipeline, string, error) { - return nil, "", util.NewInternalServerError(err, "Failed to list pipelines: %v", err) +// Runs two SQL queries in a transaction to return a list of matching pipelines, as well as their +// total_size. The total_size does not reflect the page size. +func (s *PipelineStore) ListPipelines(opts *list.Options) ([]*model.Pipeline, int, string, error) { + errorF := func(err error) ([]*model.Pipeline, int, string, error) { + return nil, 0, "", util.NewInternalServerError(err, "Failed to list pipelines: %v", err) } - sqlBuilder := sq.Select("*").From("pipelines").Where(sq.Eq{"Status": model.PipelineReady}) - sql, args, err := opts.AddToSelect(sqlBuilder).ToSql() + buildQuery := func(sqlBuilder sq.SelectBuilder) sq.SelectBuilder { + return sqlBuilder.From("pipelines").Where(sq.Eq{"Status": model.PipelineReady}) + } + + sqlBuilder := buildQuery(sq.Select("*")) + + // SQL for row list + rowsSql, rowsArgs, err := opts.AddPaginationToSelect(sqlBuilder).ToSql() if err != nil { return errorF(err) } - rows, err := s.db.Query(sql, args...) + // SQL for getting total size. This matches the query to get all the rows above, in order + // to do the same filter, but counts instead of scanning the rows. + sizeSql, sizeArgs, err := buildQuery(sq.Select("count(*)")).ToSql() if err != nil { return errorF(err) } - defer rows.Close() + // Use a transaction to make sure we're returning the total_size of the same rows queried + tx, err := s.db.Begin() + if err != nil { + glog.Errorf("Failed to start transaction to list pipelines") + return errorF(err) + } + + rows, err := tx.Query(rowsSql, rowsArgs...) + if err != nil { + tx.Rollback() + return errorF(err) + } pipelines, err := s.scanRows(rows) if err != nil { + tx.Rollback() + return errorF(err) + } + rows.Close() + + sizeRow, err := tx.Query(sizeSql, sizeArgs...) + if err != nil { + tx.Rollback() + return errorF(err) + } + total_size, err := list.ScanRowToTotalSize(sizeRow) + if err != nil { + tx.Rollback() + return errorF(err) + } + sizeRow.Close() + + err = tx.Commit() + if err != nil { + glog.Errorf("Failed to commit transaction to list pipelines") return errorF(err) } if len(pipelines) <= opts.PageSize { - return pipelines, "", nil + return pipelines, total_size, "", nil } npt, err := opts.NextPageToken(pipelines[opts.PageSize]) - return pipelines[:opts.PageSize], npt, err + return pipelines[:opts.PageSize], total_size, npt, err } func (s *PipelineStore) scanRows(rows *sql.Rows) ([]*model.Pipeline, error) { diff --git a/backend/src/apiserver/storage/pipeline_store_test.go b/backend/src/apiserver/storage/pipeline_store_test.go index 46dcc271351..4ee9d0f6a31 100644 --- a/backend/src/apiserver/storage/pipeline_store_test.go +++ b/backend/src/apiserver/storage/pipeline_store_test.go @@ -62,10 +62,11 @@ func TestListPipelines_FilterOutNotReady(t *testing.T) { opts, err := list.NewOptions(&model.Pipeline{}, 10, "id", nil) assert.Nil(t, err) - pipelines, nextPageToken, err := pipelineStore.ListPipelines(opts) + pipelines, total_size, nextPageToken, err := pipelineStore.ListPipelines(opts) assert.Nil(t, err) assert.Equal(t, "", nextPageToken) + assert.Equal(t, 2, total_size) assert.Equal(t, pipelinesExpected, pipelines) } @@ -96,9 +97,10 @@ func TestListPipelines_Pagination(t *testing.T) { opts, err := list.NewOptions(&model.Pipeline{}, 2, "name", nil) assert.Nil(t, err) - pipelines, nextPageToken, err := pipelineStore.ListPipelines(opts) + pipelines, total_size, nextPageToken, err := pipelineStore.ListPipelines(opts) assert.Nil(t, err) assert.NotEmpty(t, nextPageToken) + assert.Equal(t, 4, total_size) assert.Equal(t, pipelinesExpected, pipelines) expectedPipeline2 := &model.Pipeline{ @@ -118,9 +120,10 @@ func TestListPipelines_Pagination(t *testing.T) { opts, err = list.NewOptionsFromToken(nextPageToken, 2) assert.Nil(t, err) - pipelines, nextPageToken, err = pipelineStore.ListPipelines(opts) + pipelines, total_size, nextPageToken, err = pipelineStore.ListPipelines(opts) assert.Nil(t, err) assert.Empty(t, nextPageToken) + assert.Equal(t, 4, total_size) assert.Equal(t, pipelinesExpected2, pipelines) } @@ -152,9 +155,10 @@ func TestListPipelines_Pagination_Descend(t *testing.T) { opts, err := list.NewOptions(&model.Pipeline{}, 2, "name desc", nil) assert.Nil(t, err) - pipelines, nextPageToken, err := pipelineStore.ListPipelines(opts) + pipelines, total_size, nextPageToken, err := pipelineStore.ListPipelines(opts) assert.Nil(t, err) assert.NotEmpty(t, nextPageToken) + assert.Equal(t, 4, total_size) assert.Equal(t, pipelinesExpected, pipelines) expectedPipeline1 := &model.Pipeline{ @@ -173,9 +177,10 @@ func TestListPipelines_Pagination_Descend(t *testing.T) { opts, err = list.NewOptionsFromToken(nextPageToken, 2) assert.Nil(t, err) - pipelines, nextPageToken, err = pipelineStore.ListPipelines(opts) + pipelines, total_size, nextPageToken, err = pipelineStore.ListPipelines(opts) assert.Nil(t, err) assert.Empty(t, nextPageToken) + assert.Equal(t, 4, total_size) assert.Equal(t, pipelinesExpected2, pipelines) } @@ -194,9 +199,10 @@ func TestListPipelines_Pagination_LessThanPageSize(t *testing.T) { opts, err := list.NewOptions(&model.Pipeline{}, 2, "", nil) assert.Nil(t, err) - pipelines, nextPageToken, err := pipelineStore.ListPipelines(opts) + pipelines, total_size, nextPageToken, err := pipelineStore.ListPipelines(opts) assert.Nil(t, err) assert.Equal(t, "", nextPageToken) + assert.Equal(t, 1, total_size) assert.Equal(t, pipelinesExpected, pipelines) } @@ -207,7 +213,7 @@ func TestListPipelinesError(t *testing.T) { db.Close() opts, err := list.NewOptions(&model.Pipeline{}, 2, "", nil) assert.Nil(t, err) - _, _, err = pipelineStore.ListPipelines(opts) + _, _, _, err = pipelineStore.ListPipelines(opts) assert.Equal(t, codes.Internal, err.(*util.UserError).ExternalStatusCode()) } diff --git a/backend/src/apiserver/storage/run_store.go b/backend/src/apiserver/storage/run_store.go index 7e91b5efe2f..e980ae2252e 100644 --- a/backend/src/apiserver/storage/run_store.go +++ b/backend/src/apiserver/storage/run_store.go @@ -31,7 +31,7 @@ import ( type RunStoreInterface interface { GetRun(runId string) (*model.RunDetail, error) - ListRuns(filterContext *common.FilterContext, opts *list.Options) ([]*model.Run, string, error) + ListRuns(filterContext *common.FilterContext, opts *list.Options) ([]*model.Run, int, string, error) // Create a run entry in the database CreateRun(run *model.RunDetail) (*model.RunDetail, error) @@ -61,36 +61,58 @@ type RunStore struct { time util.TimeInterface } +// Runs two SQL queries in a transaction to return a list of matching runs, as well as their +// total_size. The total_size does not reflect the page size, but it does reflect the number of runs +// matching the supplied filters and resource references. func (s *RunStore) ListRuns( - filterContext *common.FilterContext, opts *list.Options) ([]*model.Run, string, error) { - errorF := func(err error) ([]*model.Run, string, error) { - return nil, "", util.NewInternalServerError(err, "Failed to list runs: %v", err) + filterContext *common.FilterContext, opts *list.Options) ([]*model.Run, int, string, error) { + errorF := func(err error) ([]*model.Run, int, string, error) { + return nil, 0, "", util.NewInternalServerError(err, "Failed to list runs: %v", err) } - // Add filter condition - filteredSelectBuilder, err := s.toFilteredQuery(filterContext) + rowsSql, rowsArgs, err := s.buildSelectRunsQuery(false, opts, filterContext) if err != nil { return errorF(err) } - sqlBuilder := s.selectRunDetails(filteredSelectBuilder) + sizeSql, sizeArgs, err := s.buildSelectRunsQuery(true, opts, filterContext) if err != nil { return errorF(err) } - sql, args, err := opts.AddToSelect(sqlBuilder).ToSql() + // Use a transaction to make sure we're returning the total_size of the same rows queried + tx, err := s.db.Begin() if err != nil { + glog.Error("Failed to start transaction to list runs") return errorF(err) } - rows, err := s.db.Query(sql, args...) + rows, err := tx.Query(rowsSql, rowsArgs...) + if err != nil { + return errorF(err) + } + runDetails, err := s.scanRowsToRunDetails(rows) + if err != nil { + tx.Rollback() + return errorF(err) + } + rows.Close() + + sizeRow, err := tx.Query(sizeSql, sizeArgs...) + if err != nil { + tx.Rollback() + return errorF(err) + } + total_size, err := list.ScanRowToTotalSize(sizeRow) if err != nil { + tx.Rollback() return errorF(err) } - defer rows.Close() + sizeRow.Close() - runDetails, err := s.scanRows(rows) + err = tx.Commit() if err != nil { + glog.Error("Failed to commit transaction to list runs") return errorF(err) } @@ -101,34 +123,42 @@ func (s *RunStore) ListRuns( } if len(runs) <= opts.PageSize { - return runs, "", nil + return runs, total_size, "", nil } npt, err := opts.NextPageToken(runs[opts.PageSize]) - return runs[:opts.PageSize], npt, err + return runs[:opts.PageSize], total_size, npt, err } -func (s *RunStore) toFilteredQuery(filterContext *common.FilterContext) (sq.SelectBuilder, error) { - selectBuilder := sq.Select("*").From("run_details") - if filterContext.ReferenceKey != nil { - resourceReferenceFilter, args, err := sq.Select("ResourceUUID"). - From("resource_references as rf"). - Where(sq.And{ - sq.Eq{"rf.ResourceType": common.Run}, - sq.Eq{"rf.ReferenceUUID": filterContext.ID}, - sq.Eq{"rf.ReferenceType": filterContext.Type}}).ToSql() - if err != nil { - return selectBuilder, util.NewInternalServerError( - err, "Failed to create subquery to filter by resource reference: %v", err.Error()) - } - return selectBuilder.Where(fmt.Sprintf("UUID in (%s)", resourceReferenceFilter), args...), nil +func (s *RunStore) buildSelectRunsQuery(selectCount bool, opts *list.Options, + filterContext *common.FilterContext) (string, []interface{}, error) { + filteredSelectBuilder, err := list.FilterOnResourceReference("run_details", common.Run, selectCount, filterContext) + if err != nil { + return "", nil, util.NewInternalServerError(err, "Failed to list runs: %v", err) } - return selectBuilder, nil + + sqlBuilder := opts.AddFilterToSelect(filteredSelectBuilder) + if err != nil { + return "", nil, util.NewInternalServerError(err, "Failed to list runs: %v", err) + } + + // If we're not just counting, then also add select columns and perform a left join + // to get resource reference information. Also add pagination. + if !selectCount { + sqlBuilder = s.addMetricsAndResourceReferences(sqlBuilder) + sqlBuilder = opts.AddPaginationToSelect(sqlBuilder) + } + sql, args, err := sqlBuilder.ToSql() + if err != nil { + return "", nil, util.NewInternalServerError(err, "Failed to list runs: %v", err) + } + + return sql, args, err } // GetRun Get the run manifest from Workflow CRD func (s *RunStore) GetRun(runId string) (*model.RunDetail, error) { - sql, args, err := s.selectRunDetails(sq.Select("*").From("run_details")). + sql, args, err := s.addMetricsAndResourceReferences(sq.Select("*").From("run_details")). Where(sq.Eq{"UUID": runId}). Limit(1). ToSql() @@ -141,7 +171,7 @@ func (s *RunStore) GetRun(runId string) (*model.RunDetail, error) { return nil, util.NewInternalServerError(err, "Failed to get run: %v", err.Error()) } defer r.Close() - runs, err := s.scanRows(r) + runs, err := s.scanRowsToRunDetails(r) if err != nil || len(runs) > 1 { return nil, util.NewInternalServerError(err, "Failed to get run: %v", err.Error()) @@ -156,7 +186,7 @@ func (s *RunStore) GetRun(runId string) (*model.RunDetail, error) { return runs[0], nil } -func (s *RunStore) selectRunDetails(filteredSelectBuilder sq.SelectBuilder) sq.SelectBuilder { +func (s *RunStore) addMetricsAndResourceReferences(filteredSelectBuilder sq.SelectBuilder) sq.SelectBuilder { metricConcatQuery := s.db.Concat([]string{`"["`, s.db.GroupConcat("m.Payload", ","), `"]"`}, "") subQ := sq. Select("rd.*", metricConcatQuery+" AS metrics"). @@ -173,7 +203,7 @@ func (s *RunStore) selectRunDetails(filteredSelectBuilder sq.SelectBuilder) sq.S GroupBy("subq.UUID") } -func (s *RunStore) scanRows(rows *sql.Rows) ([]*model.RunDetail, error) { +func (s *RunStore) scanRowsToRunDetails(rows *sql.Rows) ([]*model.RunDetail, error) { var runs []*model.RunDetail for rows.Next() { var uuid, displayName, name, storageState, namespace, description, pipelineId, pipelineSpecManifest, diff --git a/backend/src/apiserver/storage/run_store_test.go b/backend/src/apiserver/storage/run_store_test.go index b31a8866c23..96e9bb6e4c4 100644 --- a/backend/src/apiserver/storage/run_store_test.go +++ b/backend/src/apiserver/storage/run_store_test.go @@ -39,6 +39,7 @@ func initializeRunStore() (*DB, *RunStore) { Run: model.Run{ UUID: "1", Name: "run1", + DisplayName: "run1", StorageState: api.Run_STORAGESTATE_AVAILABLE.String(), Namespace: "n1", CreatedAtInSec: 1, @@ -60,6 +61,7 @@ func initializeRunStore() (*DB, *RunStore) { Run: model.Run{ UUID: "2", Name: "run2", + DisplayName: "run2", StorageState: api.Run_STORAGESTATE_AVAILABLE.String(), Namespace: "n2", CreatedAtInSec: 2, @@ -81,6 +83,7 @@ func initializeRunStore() (*DB, *RunStore) { Run: model.Run{ UUID: "3", Name: "run3", + DisplayName: "run3", Namespace: "n3", CreatedAtInSec: 3, StorageState: api.Run_STORAGESTATE_AVAILABLE.String(), @@ -112,6 +115,7 @@ func TestListRuns_Pagination(t *testing.T) { { UUID: "1", Name: "run1", + DisplayName: "run1", Namespace: "n1", CreatedAtInSec: 1, ScheduledAtInSec: 1, @@ -129,6 +133,7 @@ func TestListRuns_Pagination(t *testing.T) { { UUID: "2", Name: "run2", + DisplayName: "run2", Namespace: "n2", CreatedAtInSec: 2, ScheduledAtInSec: 2, @@ -146,21 +151,60 @@ func TestListRuns_Pagination(t *testing.T) { opts, err := list.NewOptions(&model.Run{}, 1, "", nil) assert.Nil(t, err) - runs, nextPageToken, err := runStore.ListRuns( + runs, total_size, nextPageToken, err := runStore.ListRuns( &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Experiment, ID: defaultFakeExpId}}, opts) assert.Nil(t, err) + assert.Equal(t, 2, total_size) assert.Equal(t, expectedFirstPageRuns, runs, "Unexpected Run listed.") assert.NotEmpty(t, nextPageToken) opts, err = list.NewOptionsFromToken(nextPageToken, 1) assert.Nil(t, err) - runs, nextPageToken, err = runStore.ListRuns( + runs, total_size, nextPageToken, err = runStore.ListRuns( &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Experiment, ID: defaultFakeExpId}}, opts) assert.Nil(t, err) + assert.Equal(t, 2, total_size) assert.Equal(t, expectedSecondPageRuns, runs, "Unexpected Run listed.") assert.Empty(t, nextPageToken) } +func TestListRuns_TotalSizeWithNoFilter(t *testing.T) { + db, runStore := initializeRunStore() + defer db.Close() + + opts, _ := list.NewOptions(&model.Run{}, 1, "", nil) + + // No filter + runs, total_size, _, err := runStore.ListRuns(&common.FilterContext{}, opts) + assert.Nil(t, err) + assert.Equal(t, 1, len(runs)) + assert.Equal(t, 3, total_size) +} + +func TestListRuns_TotalSizeWithFilter(t *testing.T) { + db, runStore := initializeRunStore() + defer db.Close() + + // Add a filter + opts, _ := list.NewOptions(&model.Run{}, 1, "", &api.Filter{ + Predicates: []*api.Predicate{ + &api.Predicate{ + Key: "name", + Op: api.Predicate_IN, + Value: &api.Predicate_StringValues{ + StringValues: &api.StringValues{ + Values: []string{"run1", "run3"}, + }, + }, + }, + }, + }) + runs, total_size, _, err := runStore.ListRuns(&common.FilterContext{}, opts) + assert.Nil(t, err) + assert.Equal(t, 1, len(runs)) + assert.Equal(t, 2, total_size) +} + func TestListRuns_Pagination_Descend(t *testing.T) { db, runStore := initializeRunStore() defer db.Close() @@ -169,6 +213,7 @@ func TestListRuns_Pagination_Descend(t *testing.T) { { UUID: "2", Name: "run2", + DisplayName: "run2", Namespace: "n2", CreatedAtInSec: 2, ScheduledAtInSec: 2, @@ -186,6 +231,7 @@ func TestListRuns_Pagination_Descend(t *testing.T) { { UUID: "1", Name: "run1", + DisplayName: "run1", Namespace: "n1", CreatedAtInSec: 1, ScheduledAtInSec: 1, @@ -202,18 +248,20 @@ func TestListRuns_Pagination_Descend(t *testing.T) { opts, err := list.NewOptions(&model.Run{}, 1, "id desc", nil) assert.Nil(t, err) - runs, nextPageToken, err := runStore.ListRuns( + runs, total_size, nextPageToken, err := runStore.ListRuns( &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Experiment, ID: defaultFakeExpId}}, opts) assert.Nil(t, err) + assert.Equal(t, 2, total_size) assert.Equal(t, expectedFirstPageRuns, runs, "Unexpected Run listed.") assert.NotEmpty(t, nextPageToken) opts, err = list.NewOptionsFromToken(nextPageToken, 1) assert.Nil(t, err) - runs, nextPageToken, err = runStore.ListRuns( + runs, total_size, nextPageToken, err = runStore.ListRuns( &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Experiment, ID: defaultFakeExpId}}, opts) assert.Nil(t, err) + assert.Equal(t, 2, total_size) assert.Equal(t, expectedSecondPageRuns, runs, "Unexpected Run listed.") assert.Empty(t, nextPageToken) } @@ -226,6 +274,7 @@ func TestListRuns_Pagination_LessThanPageSize(t *testing.T) { { UUID: "1", Name: "run1", + DisplayName: "run1", Namespace: "n1", CreatedAtInSec: 1, ScheduledAtInSec: 1, @@ -242,6 +291,7 @@ func TestListRuns_Pagination_LessThanPageSize(t *testing.T) { { UUID: "2", Name: "run2", + DisplayName: "run2", Namespace: "n2", CreatedAtInSec: 2, ScheduledAtInSec: 2, @@ -258,9 +308,10 @@ func TestListRuns_Pagination_LessThanPageSize(t *testing.T) { opts, err := list.NewOptions(&model.Run{}, 10, "", nil) assert.Nil(t, err) - runs, nextPageToken, err := runStore.ListRuns( + runs, total_size, nextPageToken, err := runStore.ListRuns( &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Experiment, ID: defaultFakeExpId}}, opts) assert.Nil(t, err) + assert.Equal(t, 2, total_size) assert.Equal(t, expectedRuns, runs, "Unexpected Run listed.") assert.Empty(t, nextPageToken) } @@ -270,7 +321,7 @@ func TestListRunsError(t *testing.T) { db.Close() opts, err := list.NewOptions(&model.Run{}, 1, "", nil) - _, _, err = runStore.ListRuns( + _, _, _, err = runStore.ListRuns( &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Experiment, ID: defaultFakeExpId}}, opts) assert.Equal(t, codes.Internal, err.(*util.UserError).ExternalStatusCode(), "Expected to throw an internal error") @@ -284,6 +335,7 @@ func TestGetRun(t *testing.T) { Run: model.Run{ UUID: "1", Name: "run1", + DisplayName: "run1", Namespace: "n1", CreatedAtInSec: 1, ScheduledAtInSec: 1, @@ -331,6 +383,7 @@ func TestCreateOrUpdateRun_UpdateSuccess(t *testing.T) { Run: model.Run{ UUID: "1", Name: "run1", + DisplayName: "run1", Namespace: "n1", CreatedAtInSec: 1, ScheduledAtInSec: 1, @@ -367,6 +420,7 @@ func TestCreateOrUpdateRun_UpdateSuccess(t *testing.T) { Run: model.Run{ UUID: "1", Name: "run1", + DisplayName: "run1", Namespace: "n1", CreatedAtInSec: 1, ScheduledAtInSec: 1, @@ -624,6 +678,7 @@ func TestListRuns_WithMetrics(t *testing.T) { { UUID: "1", Name: "run1", + DisplayName: "run1", Namespace: "n1", CreatedAtInSec: 1, ScheduledAtInSec: 1, @@ -641,6 +696,7 @@ func TestListRuns_WithMetrics(t *testing.T) { { UUID: "2", Name: "run2", + DisplayName: "run2", Namespace: "n2", CreatedAtInSec: 2, ScheduledAtInSec: 2, @@ -659,7 +715,8 @@ func TestListRuns_WithMetrics(t *testing.T) { opts, err := list.NewOptions(&model.Run{}, 2, "", nil) assert.Nil(t, err) - runs, _, err := runStore.ListRuns(&common.FilterContext{}, opts) + runs, total_size, _, err := runStore.ListRuns(&common.FilterContext{}, opts) + assert.Equal(t, 3, total_size) assert.Nil(t, err) assert.Equal(t, expectedRuns, runs, "Unexpected Run listed.") } @@ -750,6 +807,7 @@ func TestArchiveRun_IncludedInRunList(t *testing.T) { { UUID: "1", Name: "run1", + DisplayName: "run1", Namespace: "n1", CreatedAtInSec: 1, ScheduledAtInSec: 1, @@ -764,9 +822,10 @@ func TestArchiveRun_IncludedInRunList(t *testing.T) { }, }} opts, err := list.NewOptions(&model.Run{}, 1, "", nil) - runs, nextPageToken, err := runStore.ListRuns( + runs, total_size, nextPageToken, err := runStore.ListRuns( &common.FilterContext{ReferenceKey: &common.ReferenceKey{Type: common.Experiment, ID: defaultFakeExpId}}, opts) assert.Nil(t, err) + assert.Equal(t, 2, total_size) assert.Equal(t, expectedRuns, runs) assert.NotEmpty(t, nextPageToken) } diff --git a/backend/src/common/client/api_server/experiment_client.go b/backend/src/common/client/api_server/experiment_client.go index 93738159f2a..f246335797e 100644 --- a/backend/src/common/client/api_server/experiment_client.go +++ b/backend/src/common/client/api_server/experiment_client.go @@ -16,7 +16,7 @@ import ( type ExperimentInterface interface { Create(params *params.CreateExperimentParams) (*model.APIExperiment, error) Get(params *params.GetExperimentParams) (*model.APIExperiment, error) - List(params *params.ListExperimentParams) ([]*model.APIExperiment, string, error) + List(params *params.ListExperimentParams) ([]*model.APIExperiment, int, string, error) ListAll(params *params.ListExperimentParams, maxResultSize int) ([]*model.APIExperiment, error) } @@ -89,7 +89,7 @@ func (c *ExperimentClient) Get(parameters *params.GetExperimentParams) (*model.A } func (c *ExperimentClient) List(parameters *params.ListExperimentParams) ( - []*model.APIExperiment, string, error) { + []*model.APIExperiment, int, string, error) { // Create context with timeout ctx, cancel := context.WithTimeout(context.Background(), apiServerDefaultTimeout) defer cancel() @@ -104,12 +104,12 @@ func (c *ExperimentClient) List(parameters *params.ListExperimentParams) ( err = CreateErrorCouldNotRecoverAPIStatus(err) } - return nil, "", util.NewUserError(err, + return nil, 0, "", util.NewUserError(err, fmt.Sprintf("Failed to list experiments. Params: '%+v'", parameters), fmt.Sprintf("Failed to list experiments")) } - return response.Payload.Experiments, response.Payload.NextPageToken, nil + return response.Payload.Experiments, int(response.Payload.TotalSize), response.Payload.NextPageToken, nil } func (c *ExperimentClient) Delete(parameters *params.DeleteExperimentParams) error { @@ -150,7 +150,7 @@ func listAllForExperiment(client ExperimentInterface, parameters *params.ListExp firstCall := true for (firstCall || (parameters.PageToken != nil && *parameters.PageToken != "")) && (len(allResults) < maxResultSize) { - results, pageToken, err := client.List(parameters) + results, _, pageToken, err := client.List(parameters) if err != nil { return nil, err } diff --git a/backend/src/common/client/api_server/experiment_client_fake.go b/backend/src/common/client/api_server/experiment_client_fake.go index 726638f5dde..43f3779e8d8 100644 --- a/backend/src/common/client/api_server/experiment_client_fake.go +++ b/backend/src/common/client/api_server/experiment_client_fake.go @@ -49,7 +49,7 @@ func (c *ExperimentClientFake) Get(params *experimentparams.GetExperimentParams) } func (c *ExperimentClientFake) List(params *experimentparams.ListExperimentParams) ( - []*experimentmodel.APIExperiment, string, error) { + []*experimentmodel.APIExperiment, int, string, error) { const ( FirstToken = "" SecondToken = "SECOND_TOKEN" @@ -66,13 +66,13 @@ func (c *ExperimentClientFake) List(params *experimentparams.ListExperimentParam return []*experimentmodel.APIExperiment{ getDefaultExperiment("100", "MY_FIRST_EXPERIMENT"), getDefaultExperiment("101", "MY_SECOND_EXPERIMENT"), - }, SecondToken, nil + }, 2, SecondToken, nil case SecondToken: return []*experimentmodel.APIExperiment{ getDefaultExperiment("102", "MY_THIRD_EXPERIMENT"), - }, FinalToken, nil + }, 1, FinalToken, nil default: - return nil, "", fmt.Errorf(InvalidFakeRequest, token) + return nil, 0, "", fmt.Errorf(InvalidFakeRequest, token) } } diff --git a/backend/src/common/client/api_server/job_client.go b/backend/src/common/client/api_server/job_client.go index e366c0884dc..af7ef52d32d 100644 --- a/backend/src/common/client/api_server/job_client.go +++ b/backend/src/common/client/api_server/job_client.go @@ -19,7 +19,7 @@ type JobInterface interface { Delete(params *params.DeleteJobParams) error Enable(params *params.EnableJobParams) error Disable(params *params.DisableJobParams) error - List(params *params.ListJobsParams) ([]*model.APIJob, string, error) + List(params *params.ListJobsParams) ([]*model.APIJob, int, string, error) ListAll(params *params.ListJobsParams, maxResultSize int) ([]*model.APIJob, error) } @@ -161,7 +161,7 @@ func (c *JobClient) Disable(parameters *params.DisableJobParams) error { } func (c *JobClient) List(parameters *params.ListJobsParams) ( - []*model.APIJob, string, error) { + []*model.APIJob, int, string, error) { // Create context with timeout ctx, cancel := context.WithTimeout(context.Background(), apiServerDefaultTimeout) defer cancel() @@ -176,12 +176,12 @@ func (c *JobClient) List(parameters *params.ListJobsParams) ( err = CreateErrorCouldNotRecoverAPIStatus(err) } - return nil, "", util.NewUserError(err, + return nil, 0, "", util.NewUserError(err, fmt.Sprintf("Failed to list jobs. Params: '%+v'", parameters), fmt.Sprintf("Failed to list jobs")) } - return response.Payload.Jobs, response.Payload.NextPageToken, nil + return response.Payload.Jobs, int(response.Payload.TotalSize), response.Payload.NextPageToken, nil } func (c *JobClient) ListAll(parameters *params.ListJobsParams, maxResultSize int) ( @@ -199,7 +199,7 @@ func listAllForJob(client JobInterface, parameters *params.ListJobsParams, firstCall := true for (firstCall || (parameters.PageToken != nil && *parameters.PageToken != "")) && (len(allResults) < maxResultSize) { - results, pageToken, err := client.List(parameters) + results, _, pageToken, err := client.List(parameters) if err != nil { return nil, err } diff --git a/backend/src/common/client/api_server/job_client_fake.go b/backend/src/common/client/api_server/job_client_fake.go index a88087587cb..97d6f758be2 100644 --- a/backend/src/common/client/api_server/job_client_fake.go +++ b/backend/src/common/client/api_server/job_client_fake.go @@ -76,7 +76,7 @@ func (c *JobClientFake) Disable(params *jobparams.DisableJobParams) error { } func (c *JobClientFake) List(params *jobparams.ListJobsParams) ( - []*jobmodel.APIJob, string, error) { + []*jobmodel.APIJob, int, string, error) { const ( FirstToken = "" SecondToken = "SECOND_TOKEN" @@ -93,13 +93,13 @@ func (c *JobClientFake) List(params *jobparams.ListJobsParams) ( return []*jobmodel.APIJob{ getDefaultJob("100", "MY_FIRST_JOB"), getDefaultJob("101", "MY_SECOND_JOB"), - }, SecondToken, nil + }, 2, SecondToken, nil case SecondToken: return []*jobmodel.APIJob{ getDefaultJob("102", "MY_THIRD_JOB"), - }, FinalToken, nil + }, 1, FinalToken, nil default: - return nil, "", fmt.Errorf(InvalidFakeRequest, token) + return nil, 0, "", fmt.Errorf(InvalidFakeRequest, token) } } diff --git a/backend/src/common/client/api_server/pipeline_client.go b/backend/src/common/client/api_server/pipeline_client.go index 14219de4e93..e2d3bd5b4bf 100644 --- a/backend/src/common/client/api_server/pipeline_client.go +++ b/backend/src/common/client/api_server/pipeline_client.go @@ -20,7 +20,7 @@ type PipelineInterface interface { Get(params *params.GetPipelineParams) (*model.APIPipeline, error) Delete(params *params.DeletePipelineParams) error GetTemplate(params *params.GetTemplateParams) (*workflowapi.Workflow, error) - List(params *params.ListPipelinesParams) ([]*model.APIPipeline, string, error) + List(params *params.ListPipelinesParams) ([]*model.APIPipeline, int, string, error) ListAll(params *params.ListPipelinesParams, maxResultSize int) ( []*model.APIPipeline, error) } @@ -150,7 +150,7 @@ func (c *PipelineClient) GetTemplate(parameters *params.GetTemplateParams) ( } func (c *PipelineClient) List(parameters *params.ListPipelinesParams) ( - []*model.APIPipeline, string, error) { + []*model.APIPipeline, int, string, error) { // Create context with timeout ctx, cancel := context.WithTimeout(context.Background(), apiServerDefaultTimeout) defer cancel() @@ -165,12 +165,12 @@ func (c *PipelineClient) List(parameters *params.ListPipelinesParams) ( err = CreateErrorCouldNotRecoverAPIStatus(err) } - return nil, "", util.NewUserError(err, + return nil, 0, "", util.NewUserError(err, fmt.Sprintf("Failed to list pipelines. Params: '%+v'", parameters), fmt.Sprintf("Failed to list pipelines")) } - return response.Payload.Pipelines, response.Payload.NextPageToken, nil + return response.Payload.Pipelines, int(response.Payload.TotalSize), response.Payload.NextPageToken, nil } func (c *PipelineClient) ListAll(parameters *params.ListPipelinesParams, maxResultSize int) ( @@ -188,7 +188,7 @@ func listAllForPipeline(client PipelineInterface, parameters *params.ListPipelin firstCall := true for (firstCall || (parameters.PageToken != nil && *parameters.PageToken != "")) && (len(allResults) < maxResultSize) { - results, pageToken, err := client.List(parameters) + results, _, pageToken, err := client.List(parameters) if err != nil { return nil, err } diff --git a/backend/src/common/client/api_server/pipeline_client_fake.go b/backend/src/common/client/api_server/pipeline_client_fake.go index dec42f0bed4..9e7edd4fcde 100644 --- a/backend/src/common/client/api_server/pipeline_client_fake.go +++ b/backend/src/common/client/api_server/pipeline_client_fake.go @@ -96,7 +96,7 @@ func (c *PipelineClientFake) GetTemplate(params *pipelineparams.GetTemplateParam } func (c *PipelineClientFake) List(params *pipelineparams.ListPipelinesParams) ( - []*pipelinemodel.APIPipeline, string, error) { + []*pipelinemodel.APIPipeline, int, string, error) { const ( FirstToken = "" @@ -114,13 +114,13 @@ func (c *PipelineClientFake) List(params *pipelineparams.ListPipelinesParams) ( return []*pipelinemodel.APIPipeline{ getDefaultPipeline("PIPELINE_ID_100"), getDefaultPipeline("PIPELINE_ID_101"), - }, SecondToken, nil + }, 2, SecondToken, nil case SecondToken: return []*pipelinemodel.APIPipeline{ getDefaultPipeline("PIPELINE_ID_102"), - }, FinalToken, nil + }, 1, FinalToken, nil default: - return nil, "", fmt.Errorf(InvalidFakeRequest, token) + return nil, 0, "", fmt.Errorf(InvalidFakeRequest, token) } } diff --git a/backend/src/common/client/api_server/run_client.go b/backend/src/common/client/api_server/run_client.go index bce24942cfc..9ec24f906f4 100644 --- a/backend/src/common/client/api_server/run_client.go +++ b/backend/src/common/client/api_server/run_client.go @@ -18,7 +18,7 @@ import ( type RunInterface interface { Archive(params *params.ArchiveRunParams) error Get(params *params.GetRunParams) (*model.APIRunDetail, *workflowapi.Workflow, error) - List(params *params.ListRunsParams) ([]*model.APIRun, string, error) + List(params *params.ListRunsParams) ([]*model.APIRun, int, string, error) ListAll(params *params.ListRunsParams, maxResultSize int) ([]*model.APIRun, error) Unarchive(params *params.UnarchiveRunParams) error } @@ -184,7 +184,7 @@ func (c *RunClient) Delete(parameters *params.DeleteRunParams) error { } func (c *RunClient) List(parameters *params.ListRunsParams) ( - []*model.APIRun, string, error) { + []*model.APIRun, int, string, error) { // Create context with timeout ctx, cancel := context.WithTimeout(context.Background(), apiServerDefaultTimeout) defer cancel() @@ -200,12 +200,12 @@ func (c *RunClient) List(parameters *params.ListRunsParams) ( err = CreateErrorCouldNotRecoverAPIStatus(err) } - return nil, "", util.NewUserError(err, + return nil, 0, "", util.NewUserError(err, fmt.Sprintf("Failed to list runs. Params: '%+v'", parameters), fmt.Sprintf("Failed to list runs")) } - return response.Payload.Runs, response.Payload.NextPageToken, nil + return response.Payload.Runs, int(response.Payload.TotalSize), response.Payload.NextPageToken, nil } func (c *RunClient) ListAll(parameters *params.ListRunsParams, maxResultSize int) ( @@ -223,7 +223,7 @@ func listAllForRun(client RunInterface, parameters *params.ListRunsParams, maxRe firstCall := true for (firstCall || (parameters.PageToken != nil && *parameters.PageToken != "")) && (len(allResults) < maxResultSize) { - results, pageToken, err := client.List(parameters) + results, _, pageToken, err := client.List(parameters) if err != nil { return nil, err } diff --git a/backend/src/common/client/api_server/run_client_fake.go b/backend/src/common/client/api_server/run_client_fake.go index b368de61fa7..82aff96ab03 100644 --- a/backend/src/common/client/api_server/run_client_fake.go +++ b/backend/src/common/client/api_server/run_client_fake.go @@ -43,7 +43,7 @@ func (c *RunClientFake) Get(params *runparams.GetRunParams) (*runmodel.APIRunDet } func (c *RunClientFake) List(params *runparams.ListRunsParams) ( - []*runmodel.APIRun, string, error) { + []*runmodel.APIRun, int, string, error) { const ( FirstToken = "" SecondToken = "SECOND_TOKEN" @@ -60,13 +60,13 @@ func (c *RunClientFake) List(params *runparams.ListRunsParams) ( return []*runmodel.APIRun{ getDefaultRun("100", "MY_FIRST_RUN").Run, getDefaultRun("101", "MY_SECOND_RUN").Run, - }, SecondToken, nil + }, 2, SecondToken, nil case SecondToken: return []*runmodel.APIRun{ getDefaultRun("102", "MY_THIRD_RUN").Run, - }, FinalToken, nil + }, 1, FinalToken, nil default: - return nil, "", fmt.Errorf(InvalidFakeRequest, token) + return nil, 0, "", fmt.Errorf(InvalidFakeRequest, token) } } diff --git a/backend/test/experiment_api_test.go b/backend/test/experiment_api_test.go index c32b2b24930..e55b4c30fe2 100644 --- a/backend/test/experiment_api_test.go +++ b/backend/test/experiment_api_test.go @@ -43,8 +43,9 @@ func (s *ExperimentApiTest) TestExperimentAPI() { t := s.T() /* ---------- Verify no experiment exist ---------- */ - experiments, _, err := s.experimentClient.List(¶ms.ListExperimentParams{}) + experiments, totalSize, _, err := s.experimentClient.List(¶ms.ListExperimentParams{}) assert.Nil(t, err) + assert.Equal(t, 0, totalSize) assert.True(t, len(experiments) == 0) /* ---------- Create a new experiment ---------- */ @@ -78,8 +79,9 @@ func (s *ExperimentApiTest) TestExperimentAPI() { assert.Nil(t, err) /* ---------- Verify list experiments works ---------- */ - experiments, nextPageToken, err := s.experimentClient.List(¶ms.ListExperimentParams{}) + experiments, totalSize, nextPageToken, err := s.experimentClient.List(¶ms.ListExperimentParams{}) assert.Nil(t, err) + assert.Equal(t, 3, totalSize) assert.Equal(t, 3, len(experiments)) for _, e := range experiments { // Sampling one of the experiments and verify the result is expected. @@ -89,54 +91,60 @@ func (s *ExperimentApiTest) TestExperimentAPI() { } /* ---------- Verify list experiments sorted by names ---------- */ - experiments, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ + experiments, totalSize, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("name")}) assert.Nil(t, err) + assert.Equal(t, 3, totalSize) assert.Equal(t, 2, len(experiments)) assert.Equal(t, "moonshot", experiments[0].Name) assert.Equal(t, "prediction", experiments[1].Name) assert.NotEmpty(t, nextPageToken) - experiments, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ + experiments, totalSize, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ PageToken: util.StringPointer(nextPageToken), PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("name")}) assert.Nil(t, err) + assert.Equal(t, 3, totalSize) assert.Equal(t, 1, len(experiments)) assert.Equal(t, "training", experiments[0].Name) assert.Empty(t, nextPageToken) /* ---------- Verify list experiments sorted by creation time ---------- */ - experiments, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ + experiments, totalSize, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("created_at")}) assert.Nil(t, err) + assert.Equal(t, 3, totalSize) assert.Equal(t, 2, len(experiments)) assert.Equal(t, "training", experiments[0].Name) assert.Equal(t, "prediction", experiments[1].Name) assert.NotEmpty(t, nextPageToken) - experiments, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ + experiments, totalSize, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ PageToken: util.StringPointer(nextPageToken), PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("created_at")}) assert.Nil(t, err) + assert.Equal(t, 3, totalSize) assert.Equal(t, 1, len(experiments)) assert.Equal(t, "moonshot", experiments[0].Name) assert.Empty(t, nextPageToken) /* ---------- List experiments sort by unsupported field. Should fail. ---------- */ - _, _, err = s.experimentClient.List(¶ms.ListExperimentParams{ + _, _, _, err = s.experimentClient.List(¶ms.ListExperimentParams{ PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("unknownfield")}) assert.NotNil(t, err) /* ---------- List experiments sorted by names descend order ---------- */ - experiments, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ + experiments, totalSize, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("name desc")}) assert.Nil(t, err) + assert.Equal(t, 3, totalSize) assert.Equal(t, 2, len(experiments)) assert.Equal(t, "training", experiments[0].Name) assert.Equal(t, "prediction", experiments[1].Name) assert.NotEmpty(t, nextPageToken) - experiments, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ + experiments, totalSize, nextPageToken, err = s.experimentClient.List(¶ms.ListExperimentParams{ PageToken: util.StringPointer(nextPageToken), PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("name desc")}) assert.Nil(t, err) + assert.Equal(t, 3, totalSize) assert.Equal(t, 1, len(experiments)) assert.Equal(t, "moonshot", experiments[0].Name) assert.Empty(t, nextPageToken) diff --git a/backend/test/job_api_test.go b/backend/test/job_api_test.go index 22f701f7463..24f01d0b578 100644 --- a/backend/test/job_api_test.go +++ b/backend/test/job_api_test.go @@ -134,44 +134,50 @@ func (s *JobApiTestSuite) TestJobApis() { s.checkArgParamsJob(t, argParamsJob, argParamsExperiment.ID) /* ---------- List all the jobs. Both jobs should be returned ---------- */ - jobs, _, err := s.jobClient.List(&jobparams.ListJobsParams{}) + jobs, totalSize, _, err := s.jobClient.List(&jobparams.ListJobsParams{}) assert.Nil(t, err) + assert.Equal(t, 2, totalSize) assert.Equal(t, 2, len(jobs)) /* ---------- List the jobs, paginated, default sort ---------- */ - jobs, nextPageToken, err := s.jobClient.List(&jobparams.ListJobsParams{PageSize: util.Int32Pointer(1)}) + jobs, totalSize, nextPageToken, err := s.jobClient.List(&jobparams.ListJobsParams{PageSize: util.Int32Pointer(1)}) assert.Nil(t, err) assert.Equal(t, 1, len(jobs)) + assert.Equal(t, 2, totalSize) assert.Equal(t, "hello world", jobs[0].Name) - jobs, _, err = s.jobClient.List(&jobparams.ListJobsParams{ + jobs, totalSize, _, err = s.jobClient.List(&jobparams.ListJobsParams{ PageSize: util.Int32Pointer(1), PageToken: util.StringPointer(nextPageToken)}) assert.Nil(t, err) assert.Equal(t, 1, len(jobs)) + assert.Equal(t, 2, totalSize) assert.Equal(t, "argument parameter", jobs[0].Name) /* ---------- List the jobs, paginated, sort by name ---------- */ - jobs, nextPageToken, err = s.jobClient.List(&jobparams.ListJobsParams{ + jobs, totalSize, nextPageToken, err = s.jobClient.List(&jobparams.ListJobsParams{ PageSize: util.Int32Pointer(1), SortBy: util.StringPointer("name")}) assert.Nil(t, err) + assert.Equal(t, 2, totalSize) assert.Equal(t, 1, len(jobs)) assert.Equal(t, "argument parameter", jobs[0].Name) - jobs, _, err = s.jobClient.List(&jobparams.ListJobsParams{ + jobs, totalSize, _, err = s.jobClient.List(&jobparams.ListJobsParams{ PageSize: util.Int32Pointer(1), SortBy: util.StringPointer("name"), PageToken: util.StringPointer(nextPageToken)}) assert.Nil(t, err) + assert.Equal(t, 2, totalSize) assert.Equal(t, 1, len(jobs)) assert.Equal(t, "hello world", jobs[0].Name) /* ---------- List the jobs, sort by unsupported field ---------- */ - jobs, _, err = s.jobClient.List(&jobparams.ListJobsParams{ + jobs, _, _, err = s.jobClient.List(&jobparams.ListJobsParams{ PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("unknown")}) assert.NotNil(t, err) /* ---------- List jobs for hello world experiment. One job should be returned ---------- */ - jobs, _, err = s.jobClient.List(&jobparams.ListJobsParams{ + jobs, totalSize, _, err = s.jobClient.List(&jobparams.ListJobsParams{ ResourceReferenceKeyType: util.StringPointer(string(run_model.APIResourceTypeEXPERIMENT)), ResourceReferenceKeyID: util.StringPointer(helloWorldExperiment.ID)}) assert.Nil(t, err) assert.Equal(t, 1, len(jobs)) + assert.Equal(t, 1, totalSize) assert.Equal(t, "hello world", jobs[0].Name) // The scheduledWorkflow CRD would create the run and it synced to the DB by persistent agent. @@ -180,20 +186,22 @@ func (s *JobApiTestSuite) TestJobApis() { time.Sleep(40 * time.Second) /* ---------- Check run for hello world job ---------- */ - runs, _, err := s.runClient.List(&runParams.ListRunsParams{ + runs, totalSize, _, err := s.runClient.List(&runParams.ListRunsParams{ ResourceReferenceKeyType: util.StringPointer(string(run_model.APIResourceTypeEXPERIMENT)), ResourceReferenceKeyID: util.StringPointer(helloWorldExperiment.ID)}) assert.Nil(t, err) assert.Equal(t, 1, len(runs)) + assert.Equal(t, 1, totalSize) helloWorldRun := runs[0] s.checkHelloWorldRun(t, helloWorldRun, helloWorldExperiment.ID, helloWorldJob.ID) /* ---------- Check run for argument parameter job ---------- */ - runs, _, err = s.runClient.List(&runParams.ListRunsParams{ + runs, totalSize, _, err = s.runClient.List(&runParams.ListRunsParams{ ResourceReferenceKeyType: util.StringPointer(string(run_model.APIResourceTypeEXPERIMENT)), ResourceReferenceKeyID: util.StringPointer(argParamsExperiment.ID)}) assert.Nil(t, err) assert.Equal(t, 1, len(runs)) + assert.Equal(t, 1, totalSize) argParamsRun := runs[0] s.checkArgParamsRun(t, argParamsRun, argParamsExperiment.ID, argParamsJob.ID) diff --git a/backend/test/pipeline_api_test.go b/backend/test/pipeline_api_test.go index 69a34a57eb8..cf6402e1380 100644 --- a/backend/test/pipeline_api_test.go +++ b/backend/test/pipeline_api_test.go @@ -90,9 +90,10 @@ func (s *PipelineApiTest) TestPipelineAPI() { assert.Equal(t, "arguments.tar.gz", argumentUrlPipeline.Name) /* ---------- Verify list pipeline works ---------- */ - pipelines, _, err := s.pipelineClient.List(params.NewListPipelinesParams()) + pipelines, totalSize, _, err := s.pipelineClient.List(params.NewListPipelinesParams()) assert.Nil(t, err) assert.Equal(t, 4, len(pipelines)) + assert.Equal(t, 4, totalSize) for _, p := range pipelines { // Sampling one of the pipelines and verify the result is expected. if p.Name == "arguments-parameters.yaml" { @@ -101,57 +102,63 @@ func (s *PipelineApiTest) TestPipelineAPI() { } /* ---------- Verify list pipeline sorted by names ---------- */ - listFirstPagePipelines, nextPageToken, err := s.pipelineClient.List( + listFirstPagePipelines, totalSize, nextPageToken, err := s.pipelineClient.List( ¶ms.ListPipelinesParams{PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("name")}) assert.Nil(t, err) assert.Equal(t, 2, len(listFirstPagePipelines)) + assert.Equal(t, 4, totalSize) assert.Equal(t, "arguments-parameters.yaml", listFirstPagePipelines[0].Name) assert.Equal(t, "arguments.tar.gz", listFirstPagePipelines[1].Name) assert.NotEmpty(t, nextPageToken) - listSecondPagePipelines, nextPageToken, err := s.pipelineClient.List( + listSecondPagePipelines, totalSize, nextPageToken, err := s.pipelineClient.List( ¶ms.ListPipelinesParams{PageToken: util.StringPointer(nextPageToken), PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("name")}) assert.Nil(t, err) assert.Equal(t, 2, len(listSecondPagePipelines)) + assert.Equal(t, 4, totalSize) assert.Equal(t, "sequential", listSecondPagePipelines[0].Name) assert.Equal(t, "zip-arguments-parameters", listSecondPagePipelines[1].Name) assert.Empty(t, nextPageToken) /* ---------- Verify list pipeline sorted by creation time ---------- */ - listFirstPagePipelines, nextPageToken, err = s.pipelineClient.List( + listFirstPagePipelines, totalSize, nextPageToken, err = s.pipelineClient.List( ¶ms.ListPipelinesParams{PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("created_at")}) assert.Nil(t, err) assert.Equal(t, 2, len(listFirstPagePipelines)) + assert.Equal(t, 4, totalSize) assert.Equal(t, "arguments-parameters.yaml", listFirstPagePipelines[0].Name) assert.Equal(t, "sequential", listFirstPagePipelines[1].Name) assert.NotEmpty(t, nextPageToken) - listSecondPagePipelines, nextPageToken, err = s.pipelineClient.List( + listSecondPagePipelines, totalSize, nextPageToken, err = s.pipelineClient.List( ¶ms.ListPipelinesParams{PageToken: util.StringPointer(nextPageToken), PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("created_at")}) assert.Nil(t, err) assert.Equal(t, 2, len(listSecondPagePipelines)) + assert.Equal(t, 4, totalSize) assert.Equal(t, "zip-arguments-parameters", listSecondPagePipelines[0].Name) assert.Equal(t, "arguments.tar.gz", listSecondPagePipelines[1].Name) assert.Empty(t, nextPageToken) /* ---------- List pipelines sort by unsupported description field. Should fail. ---------- */ - _, _, err = s.pipelineClient.List(¶ms.ListPipelinesParams{ + _, _, _, err = s.pipelineClient.List(¶ms.ListPipelinesParams{ PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("unknownfield")}) assert.NotNil(t, err) /* ---------- List pipelines sorted by names descend order ---------- */ - listFirstPagePipelines, nextPageToken, err = s.pipelineClient.List( + listFirstPagePipelines, totalSize, nextPageToken, err = s.pipelineClient.List( ¶ms.ListPipelinesParams{PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("name desc")}) assert.Nil(t, err) assert.Equal(t, 2, len(listFirstPagePipelines)) + assert.Equal(t, 4, totalSize) assert.Equal(t, "zip-arguments-parameters", listFirstPagePipelines[0].Name) assert.Equal(t, "sequential", listFirstPagePipelines[1].Name) assert.NotEmpty(t, nextPageToken) - listSecondPagePipelines, nextPageToken, err = s.pipelineClient.List(¶ms.ListPipelinesParams{ + listSecondPagePipelines, totalSize, nextPageToken, err = s.pipelineClient.List(¶ms.ListPipelinesParams{ PageToken: util.StringPointer(nextPageToken), PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("name desc")}) assert.Nil(t, err) assert.Equal(t, 2, len(listSecondPagePipelines)) + assert.Equal(t, 4, totalSize) assert.Equal(t, "arguments.tar.gz", listSecondPagePipelines[0].Name) assert.Equal(t, "arguments-parameters.yaml", listSecondPagePipelines[1].Name) assert.Empty(t, nextPageToken) diff --git a/backend/test/run_api_test.go b/backend/test/run_api_test.go index 35b3f231bf2..a6b7010a648 100644 --- a/backend/test/run_api_test.go +++ b/backend/test/run_api_test.go @@ -121,44 +121,50 @@ func (s *RunApiTestSuite) TestRunApis() { s.checkArgParamsRunDetail(t, argParamsRunDetail, argParamsExperiment.ID) /* ---------- List all the runs. Both runs should be returned ---------- */ - runs, _, err := s.runClient.List(&runparams.ListRunsParams{}) + runs, totalSize, _, err := s.runClient.List(&runparams.ListRunsParams{}) assert.Nil(t, err) assert.Equal(t, 2, len(runs)) + assert.Equal(t, 2, totalSize) /* ---------- List the runs, paginated, default sort ---------- */ - runs, nextPageToken, err := s.runClient.List(&runparams.ListRunsParams{PageSize: util.Int32Pointer(1)}) + runs, totalSize, nextPageToken, err := s.runClient.List(&runparams.ListRunsParams{PageSize: util.Int32Pointer(1)}) assert.Nil(t, err) assert.Equal(t, 1, len(runs)) + assert.Equal(t, 2, totalSize) assert.Equal(t, "hello world", runs[0].Name) - runs, _, err = s.runClient.List(&runparams.ListRunsParams{ + runs, totalSize, _, err = s.runClient.List(&runparams.ListRunsParams{ PageSize: util.Int32Pointer(1), PageToken: util.StringPointer(nextPageToken)}) assert.Nil(t, err) assert.Equal(t, 1, len(runs)) + assert.Equal(t, 2, totalSize) assert.Equal(t, "argument parameter", runs[0].Name) /* ---------- List the runs, paginated, sort by name ---------- */ - runs, nextPageToken, err = s.runClient.List(&runparams.ListRunsParams{ + runs, totalSize, nextPageToken, err = s.runClient.List(&runparams.ListRunsParams{ PageSize: util.Int32Pointer(1), SortBy: util.StringPointer("name")}) assert.Nil(t, err) assert.Equal(t, 1, len(runs)) + assert.Equal(t, 2, totalSize) assert.Equal(t, "argument parameter", runs[0].Name) - runs, _, err = s.runClient.List(&runparams.ListRunsParams{ + runs, totalSize, _, err = s.runClient.List(&runparams.ListRunsParams{ PageSize: util.Int32Pointer(1), SortBy: util.StringPointer("name"), PageToken: util.StringPointer(nextPageToken)}) assert.Nil(t, err) assert.Equal(t, 1, len(runs)) + assert.Equal(t, 2, totalSize) assert.Equal(t, "hello world", runs[0].Name) /* ---------- List the runs, sort by unsupported field ---------- */ - _, _, err = s.runClient.List(&runparams.ListRunsParams{ + _, _, _, err = s.runClient.List(&runparams.ListRunsParams{ PageSize: util.Int32Pointer(2), SortBy: util.StringPointer("unknownfield")}) assert.NotNil(t, err) /* ---------- List runs for hello world experiment. One run should be returned ---------- */ - runs, _, err = s.runClient.List(&runparams.ListRunsParams{ + runs, totalSize, _, err = s.runClient.List(&runparams.ListRunsParams{ ResourceReferenceKeyType: util.StringPointer(string(run_model.APIResourceTypeEXPERIMENT)), ResourceReferenceKeyID: util.StringPointer(helloWorldExperiment.ID)}) assert.Nil(t, err) assert.Equal(t, 1, len(runs)) + assert.Equal(t, 1, totalSize) assert.Equal(t, "hello world", runs[0].Name) /* ---------- Archive a run ------------*/ @@ -167,11 +173,12 @@ func (s *RunApiTestSuite) TestRunApis() { }) /* ---------- List runs for hello world experiment. The same run should still be returned, but should be archived ---------- */ - runs, _, err = s.runClient.List(&runparams.ListRunsParams{ + runs, totalSize, _, err = s.runClient.List(&runparams.ListRunsParams{ ResourceReferenceKeyType: util.StringPointer(string(run_model.APIResourceTypeEXPERIMENT)), ResourceReferenceKeyID: util.StringPointer(helloWorldExperiment.ID)}) assert.Nil(t, err) assert.Equal(t, 1, len(runs)) + assert.Equal(t, 1, totalSize) assert.Equal(t, "hello world", runs[0].Name) assert.Equal(t, string(runs[0].StorageState), api.Run_STORAGESTATE_ARCHIVED.String()) diff --git a/backend/test/test_utils.go b/backend/test/test_utils.go index 4af412f20fc..c5bf875e445 100644 --- a/backend/test/test_utils.go +++ b/backend/test/test_utils.go @@ -72,7 +72,7 @@ func getClientConfig(namespace string) clientcmd.ClientConfig { } func deleteAllPipelines(client *api_server.PipelineClient, t *testing.T) { - pipelines, _, err := client.List(&pipelineparams.ListPipelinesParams{}) + pipelines, _, _, err := client.List(&pipelineparams.ListPipelinesParams{}) assert.Nil(t, err) for _, p := range pipelines { assert.Nil(t, client.Delete(&pipelineparams.DeletePipelineParams{ID: p.ID})) @@ -80,7 +80,7 @@ func deleteAllPipelines(client *api_server.PipelineClient, t *testing.T) { } func deleteAllExperiments(client *api_server.ExperimentClient, t *testing.T) { - experiments, _, err := client.List(&experimentparams.ListExperimentParams{}) + experiments, _, _, err := client.List(&experimentparams.ListExperimentParams{}) assert.Nil(t, err) for _, e := range experiments { assert.Nil(t, client.Delete(&experimentparams.DeleteExperimentParams{ID: e.ID})) @@ -88,7 +88,7 @@ func deleteAllExperiments(client *api_server.ExperimentClient, t *testing.T) { } func deleteAllRuns(client *api_server.RunClient, t *testing.T) { - runs, _, err := client.List(&runparams.ListRunsParams{}) + runs, _, _, err := client.List(&runparams.ListRunsParams{}) assert.Nil(t, err) for _, r := range runs { assert.Nil(t, client.Delete(&runparams.DeleteRunParams{ID: r.ID})) @@ -96,7 +96,7 @@ func deleteAllRuns(client *api_server.RunClient, t *testing.T) { } func deleteAllJobs(client *api_server.JobClient, t *testing.T) { - jobs, _, err := client.List(&jobparams.ListJobsParams{}) + jobs, _, _, err := client.List(&jobparams.ListJobsParams{}) assert.Nil(t, err) for _, j := range jobs { assert.Nil(t, client.Delete(&jobparams.DeleteJobParams{ID: j.ID})) From 423f33c4bdd72894bb44457d7915cf179cb2abbd Mon Sep 17 00:00:00 2001 From: Riley Bauer <34456002+rileyjbauer@users.noreply.github.com> Date: Thu, 31 Jan 2019 14:48:27 -0800 Subject: [PATCH 3/4] Regenerate frontend API files now that listCount APIs are merged (#757) * Regenerate frontend swagger-generated API files now that listCount APIs are merged * Manually remove 'url' import from filter/api.ts --- frontend/src/apis/experiment/api.ts | 6 ++++ frontend/src/apis/filter/api.ts | 53 ++++++++++++++--------------- frontend/src/apis/job/api.ts | 6 ++++ frontend/src/apis/pipeline/api.ts | 6 ++++ frontend/src/apis/run/api.ts | 6 ++++ 5 files changed, 50 insertions(+), 27 deletions(-) diff --git a/frontend/src/apis/experiment/api.ts b/frontend/src/apis/experiment/api.ts index 4cd629e4251..9024ba5e42d 100644 --- a/frontend/src/apis/experiment/api.ts +++ b/frontend/src/apis/experiment/api.ts @@ -122,6 +122,12 @@ export interface ApiListExperimentsResponse { * @memberof ApiListExperimentsResponse */ experiments?: Array; + /** + * + * @type {number} + * @memberof ApiListExperimentsResponse + */ + total_size?: number; /** * * @type {string} diff --git a/frontend/src/apis/filter/api.ts b/frontend/src/apis/filter/api.ts index 39a5a32faa8..e324b0ab50d 100644 --- a/frontend/src/apis/filter/api.ts +++ b/frontend/src/apis/filter/api.ts @@ -5,14 +5,13 @@ * No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen) * * OpenAPI spec version: version not set - * + * * * NOTE: This class is auto generated by the swagger code generator program. * https://github.com/swagger-api/swagger-codegen.git * Do not edit the class manually. */ - import * as portableFetch from "portable-fetch"; import { Configuration } from "./configuration"; @@ -39,7 +38,7 @@ export interface FetchAPI { } /** - * + * * @export * @interface FetchArgs */ @@ -49,7 +48,7 @@ export interface FetchArgs { } /** - * + * * @export * @class BaseAPI */ @@ -65,7 +64,7 @@ export class BaseAPI { }; /** - * + * * @export * @class RequiredError * @extends {Error} @@ -92,13 +91,13 @@ export interface ApiFilter { } /** - * + * * @export * @interface ApiIntValues */ export interface ApiIntValues { /** - * + * * @type {Array<number>} * @memberof ApiIntValues */ @@ -106,13 +105,13 @@ export interface ApiIntValues { } /** - * + * * @export * @interface ApiLongValues */ export interface ApiLongValues { /** - * + * * @type {Array<string>} * @memberof ApiLongValues */ @@ -126,31 +125,31 @@ export interface ApiLongValues { */ export interface ApiPredicate { /** - * + * * @type {PredicateOp} * @memberof ApiPredicate */ op?: PredicateOp; /** - * + * * @type {string} * @memberof ApiPredicate */ key?: string; /** - * + * * @type {number} * @memberof ApiPredicate */ int_value?: number; /** - * + * * @type {string} * @memberof ApiPredicate */ long_value?: string; /** - * + * * @type {string} * @memberof ApiPredicate */ @@ -168,13 +167,13 @@ export interface ApiPredicate { */ int_values?: ApiIntValues; /** - * + * * @type {ApiLongValues} * @memberof ApiPredicate */ long_values?: ApiLongValues; /** - * + * * @type {ApiStringValues} * @memberof ApiPredicate */ @@ -182,13 +181,13 @@ export interface ApiPredicate { } /** - * + * * @export * @interface ApiStringValues */ export interface ApiStringValues { /** - * + * * @type {Array<string>} * @memberof ApiStringValues */ @@ -201,15 +200,15 @@ export interface ApiStringValues { * @enum {string} */ export enum PredicateOp { - UNKNOWN = 'UNKNOWN', - EQUALS = 'EQUALS', - NOTEQUALS = 'NOT_EQUALS', - GREATERTHAN = 'GREATER_THAN', - GREATERTHANEQUALS = 'GREATER_THAN_EQUALS', - LESSTHAN = 'LESS_THAN', - LESSTHANEQUALS = 'LESS_THAN_EQUALS', - IN = 'IN', - ISSUBSTRING = 'IS_SUBSTRING' + UNKNOWN = 'UNKNOWN', + EQUALS = 'EQUALS', + NOTEQUALS = 'NOT_EQUALS', + GREATERTHAN = 'GREATER_THAN', + GREATERTHANEQUALS = 'GREATER_THAN_EQUALS', + LESSTHAN = 'LESS_THAN', + LESSTHANEQUALS = 'LESS_THAN_EQUALS', + IN = 'IN', + ISSUBSTRING = 'IS_SUBSTRING' } diff --git a/frontend/src/apis/job/api.ts b/frontend/src/apis/job/api.ts index 157b4f7d686..c8481583e8f 100644 --- a/frontend/src/apis/job/api.ts +++ b/frontend/src/apis/job/api.ts @@ -202,6 +202,12 @@ export interface ApiListJobsResponse { * @memberof ApiListJobsResponse */ jobs?: Array; + /** + * + * @type {number} + * @memberof ApiListJobsResponse + */ + total_size?: number; /** * * @type {string} diff --git a/frontend/src/apis/pipeline/api.ts b/frontend/src/apis/pipeline/api.ts index 2d67465dbfe..2d2ba1b78d4 100644 --- a/frontend/src/apis/pipeline/api.ts +++ b/frontend/src/apis/pipeline/api.ts @@ -104,6 +104,12 @@ export interface ApiListPipelinesResponse { * @memberof ApiListPipelinesResponse */ pipelines?: Array; + /** + * + * @type {number} + * @memberof ApiListPipelinesResponse + */ + total_size?: number; /** * * @type {string} diff --git a/frontend/src/apis/run/api.ts b/frontend/src/apis/run/api.ts index 67c7e73aee6..e045a48669d 100644 --- a/frontend/src/apis/run/api.ts +++ b/frontend/src/apis/run/api.ts @@ -90,6 +90,12 @@ export interface ApiListRunsResponse { * @memberof ApiListRunsResponse */ runs?: Array; + /** + * + * @type {number} + * @memberof ApiListRunsResponse + */ + total_size?: number; /** * * @type {string} From e29b1819b0c5ac36ecf229129d09161d59d28e9b Mon Sep 17 00:00:00 2001 From: Yasser Elsayed Date: Thu, 31 Jan 2019 15:46:03 -0800 Subject: [PATCH 4/4] Add UI actions to Buttons module (#758) * add actions to Buttons module * pr comments --- frontend/src/TestUtils.tsx | 25 ++ frontend/src/lib/Buttons.ts | 410 +++++++++++++----- frontend/src/pages/AllRunsList.tsx | 36 +- frontend/src/pages/Compare.test.tsx | 20 +- frontend/src/pages/Compare.tsx | 5 +- frontend/src/pages/ExperimentDetails.test.tsx | 17 +- frontend/src/pages/ExperimentDetails.tsx | 90 +--- frontend/src/pages/ExperimentList.test.tsx | 26 +- frontend/src/pages/ExperimentList.tsx | 52 +-- frontend/src/pages/PipelineDetails.test.tsx | 31 +- frontend/src/pages/PipelineDetails.tsx | 65 +-- frontend/src/pages/PipelineList.test.tsx | 22 +- frontend/src/pages/PipelineList.tsx | 57 +-- .../src/pages/RecurringRunDetails.test.tsx | 60 +-- frontend/src/pages/RecurringRunDetails.tsx | 73 +--- frontend/src/pages/RunDetails.tsx | 27 +- .../pages/__snapshots__/Compare.test.tsx.snap | 48 +- .../ExperimentDetails.test.tsx.snap | 56 +-- 18 files changed, 516 insertions(+), 604 deletions(-) diff --git a/frontend/src/TestUtils.tsx b/frontend/src/TestUtils.tsx index 6e9a3ee756d..bb5d9942b5e 100644 --- a/frontend/src/TestUtils.tsx +++ b/frontend/src/TestUtils.tsx @@ -17,8 +17,10 @@ import * as React from 'react'; // @ts-ignore import createRouterContext from 'react-router-test-context'; +import { PageProps, Page } from './pages/Page'; import { mount, ReactWrapper } from 'enzyme'; import { object } from 'prop-types'; +import { match } from 'react-router'; export default class TestUtils { /** @@ -54,4 +56,27 @@ export default class TestUtils { }; }); } + + // tslint:disable-next-line:variable-name + public static generatePageProps(PageElement: new (x: PageProps) => Page, + location: Location, matchValue: match, + historyPushSpy: jest.SpyInstance | null, updateBannerSpy: jest.SpyInstance, + updateDialogSpy: jest.SpyInstance, updateToolbarSpy: jest.SpyInstance, + updateSnackbarSpy: jest.SpyInstance): PageProps { + const pageProps = { + history: { push: historyPushSpy } as any, + location: location as any, + match: matchValue, + toolbarProps: { actions: [], breadcrumbs: [], pageTitle: '' }, + updateBanner: updateBannerSpy as any, + updateDialog: updateDialogSpy as any, + updateSnackbar: updateSnackbarSpy as any, + updateToolbar: updateToolbarSpy as any, + } as PageProps; + pageProps.toolbarProps = new PageElement(pageProps).getInitialToolbarState(); + // The toolbar spy gets called in the getInitialToolbarState method, reset it + // in order to simplify tests + updateToolbarSpy.mockReset(); + return pageProps; + } } diff --git a/frontend/src/lib/Buttons.ts b/frontend/src/lib/Buttons.ts index 90e2cd498ee..b136309037e 100644 --- a/frontend/src/lib/Buttons.ts +++ b/frontend/src/lib/Buttons.ts @@ -18,112 +18,306 @@ import AddIcon from '@material-ui/icons/Add'; import CollapseIcon from '@material-ui/icons/UnfoldLess'; import ExpandIcon from '@material-ui/icons/UnfoldMore'; import { ToolbarActionConfig } from '../components/Toolbar'; +import { PageProps } from '../pages/Page'; +import { URLParser } from './URLParser'; +import { RoutePage, QUERY_PARAMS } from '../components/Router'; +import { Apis } from './Apis'; +import { errorToMessage, s } from './Utils'; -interface ButtonsMap { [key: string]: (action: () => void) => ToolbarActionConfig; } - -// tslint:disable-next-line:variable-name -const Buttons: ButtonsMap = { - archive: action => ({ - action, - disabled: true, - disabledTitle: 'Select at least one resource to archive', - id: 'archiveBtn', - title: 'Archive', - tooltip: 'Archive', - }), - cloneRun: action => ({ - action, - disabled: true, - disabledTitle: 'Select a run to clone', - id: 'cloneBtn', - title: 'Clone run', - tooltip: 'Create a copy from this run\s initial state', - }), - collapseSections: action => ({ - action, - icon: CollapseIcon, - id: 'collapseBtn', - title: 'Collapse all', - tooltip: 'Collapse all sections', - }), - compareRuns: action => ({ - action, - disabled: true, - disabledTitle: 'Select multiple runs to compare', - id: 'compareBtn', - title: 'Compare runs', - tooltip: 'Compare up to 10 selected runs', - }), - delete: action => ({ - action, - disabled: true, - disabledTitle: 'Select at least one resource to delete', - id: 'deleteBtn', - title: 'Delete', - tooltip: 'Delete', - }), - disableRun: action => ({ - action, - disabled: true, - disabledTitle: 'Run schedule already disabled', - id: 'disableBtn', - title: 'Disable', - tooltip: 'Disable the run\'s trigger', - }), - enableRun: action => ({ - action, - disabled: true, - disabledTitle: 'Run schedule already enabled', - id: 'enableBtn', - title: 'Enable', - tooltip: 'Enable the run\'s trigger', - }), - expandSections: action => ({ - action, - icon: ExpandIcon, - id: 'expandBtn', - title: 'Expand all', - tooltip: 'Expand all sections', - }), - newExperiment: action => ({ - action, - icon: AddIcon, - id: 'newExperimentBtn', - outlined: true, - title: 'Create an experiment', - tooltip: 'Create a new experiment', - }), - newRun: action => ({ - action, - icon: AddIcon, - id: 'createNewRunBtn', - outlined: true, - primary: true, - title: 'Create run', - tooltip: 'Create a new run within this pipeline', - }), - refresh: action => ({ - action, - id: 'refreshBtn', - title: 'Refresh', - tooltip: 'Refresh the list', - }), - restore: action => ({ - action, - disabled: true, - disabledTitle: 'Select at least one resource to restore', - id: 'archiveBtn', - title: 'Archive', - tooltip: 'Archive', - }), - upload: action => ({ - action, - icon: AddIcon, - id: 'uploadBtn', - outlined: true, - title: 'Upload pipeline', - tooltip: 'Upload pipeline', - }), -}; - -export default Buttons; +export default class Buttons { + private _props: PageProps; + private _refresh: () => void; + private _urlParser: URLParser; + + constructor(pageProps: PageProps, refresh: () => void) { + this._props = pageProps; + this._refresh = refresh; + this._urlParser = new URLParser(pageProps); + } + + public cloneRun(getSelectedIds: () => string[], useCurrentResource: boolean): ToolbarActionConfig { + return { + action: () => this._cloneRun(getSelectedIds()), + disabled: !useCurrentResource, + disabledTitle: useCurrentResource ? undefined : 'Select a run to clone', + id: 'cloneBtn', + title: 'Clone run', + tooltip: 'Create a copy from this run\s initial state', + }; + } + + public collapseSections(action: () => void): ToolbarActionConfig { + return { + action, + icon: CollapseIcon, + id: 'collapseBtn', + title: 'Collapse all', + tooltip: 'Collapse all sections', + }; + } + + public compareRuns(getSelectedIds: () => string[]): ToolbarActionConfig { + return { + action: () => this._compareRuns(getSelectedIds()), + disabled: true, + disabledTitle: 'Select multiple runs to compare', + id: 'compareBtn', + title: 'Compare runs', + tooltip: 'Compare up to 10 selected runs', + }; + } + + public delete(getSelectedIds: () => string[], resourceName: 'pipeline' | 'recurring run config', + callback: (selectedIds: string[], success: boolean) => void, useCurrentResource: boolean): ToolbarActionConfig { + return { + action: () => resourceName === 'pipeline' ? + this._deletePipeline(getSelectedIds(), callback, useCurrentResource) : + this._deleteRecurringRun(getSelectedIds()[0], useCurrentResource, callback), + disabled: !useCurrentResource, + disabledTitle: useCurrentResource ? undefined : `Select at least one ${resourceName} to delete`, + id: 'deleteBtn', + title: 'Delete', + tooltip: 'Delete', + }; + } + + public disableRecurringRun(getId: () => string): ToolbarActionConfig { + return { + action: () => this._setRecurringRunEnabledState(getId(), false), + disabled: true, + disabledTitle: 'Run schedule already disabled', + id: 'disableBtn', + title: 'Disable', + tooltip: 'Disable the run\'s trigger', + }; + } + + public enableRecurringRun(getId: () => string): ToolbarActionConfig { + return { + action: () => this._setRecurringRunEnabledState(getId(), true), + disabled: true, + disabledTitle: 'Run schedule already enabled', + id: 'enableBtn', + title: 'Enable', + tooltip: 'Enable the run\'s trigger', + }; + } + + public expandSections(action: () => void): ToolbarActionConfig { + return { + action, + icon: ExpandIcon, + id: 'expandBtn', + title: 'Expand all', + tooltip: 'Expand all sections', + }; + } + + public newExperiment(getPipelineId?: () => string): ToolbarActionConfig { + return { + action: () => this._createNewExperiment(getPipelineId ? getPipelineId() : ''), + icon: AddIcon, + id: 'newExperimentBtn', + outlined: true, + title: 'Create an experiment', + tooltip: 'Create a new experiment', + }; + } + + public newRun(getExperimentId?: () => string): ToolbarActionConfig { + return { + action: () => this._createNewRun(false, getExperimentId ? getExperimentId() : undefined), + icon: AddIcon, + id: 'createNewRunBtn', + outlined: true, + primary: true, + title: 'Create run', + tooltip: 'Create a new run', + }; + } + + public newRunFromPipeline(getPipelineId: () => string): ToolbarActionConfig { + return { + action: () => this._createNewRunFromPipeline(getPipelineId()), + icon: AddIcon, + id: 'createNewRunBtn', + outlined: true, + primary: true, + title: 'Create run', + tooltip: 'Create a new run', + }; + } + + public newRecurringRun(experimentId: string): ToolbarActionConfig { + return { + action: () => this._createNewRun(true, experimentId), + icon: AddIcon, + id: 'createNewRecurringRunBtn', + outlined: true, + title: 'Create recurring run', + tooltip: 'Create a new recurring run', + }; + } + + public refresh(action: () => void): ToolbarActionConfig { + return { + action, + id: 'refreshBtn', + title: 'Refresh', + tooltip: 'Refresh the list', + }; + } + + public upload(action: () => void): ToolbarActionConfig { + return { + action, + icon: AddIcon, + id: 'uploadBtn', + outlined: true, + title: 'Upload pipeline', + tooltip: 'Upload pipeline', + }; + } + + private _cloneRun(selectedIds: string[]): void { + if (selectedIds.length === 1) { + const runId = selectedIds[0]; + const searchString = this._urlParser.build({ [QUERY_PARAMS.cloneFromRun]: runId || '' }); + this._props.history.push(RoutePage.NEW_RUN + searchString); + } + } + + private _deletePipeline(selectedIds: string[], callback: (selectedIds: string[], success: boolean) => void, + useCurrentResource: boolean): void { + this._dialogActionHandler(selectedIds, useCurrentResource, + id => Apis.pipelineServiceApi.deletePipeline(id), callback, 'Delete', 'pipeline'); + } + + private _deleteRecurringRun(id: string, useCurrentResource: boolean, + callback: (_: string[], success: boolean) => void): void { + this._dialogActionHandler([id], useCurrentResource, Apis.jobServiceApi.deleteJob, callback, 'Delete', + 'recurring run config'); + } + + private _dialogActionHandler(selectedIds: string[], useCurrentResource: boolean, + api: (id: string) => Promise, callback: (selectedIds: string[], success: boolean) => void, + actionName: string, resourceName: string): void { + + const dialogClosedHandler = (confirmed: boolean) => + this._dialogClosed(confirmed, selectedIds, actionName, resourceName, useCurrentResource, api, callback); + + this._props.updateDialog({ + buttons: [{ + onClick: async () => await dialogClosedHandler(true), + text: actionName, + }, { + onClick: async () => await dialogClosedHandler(false), + text: 'Cancel', + }], + onClose: async () => await dialogClosedHandler(false), + title: `${actionName} ${useCurrentResource ? 'this' : selectedIds.length} ${resourceName}${useCurrentResource ? '' : s(selectedIds.length)}?`, + }); + } + + private async _dialogClosed(confirmed: boolean, selectedIds: string[], actionName: string, + resourceName: string, useCurrentResource: boolean, api: (id: string) => Promise, + callback: (selectedIds: string[], success: boolean) => void): Promise { + if (confirmed) { + const unsuccessfulIds: string[] = []; + const errorMessages: string[] = []; + await Promise.all(selectedIds.map(async (id) => { + try { + await api(id); + } catch (err) { + unsuccessfulIds.push(id); + const errorMessage = await errorToMessage(err); + errorMessages.push(`Failed to ${actionName.toLowerCase()} ${resourceName}: ${id} with error: "${errorMessage}"`); + } + })); + + const successfulOps = selectedIds.length - unsuccessfulIds.length; + if (useCurrentResource || successfulOps > 0) { + this._props.updateSnackbar({ + message: `${actionName} succeeded for ${useCurrentResource ? 'this' : successfulOps} ${resourceName}${useCurrentResource ? '' : s(successfulOps)}`, + open: true, + }); + this._refresh(); + } + + if (unsuccessfulIds.length > 0) { + this._props.updateDialog({ + buttons: [{ text: 'Dismiss' }], + content: errorMessages.join('\n\n'), + title: `Failed to ${actionName.toLowerCase()} ${useCurrentResource ? '' : unsuccessfulIds.length + ' '}${resourceName}${useCurrentResource ? '' : s(unsuccessfulIds)}`, + }); + } + + callback(unsuccessfulIds, !unsuccessfulIds.length); + } + } + private _compareRuns(selectedIds: string[]): void { + const indices = selectedIds; + if (indices.length > 1 && indices.length <= 10) { + const runIds = selectedIds.join(','); + const searchString = this._urlParser.build({ [QUERY_PARAMS.runlist]: runIds }); + this._props.history.push(RoutePage.COMPARE + searchString); + } + } + + private _createNewExperiment(pipelineId: string): void { + const searchString = pipelineId ? this._urlParser.build({ + [QUERY_PARAMS.pipelineId]: pipelineId + }) : ''; + this._props.history.push(RoutePage.NEW_EXPERIMENT + searchString); + } + + private _createNewRun(isRecurring: boolean, experimentId?: string): void { + const searchString = this._urlParser.build(Object.assign( + { [QUERY_PARAMS.experimentId]: experimentId || '' }, + isRecurring ? { [QUERY_PARAMS.isRecurring]: '1' } : {})); + this._props.history.push(RoutePage.NEW_RUN + searchString); + } + + private _createNewRunFromPipeline(pipelineId?: string): void { + let searchString = ''; + const fromRunId = this._urlParser.get(QUERY_PARAMS.fromRunId); + + if (fromRunId) { + searchString = this._urlParser.build(Object.assign( + { [QUERY_PARAMS.fromRunId]: fromRunId } + )); + } else { + searchString = this._urlParser.build(Object.assign( + { [QUERY_PARAMS.pipelineId]: pipelineId || '' } + )); + } + + this._props.history.push(RoutePage.NEW_RUN + searchString); + } + + private async _setRecurringRunEnabledState(id: string, enabled: boolean): Promise { + if (id) { + const toolbarActions = [...this._props.toolbarProps.actions]; + + const buttonIndex = enabled ? 1 : 2; + + toolbarActions[buttonIndex].busy = true; + this._props.updateToolbar({ actions: toolbarActions }); + try { + await (enabled ? Apis.jobServiceApi.enableJob(id) : Apis.jobServiceApi.disableJob(id)); + this._refresh(); + } catch (err) { + const errorMessage = await errorToMessage(err); + this._props.updateDialog({ + buttons: [{ text: 'Dismiss' }], + content: errorMessage, + title: `Failed to ${enabled ? 'enable' : 'disable'} recurring run`, + }); + } finally { + toolbarActions[buttonIndex].busy = false; + this._props.updateToolbar({ actions: toolbarActions }); + } + } + } + +} diff --git a/frontend/src/pages/AllRunsList.tsx b/frontend/src/pages/AllRunsList.tsx index f42fa6e74e0..1db2d2074f6 100644 --- a/frontend/src/pages/AllRunsList.tsx +++ b/frontend/src/pages/AllRunsList.tsx @@ -18,9 +18,7 @@ import * as React from 'react'; import Buttons from '../lib/Buttons'; import RunList from './RunList'; import { Page } from './Page'; -import { RoutePage, QUERY_PARAMS } from '../components/Router'; import { ToolbarProps } from '../components/Toolbar'; -import { URLParser } from '../lib/URLParser'; import { classes } from 'typestyle'; import { commonCss, padding } from '../Css'; @@ -41,12 +39,13 @@ class AllRunsList extends Page<{}, AllRunsListState> { } public getInitialToolbarState(): ToolbarProps { + const buttons = new Buttons(this.props, this.refresh.bind(this)); return { actions: [ - Buttons.newExperiment(this._newExperimentClicked.bind(this)), - Buttons.compareRuns(this._compareRuns.bind(this)), - Buttons.cloneRun(this._cloneRun.bind(this)), - Buttons.refresh(this.refresh.bind(this)), + buttons.newExperiment(), + buttons.compareRuns(() => this.state.selectedIds), + buttons.cloneRun(() => this.state.selectedIds, false), + buttons.refresh(this.refresh.bind(this)), ], breadcrumbs: [], pageTitle: 'Experiments', @@ -69,21 +68,6 @@ class AllRunsList extends Page<{}, AllRunsListState> { } } - private _newExperimentClicked(): void { - this.props.history.push(RoutePage.NEW_EXPERIMENT); - } - - private _compareRuns(): void { - const indices = this.state.selectedIds; - if (indices.length > 1 && indices.length <= 10) { - const runIds = this.state.selectedIds.join(','); - const searchString = new URLParser(this.props).build({ - [QUERY_PARAMS.runlist]: runIds, - }); - this.props.history.push(RoutePage.COMPARE + searchString); - } - } - private _selectionChanged(selectedIds: string[]): void { const toolbarActions = [...this.props.toolbarProps.actions]; // Compare runs button @@ -93,16 +77,6 @@ class AllRunsList extends Page<{}, AllRunsListState> { this.props.updateToolbar({ breadcrumbs: this.props.toolbarProps.breadcrumbs, actions: toolbarActions }); this.setState({ selectedIds }); } - - private _cloneRun(): void { - if (this.state.selectedIds.length === 1) { - const runId = this.state.selectedIds[0]; - const searchString = new URLParser(this.props).build({ - [QUERY_PARAMS.cloneFromRun]: runId || '' - }); - this.props.history.push(RoutePage.NEW_RUN + searchString); - } - } } export default AllRunsList; diff --git a/frontend/src/pages/Compare.test.tsx b/frontend/src/pages/Compare.test.tsx index be9f965f28c..58058cab157 100644 --- a/frontend/src/pages/Compare.test.tsx +++ b/frontend/src/pages/Compare.test.tsx @@ -46,20 +46,14 @@ describe('Compare', () => { const getRunSpy = jest.spyOn(Apis.runServiceApi, 'getRun'); const outputArtifactLoaderSpy = jest.spyOn(OutputArtifactLoader, 'load'); + function generateProps(): PageProps { - return { - history: { push: historyPushSpy } as any, - location: { - pathname: RoutePage.COMPARE, - search: `?${QUERY_PARAMS.runlist}=${MOCK_RUN_1_ID},${MOCK_RUN_2_ID},${MOCK_RUN_3_ID}` - } as any, - match: { params: {} } as any, - toolbarProps: Compare.prototype.getInitialToolbarState(), - updateBanner: updateBannerSpy, - updateDialog: updateDialogSpy, - updateSnackbar: updateSnackbarSpy, - updateToolbar: updateToolbarSpy, - }; + const location = { + pathname: RoutePage.COMPARE, + search: `?${QUERY_PARAMS.runlist}=${MOCK_RUN_1_ID},${MOCK_RUN_2_ID},${MOCK_RUN_3_ID}` + } as any; + return TestUtils.generatePageProps(Compare, location, {} as any, historyPushSpy, + updateBannerSpy, updateDialogSpy, updateToolbarSpy, updateSnackbarSpy); } const MOCK_RUN_1_ID = 'mock-run-1-id'; diff --git a/frontend/src/pages/Compare.tsx b/frontend/src/pages/Compare.tsx index 0e0386d4310..efbc7425f30 100644 --- a/frontend/src/pages/Compare.tsx +++ b/frontend/src/pages/Compare.tsx @@ -81,10 +81,11 @@ class Compare extends Page<{}, CompareState> { } public getInitialToolbarState(): ToolbarProps { + const buttons = new Buttons(this.props, this.refresh.bind(this)); return { actions: [ - Buttons.expandSections(() => this.setState({ collapseSections: {} })), - Buttons.collapseSections(this._collapseAllSections.bind(this)), + buttons.expandSections(() => this.setState({ collapseSections: {} })), + buttons.collapseSections(this._collapseAllSections.bind(this)), ], breadcrumbs: [{ displayName: 'Experiments', href: RoutePage.EXPERIMENTS }], pageTitle: 'Compare runs', diff --git a/frontend/src/pages/ExperimentDetails.test.tsx b/frontend/src/pages/ExperimentDetails.test.tsx index 570595a85f2..23fd4a9b688 100644 --- a/frontend/src/pages/ExperimentDetails.test.tsx +++ b/frontend/src/pages/ExperimentDetails.test.tsx @@ -53,16 +53,9 @@ describe('ExperimentDetails', () => { } function generateProps(): PageProps { - return { - history: { push: historyPushSpy } as any, - location: '' as any, - match: { params: { [RouteParams.experimentId]: MOCK_EXPERIMENT.id } } as any, - toolbarProps: ExperimentDetails.prototype.getInitialToolbarState(), - updateBanner: updateBannerSpy, - updateDialog: updateDialogSpy, - updateSnackbar: updateSnackbarSpy, - updateToolbar: updateToolbarSpy, - }; + const match = { params: { [RouteParams.experimentId]: MOCK_EXPERIMENT.id } } as any; + return TestUtils.generatePageProps(ExperimentDetails, {} as any, match, historyPushSpy, + updateBannerSpy, updateDialogSpy, updateToolbarSpy, updateSnackbarSpy); } async function mockNJobs(n: number): Promise { @@ -392,7 +385,7 @@ describe('ExperimentDetails', () => { tree.find('.tableRow').simulate('click'); const cloneBtn = (tree.state('runListToolbarProps') as ToolbarProps) - .actions.find(b => b.title === 'Clone'); + .actions.find(b => b.title === 'Clone run'); await cloneBtn!.action(); expect(historyPushSpy).toHaveBeenCalledWith( @@ -427,7 +420,7 @@ describe('ExperimentDetails', () => { tree.update(); const cloneBtn = (tree.state('runListToolbarProps') as ToolbarProps) - .actions.find(b => b.title === 'Clone'); + .actions.find(b => b.title === 'Clone run'); for (let i = 0; i < 4; i++) { if (i === 1) { diff --git a/frontend/src/pages/ExperimentDetails.tsx b/frontend/src/pages/ExperimentDetails.tsx index be3e4c5894d..568a881a5d5 100644 --- a/frontend/src/pages/ExperimentDetails.tsx +++ b/frontend/src/pages/ExperimentDetails.tsx @@ -15,7 +15,6 @@ */ import * as React from 'react'; -import AddIcon from '@material-ui/icons/Add'; import Button from '@material-ui/core/Button'; import Buttons from '../lib/Buttons'; import Dialog from '@material-ui/core/Dialog'; @@ -25,14 +24,13 @@ import Paper from '@material-ui/core/Paper'; import PopOutIcon from '@material-ui/icons/Launch'; import RecurringRunsManager from './RecurringRunsManager'; import RunList from '../pages/RunList'; -import Toolbar, { ToolbarActionConfig, ToolbarProps } from '../components/Toolbar'; +import Toolbar, { ToolbarProps } from '../components/Toolbar'; import Tooltip from '@material-ui/core/Tooltip'; import { ApiExperiment } from '../apis/experiment'; import { ApiResourceType } from '../apis/job'; import { Apis } from '../lib/Apis'; import { Page } from './Page'; -import { RoutePage, RouteParams, QUERY_PARAMS } from '../components/Router'; -import { URLParser } from '../lib/URLParser'; +import { RoutePage, RouteParams } from '../components/Router'; import { classes, stylesheet } from 'typestyle'; import { color, commonCss, padding } from '../Css'; import { logger } from '../lib/Utils'; @@ -103,7 +101,7 @@ interface ExperimentDetailsState { activeRecurringRunsCount: number; experiment: ApiExperiment | null; recurringRunsManagerOpen: boolean; - selectedRunIds: string[]; + selectedIds: string[]; selectedTab: number; runListToolbarProps: ToolbarProps; } @@ -112,59 +110,35 @@ class ExperimentDetails extends Page<{}, ExperimentDetailsState> { private _runlistRef = React.createRef(); - private _runListToolbarActions: ToolbarActionConfig[] = [{ - action: () => this._createNewRun(false), - icon: AddIcon, - id: 'createNewRunBtn', - outlined: true, - primary: true, - title: 'Create run', - tooltip: 'Create a new run within this experiment', - }, { - action: () => this._createNewRun(true), - icon: AddIcon, - id: 'createNewRecurringRunBtn', - outlined: true, - title: 'Create recurring run', - tooltip: 'Create a new recurring run in this experiment', - }, { - action: this._compareRuns.bind(this), - disabled: true, - disabledTitle: 'Select multiple runs to compare', - id: 'compareBtn', - title: 'Compare runs', - tooltip: 'Compare up to 10 selected runs', - }, { - action: this._cloneRun.bind(this), - disabled: true, - disabledTitle: 'Select a run to clone', - id: 'cloneBtn', - title: 'Clone', - tooltip: 'Create a copy from this run\s initial state', - }]; - 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: this._runListToolbarActions, + actions: [ + buttons.newRun(() => this.props.match.params[RouteParams.experimentId]), + buttons.newRecurringRun(this.props.match.params[RouteParams.experimentId]), + buttons.compareRuns(() => this.state.selectedIds), + buttons.cloneRun(() => this.state.selectedIds, false), + ], breadcrumbs: [], pageTitle: 'Runs', topLevelToolbar: false, }, // TODO: remove - selectedRunIds: [], + selectedIds: [], selectedTab: 0, }; } public getInitialToolbarState(): ToolbarProps { + const buttons = new Buttons(this.props, this.refresh.bind(this)); return { - actions: [Buttons.refresh(this.refresh.bind(this))], + actions: [buttons.refresh(this.refresh.bind(this))], breadcrumbs: [{ displayName: 'Experiments', href: RoutePage.EXPERIMENTS }], // TODO: determine what to show if no props. pageTitle: this.props ? this.props.match.params[RouteParams.experimentId] : '', @@ -218,7 +192,7 @@ class ExperimentDetails extends Page<{}, ExperimentDetailsState> { { } } - private _compareRuns(): void { - const indices = this.state.selectedRunIds; - if (indices.length > 1 && indices.length <= 10) { - const runIds = this.state.selectedRunIds.join(','); - const searchString = new URLParser(this.props).build({ - [QUERY_PARAMS.runlist]: runIds, - }); - this.props.history.push(RoutePage.COMPARE + searchString); - } - } - - private _createNewRun(isRecurring: boolean): void { - const searchString = new URLParser(this.props).build(Object.assign( - { [QUERY_PARAMS.experimentId]: this.state.experiment!.id || '', }, - isRecurring ? { [QUERY_PARAMS.isRecurring]: '1' } : {})); - this.props.history.push(RoutePage.NEW_RUN + searchString); - } - - private _cloneRun(): void { - if (this.state.selectedRunIds.length === 1) { - const runId = this.state.selectedRunIds[0]; - const searchString = new URLParser(this.props).build({ - [QUERY_PARAMS.cloneFromRun]: runId || '' - }); - this.props.history.push(RoutePage.NEW_RUN + searchString); - } - } - - private _selectionChanged(selectedRunIds: string[]): void { + private _selectionChanged(selectedIds: string[]): void { const toolbarActions = [...this.state.runListToolbarProps.actions]; // Compare runs button - toolbarActions[2].disabled = selectedRunIds.length <= 1 || selectedRunIds.length > 10; + toolbarActions[2].disabled = selectedIds.length <= 1 || selectedIds.length > 10; // Clone run button - toolbarActions[3].disabled = selectedRunIds.length !== 1; + toolbarActions[3].disabled = selectedIds.length !== 1; this.setState({ runListToolbarProps: { actions: toolbarActions, @@ -339,7 +285,7 @@ class ExperimentDetails extends Page<{}, ExperimentDetailsState> { pageTitle: this.state.runListToolbarProps.pageTitle, topLevelToolbar: this.state.runListToolbarProps.topLevelToolbar, }, - selectedRunIds + selectedIds }); } diff --git a/frontend/src/pages/ExperimentList.test.tsx b/frontend/src/pages/ExperimentList.test.tsx index 06c553f8fb9..f716a127f77 100644 --- a/frontend/src/pages/ExperimentList.test.tsx +++ b/frontend/src/pages/ExperimentList.test.tsx @@ -40,16 +40,8 @@ describe('ExperimentList', () => { const formatDateStringSpy = jest.spyOn(Utils, 'formatDateString'); function generateProps(): PageProps { - return { - history: { push: historyPushSpy } as any, - location: '' as any, - match: '' as any, - toolbarProps: ExperimentList.prototype.getInitialToolbarState(), - updateBanner: updateBannerSpy, - updateDialog: updateDialogSpy, - updateSnackbar: updateSnackbarSpy, - updateToolbar: updateToolbarSpy, - }; + return TestUtils.generatePageProps(ExperimentList, { pathname: RoutePage.EXPERIMENTS } as any, + '' as any, historyPushSpy, updateBannerSpy, updateDialogSpy, updateToolbarSpy, updateSnackbarSpy); } async function mountWithNExperiments(n: number, nRuns: number): Promise { @@ -261,7 +253,7 @@ describe('ExperimentList', () => { it('enables clone button when one run is selected', async () => { const tree = await mountWithNExperiments(1, 1); - (tree.instance() as any)._runSelectionChanged(['run1']); + (tree.instance() as any)._selectionChanged(['run1']); expect(updateToolbarSpy).toHaveBeenCalledTimes(2); expect(updateToolbarSpy.mock.calls[0][0].actions.find((b: any) => b.title === 'Clone run')) .toHaveProperty('disabled', true); @@ -272,7 +264,7 @@ describe('ExperimentList', () => { it('disables clone button when more than one run is selected', async () => { const tree = await mountWithNExperiments(1, 1); - (tree.instance() as any)._runSelectionChanged(['run1', 'run2']); + (tree.instance() as any)._selectionChanged(['run1', 'run2']); expect(updateToolbarSpy).toHaveBeenCalledTimes(2); expect(updateToolbarSpy.mock.calls[0][0].actions.find((b: any) => b.title === 'Clone run')) .toHaveProperty('disabled', true); @@ -283,9 +275,9 @@ describe('ExperimentList', () => { it('enables compare runs button only when more than one is selected', async () => { const tree = await mountWithNExperiments(1, 1); - (tree.instance() as any)._runSelectionChanged(['run1']); - (tree.instance() as any)._runSelectionChanged(['run1', 'run2']); - (tree.instance() as any)._runSelectionChanged(['run1', 'run2', 'run3']); + (tree.instance() as any)._selectionChanged(['run1']); + (tree.instance() as any)._selectionChanged(['run1', 'run2']); + (tree.instance() as any)._selectionChanged(['run1', 'run2', 'run3']); expect(updateToolbarSpy).toHaveBeenCalledTimes(4); expect(updateToolbarSpy.mock.calls[0][0].actions.find((b: any) => b.title === 'Compare runs')) .toHaveProperty('disabled', true); @@ -300,7 +292,7 @@ describe('ExperimentList', () => { it('navigates to compare page with the selected run ids', async () => { const tree = await mountWithNExperiments(1, 1); - (tree.instance() as any)._runSelectionChanged(['run1', 'run2', 'run3']); + (tree.instance() as any)._selectionChanged(['run1', 'run2', 'run3']); const compareBtn = (tree.instance() as ExperimentList) .getInitialToolbarState().actions.find(b => b.title === 'Compare runs'); await compareBtn!.action(); @@ -310,7 +302,7 @@ describe('ExperimentList', () => { it('navigates to new run page with the selected run id for cloning', async () => { const tree = await mountWithNExperiments(1, 1); - (tree.instance() as any)._runSelectionChanged(['run1']); + (tree.instance() as any)._selectionChanged(['run1']); const cloneBtn = (tree.instance() as ExperimentList) .getInitialToolbarState().actions.find(b => b.title === 'Clone run'); await cloneBtn!.action(); diff --git a/frontend/src/pages/ExperimentList.tsx b/frontend/src/pages/ExperimentList.tsx index ee7c74736c8..f5efcbc1c4e 100644 --- a/frontend/src/pages/ExperimentList.tsx +++ b/frontend/src/pages/ExperimentList.tsx @@ -24,9 +24,8 @@ import { ApiResourceType, ApiRun } from '../apis/run'; import { Apis, ExperimentSortKeys, ListRequest, RunSortKeys } from '../lib/Apis'; import { Link } from 'react-router-dom'; import { Page } from './Page'; -import { RoutePage, RouteParams, QUERY_PARAMS } from '../components/Router'; +import { RoutePage, RouteParams } from '../components/Router'; import { ToolbarProps } from '../components/Toolbar'; -import { URLParser } from '../lib/URLParser'; import { classes } from 'typestyle'; import { commonCss, padding } from '../Css'; import { logger } from '../lib/Utils'; @@ -40,7 +39,7 @@ interface DisplayExperiment extends ApiExperiment { interface ExperimentListState { displayExperiments: DisplayExperiment[]; - selectedRunIds: string[]; + selectedIds: string[]; selectedTab: number; } @@ -52,18 +51,19 @@ class ExperimentList extends Page<{}, ExperimentListState> { this.state = { displayExperiments: [], - selectedRunIds: [], + selectedIds: [], selectedTab: 0, }; } public getInitialToolbarState(): ToolbarProps { + const buttons = new Buttons(this.props, this.refresh.bind(this)); return { actions: [ - Buttons.newExperiment(this._newExperimentClicked.bind(this)), - Buttons.compareRuns(this._compareRuns.bind(this)), - Buttons.cloneRun(this._cloneRun.bind(this)), - Buttons.refresh(this.refresh.bind(this)), + buttons.newExperiment(), + buttons.compareRuns(() => this.state.selectedIds), + buttons.cloneRun(() => this.state.selectedIds, false), + buttons.refresh(this.refresh.bind(this)), ], breadcrumbs: [], pageTitle: 'Experiments', @@ -156,15 +156,6 @@ class ExperimentList extends Page<{}, ExperimentListState> { return response.next_page_token || ''; } - private _cloneRun(): void { - if (this.state.selectedRunIds.length === 1) { - const searchString = new URLParser(this.props).build({ - [QUERY_PARAMS.cloneFromRun]: this.state.selectedRunIds[0] || '' - }); - this.props.history.push(RoutePage.NEW_RUN + searchString); - } - } - private _nameCustomRenderer(value: string, id: string): JSX.Element { return e.stopPropagation()} to={RoutePage.EXPERIMENT_DETAILS.replace(':' + RouteParams.experimentId, id)}>{value}; @@ -180,30 +171,15 @@ class ExperimentList extends Page<{}, ExperimentListState> { ; } - private _runSelectionChanged(selectedRunIds: string[]): void { + private _selectionChanged(selectedIds: string[]): void { const actions = produce(this.props.toolbarProps.actions, draft => { // Enable/Disable Run compare button - draft[1].disabled = selectedRunIds.length <= 1 || selectedRunIds.length > 10; + draft[1].disabled = selectedIds.length <= 1 || selectedIds.length > 10; // Enable/Disable Clone button - draft[2].disabled = selectedRunIds.length !== 1; + draft[2].disabled = selectedIds.length !== 1; }); this.props.updateToolbar({ actions }); - this.setState({ selectedRunIds }); - } - - private _compareRuns(): void { - const indices = this.state.selectedRunIds; - if (indices.length > 1 && indices.length <= 10) { - const runIds = this.state.selectedRunIds.join(','); - const searchString = new URLParser(this.props).build({ - [QUERY_PARAMS.runlist]: runIds, - }); - this.props.history.push(RoutePage.COMPARE + searchString); - } - } - - private _newExperimentClicked(): void { - this.props.history.push(RoutePage.NEW_EXPERIMENT); + this.setState({ selectedIds }); } private _toggleRowExpand(rowIndex: number): void { @@ -221,8 +197,8 @@ class ExperimentList extends Page<{}, ExperimentListState> { const experiment = this.state.displayExperiments[experimentIndex]; const runIds = (experiment.last5Runs || []).map((r) => r.id!); return null} {...this.props} - disablePaging={true} selectedIds={this.state.selectedRunIds} noFilterBox={true} - onSelectionChange={this._runSelectionChanged.bind(this)} disableSorting={true} />; + disablePaging={true} selectedIds={this.state.selectedIds} noFilterBox={true} + onSelectionChange={this._selectionChanged.bind(this)} disableSorting={true} />; } } diff --git a/frontend/src/pages/PipelineDetails.test.tsx b/frontend/src/pages/PipelineDetails.test.tsx index 654a1e9749a..45883d16811 100644 --- a/frontend/src/pages/PipelineDetails.test.tsx +++ b/frontend/src/pages/PipelineDetails.test.tsx @@ -45,25 +45,16 @@ describe('PipelineDetails', () => { let testRun: ApiRunDetail = {}; function generateProps(fromRunSpec = false): PageProps { - // getInitialToolbarState relies on page props having been populated, so fill those first - const pageProps: PageProps = { - history: { push: historyPushSpy } as any, - location: { search: fromRunSpec ? `?${QUERY_PARAMS.fromRunId}=test-run-id` : '' } as any, - match: { - isExact: true, - params: fromRunSpec ? {} : { [RouteParams.pipelineId]: testPipeline.id }, - path: '', - url: '', - }, - toolbarProps: { actions: [], breadcrumbs: [], pageTitle: '' }, - updateBanner: updateBannerSpy, - updateDialog: updateDialogSpy, - updateSnackbar: updateSnackbarSpy, - updateToolbar: updateToolbarSpy, + const match = { + isExact: true, + params: fromRunSpec ? {} : { [RouteParams.pipelineId]: testPipeline.id }, + path: '', + url: '', }; - return Object.assign(pageProps, { - toolbarProps: new PipelineDetails(pageProps).getInitialToolbarState(), - }); + const location = { search: fromRunSpec ? `?${QUERY_PARAMS.fromRunId}=test-run-id` : '' } as any; + const pageProps = TestUtils.generatePageProps(PipelineDetails, location, match, historyPushSpy, + updateBannerSpy, updateDialogSpy, updateToolbarSpy, updateSnackbarSpy); + return pageProps; } beforeAll(() => jest.spyOn(console, 'error').mockImplementation()); @@ -454,7 +445,7 @@ describe('PipelineDetails', () => { await confirmBtn.onClick(); expect(updateDialogSpy).toHaveBeenCalledTimes(2); // Delete dialog + error dialog expect(updateDialogSpy).toHaveBeenLastCalledWith(expect.objectContaining({ - content: 'woops', + content: 'Failed to delete pipeline: test-pipeline-id with error: "woops"', title: 'Failed to delete pipeline', })); }); @@ -471,7 +462,7 @@ describe('PipelineDetails', () => { await confirmBtn.onClick(); expect(updateSnackbarSpy).toHaveBeenCalledTimes(1); expect(updateSnackbarSpy).toHaveBeenLastCalledWith(expect.objectContaining({ - message: 'Successfully deleted pipeline: ' + testPipeline.name, + message: 'Delete succeeded for this pipeline', open: true, })); }); diff --git a/frontend/src/pages/PipelineDetails.tsx b/frontend/src/pages/PipelineDetails.tsx index 9e22e0cd6ca..68a6ec1cacb 100644 --- a/frontend/src/pages/PipelineDetails.tsx +++ b/frontend/src/pages/PipelineDetails.tsx @@ -39,7 +39,7 @@ import { UnControlled as CodeMirror } from 'react-codemirror2'; import { Workflow } from '../../third_party/argo-ui/argo_template'; import { classes, stylesheet } from 'typestyle'; import { color, commonCss, padding, fontsize, fonts } from '../Css'; -import { logger, errorToMessage, formatDateString } from '../lib/Utils'; +import { logger, formatDateString } from '../lib/Utils'; interface PipelineDetailsState { graph?: dagre.graphlib.Graph; @@ -113,8 +113,11 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> { } public getInitialToolbarState(): ToolbarProps { + const buttons = new Buttons(this.props, this.refresh.bind(this)); const fromRunId = new URLParser(this.props).get(QUERY_PARAMS.fromRunId); - let actions: ToolbarActionConfig[] = [Buttons.newRun(this._createNewRun.bind(this))]; + let actions: ToolbarActionConfig[] = [ + buttons.newRunFromPipeline(() => this.state.pipeline ? this.state.pipeline.id! : ''), + ]; if (fromRunId) { return { @@ -127,15 +130,9 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> { } else { // Add buttons for creating experiment and deleting pipeline actions = actions.concat([ - Buttons.newExperiment(this._createNewExperiment.bind(this)), - Buttons.delete(() => this.props.updateDialog({ - buttons: [ - { onClick: () => this._deleteDialogClosed(true), text: 'Delete' }, - { onClick: () => this._deleteDialogClosed(false), text: 'Cancel' }, - ], - onClose: () => this._deleteDialogClosed(false), - title: 'Delete this pipeline?', - })) + buttons.newExperiment(() => this.state.pipeline ? this.state.pipeline.id! : ''), + buttons.delete(() => this.state.pipeline ? [this.state.pipeline.id!] : [], + 'pipeline', () => null, true), ]); return { actions, @@ -348,52 +345,6 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> { templateString, }); } - - private _createNewRun(): void { - let searchString = ''; - const fromRunId = new URLParser(this.props).get(QUERY_PARAMS.fromRunId); - - if (fromRunId) { - searchString = new URLParser(this.props).build(Object.assign( - { [QUERY_PARAMS.fromRunId]: fromRunId } - )); - } else { - searchString = new URLParser(this.props).build(Object.assign( - { [QUERY_PARAMS.pipelineId]: this.state.pipeline!.id || '' } - )); - } - - this.props.history.push(RoutePage.NEW_RUN + searchString); - } - - private _createNewExperiment(): void { - const searchString = new URLParser(this.props).build({ - [QUERY_PARAMS.pipelineId]: this.state.pipeline!.id || '' - }); - this.props.history.push(RoutePage.NEW_EXPERIMENT + searchString); - } - - private async _deleteDialogClosed(deleteConfirmed: boolean): Promise { - if (deleteConfirmed) { - try { - await Apis.pipelineServiceApi.deletePipeline(this.state.pipeline!.id!); - this.props.updateSnackbar({ - autoHideDuration: 10000, - message: `Successfully deleted pipeline: ${this.state.pipeline!.name}`, - open: true, - }); - this.props.history.push(RoutePage.PIPELINES); - } catch (err) { - const errorMessage = await errorToMessage(err); - this.props.updateDialog({ - buttons: [{ text: 'Dismiss' }], - content: errorMessage, - title: 'Failed to delete pipeline', - }); - logger.error('Deleting pipeline failed with error:', err); - } - } - } } export default PipelineDetails; diff --git a/frontend/src/pages/PipelineList.test.tsx b/frontend/src/pages/PipelineList.test.tsx index 0db992402d9..6098ad65cd7 100644 --- a/frontend/src/pages/PipelineList.test.tsx +++ b/frontend/src/pages/PipelineList.test.tsx @@ -39,16 +39,8 @@ describe('PipelineList', () => { const uploadPipelineSpy = jest.spyOn(Apis, 'uploadPipeline'); function generateProps(): PageProps { - return { - history: {} as any, - location: '' as any, - match: '' as any, - toolbarProps: PipelineList.prototype.getInitialToolbarState(), - updateBanner: updateBannerSpy, - updateDialog: updateDialogSpy, - updateSnackbar: updateSnackbarSpy, - updateToolbar: updateToolbarSpy, - }; + return TestUtils.generatePageProps(PipelineList, '' as any, '' as any, null, updateBannerSpy, + updateDialogSpy, updateToolbarSpy, updateSnackbarSpy); } async function mountWithNPipelines(n: number): Promise { @@ -328,7 +320,7 @@ describe('PipelineList', () => { const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete'); await confirmBtn.onClick(); expect(updateSnackbarSpy).toHaveBeenLastCalledWith({ - message: 'Successfully deleted 1 pipeline!', + message: 'Delete succeeded for 1 pipeline', open: true, }); }); @@ -345,7 +337,7 @@ describe('PipelineList', () => { await confirmBtn.onClick(); const lastCall = updateDialogSpy.mock.calls[1][0]; expect(lastCall).toMatchObject({ - content: 'Deleting pipeline: test pipeline name0 failed with error: "woops, failed"', + content: 'Failed to delete pipeline: test-pipeline-id0 with error: "woops, failed"', title: 'Failed to delete 1 pipeline', }); }); @@ -373,14 +365,14 @@ describe('PipelineList', () => { expect(updateDialogSpy).toHaveBeenCalledTimes(2); const lastCall = updateDialogSpy.mock.calls[1][0]; expect(lastCall).toMatchObject({ - content: 'Deleting pipeline: test pipeline name0 failed with error: "woops, failed!"\n\n' + - 'Deleting pipeline: test pipeline name1 failed with error: "woops, failed!"', + content: 'Failed to delete pipeline: test-pipeline-id0 with error: "woops, failed!"\n\n' + + 'Failed to delete pipeline: test-pipeline-id1 with error: "woops, failed!"', title: 'Failed to delete 2 pipelines', }); // Should show snackbar for the one successful deletion expect(updateSnackbarSpy).toHaveBeenLastCalledWith({ - message: 'Successfully deleted 2 pipelines!', + message: 'Delete succeeded for 2 pipelines', open: true, }); }); diff --git a/frontend/src/pages/PipelineList.tsx b/frontend/src/pages/PipelineList.tsx index 2f7bf17bff6..5204ae58ff8 100644 --- a/frontend/src/pages/PipelineList.tsx +++ b/frontend/src/pages/PipelineList.tsx @@ -27,7 +27,7 @@ import { RoutePage, RouteParams } from '../components/Router'; import { ToolbarProps } from '../components/Toolbar'; import { classes } from 'typestyle'; import { commonCss, padding } from '../Css'; -import { formatDateString, errorToMessage, s } from '../lib/Utils'; +import { formatDateString, errorToMessage } from '../lib/Utils'; interface PipelineListState { pipelines: ApiPipeline[]; @@ -49,18 +49,17 @@ class PipelineList extends Page<{}, PipelineListState> { } public getInitialToolbarState(): ToolbarProps { + const buttons = new Buttons(this.props, this.refresh.bind(this)); return { actions: [ - Buttons.upload(() => this.setStateSafe({ uploadDialogOpen: true })), - Buttons.refresh(this.refresh.bind(this)), - Buttons.delete(() => this.props.updateDialog({ - buttons: [ - { onClick: async () => await this._deleteDialogClosed(true), text: 'Delete' }, - { onClick: async () => await this._deleteDialogClosed(false), text: 'Cancel' }, - ], - onClose: async () => await this._deleteDialogClosed(false), - title: `Delete ${this.state.selectedIds.length} pipeline${s(this.state.selectedIds)}?`, - })), + buttons.upload(() => this.setStateSafe({ uploadDialogOpen: true })), + buttons.refresh(this.refresh.bind(this)), + buttons.delete( + () => this.state.selectedIds, + 'pipeline', + ids => this._selectionChanged(ids), + false, + ), ], breadcrumbs: [], pageTitle: 'Pipelines', @@ -138,42 +137,6 @@ class PipelineList extends Page<{}, PipelineListState> { this.setStateSafe({ selectedIds }); } - private async _deleteDialogClosed(deleteConfirmed: boolean): Promise { - if (deleteConfirmed) { - const unsuccessfulDeleteIds: string[] = []; - const errorMessages: string[] = []; - // TODO: Show spinner during wait. - await Promise.all(this.state.selectedIds.map(async (id) => { - try { - await Apis.pipelineServiceApi.deletePipeline(id); - } catch (err) { - unsuccessfulDeleteIds.push(id); - const pipeline = this.state.pipelines.find((p) => p.id === id); - const errorMessage = await errorToMessage(err); - errorMessages.push( - `Deleting pipeline${pipeline ? ': ' + pipeline.name : ''} failed with error: "${errorMessage}"`); - } - })); - - const successfulDeletes = this.state.selectedIds.length - unsuccessfulDeleteIds.length; - if (successfulDeletes > 0) { - this.props.updateSnackbar({ - message: `Successfully deleted ${successfulDeletes} pipeline${successfulDeletes === 1 ? '' : 's'}!`, - open: true, - }); - this.refresh(); - } - - if (unsuccessfulDeleteIds.length > 0) { - this.showErrorDialog( - `Failed to delete ${unsuccessfulDeleteIds.length} pipeline${unsuccessfulDeleteIds.length === 1 ? '' : 's'}`, - errorMessages.join('\n\n')); - } - - this._selectionChanged(unsuccessfulDeleteIds); - } - } - private async _uploadDialogClosed(confirmed: boolean, name: string, file: File | null, url: string, method: ImportMethod, description?: string): Promise { diff --git a/frontend/src/pages/RecurringRunDetails.test.tsx b/frontend/src/pages/RecurringRunDetails.test.tsx index e0752bcacce..2dfb3879bbf 100644 --- a/frontend/src/pages/RecurringRunDetails.test.tsx +++ b/frontend/src/pages/RecurringRunDetails.test.tsx @@ -24,12 +24,6 @@ import { RouteParams, RoutePage } from '../components/Router'; import { ToolbarActionConfig } from '../components/Toolbar'; import { shallow } from 'enzyme'; -class TestRecurringRunDetails extends RecurringRunDetails { - public async _deleteDialogClosed(confirmed: boolean): Promise { - super._deleteDialogClosed(confirmed); - } -} - describe('RecurringRunDetails', () => { const updateBannerSpy = jest.fn(); const updateDialogSpy = jest.fn(); @@ -45,16 +39,9 @@ describe('RecurringRunDetails', () => { let fullTestJob: ApiJob = {}; function generateProps(): PageProps { - return { - history: { push: historyPushSpy } as any, - location: '' as any, - match: { params: { [RouteParams.runId]: fullTestJob.id }, isExact: true, path: '', url: '' }, - toolbarProps: RecurringRunDetails.prototype.getInitialToolbarState(), - updateBanner: updateBannerSpy, - updateDialog: updateDialogSpy, - updateSnackbar: updateSnackbarSpy, - updateToolbar: updateToolbarSpy, - }; + const match = { params: { [RouteParams.runId]: fullTestJob.id }, isExact: true, path: '', url: '' }; + return TestUtils.generatePageProps(RecurringRunDetails, '' as any, match, historyPushSpy, + updateBannerSpy, updateDialogSpy, updateToolbarSpy, updateSnackbarSpy); } beforeEach(() => { @@ -321,7 +308,7 @@ describe('RecurringRunDetails', () => { const deleteBtn = instance.getInitialToolbarState().actions.find(b => b.title === 'Delete'); await deleteBtn!.action(); expect(updateDialogSpy).toHaveBeenLastCalledWith(expect.objectContaining({ - title: 'Delete this recurring run?', + title: 'Delete this recurring run config?', })); tree.unmount(); }); @@ -359,23 +346,32 @@ describe('RecurringRunDetails', () => { // or clicking outside it, it should be treated the same way as clicking Cancel. it('redirects back to parent experiment after delete', async () => { - const tree = shallow(); + const tree = shallow(); await TestUtils.flushPromises(); - const instance = tree.instance() as TestRecurringRunDetails; - await instance._deleteDialogClosed(true); + const deleteBtn = (tree.instance() as RecurringRunDetails) + .getInitialToolbarState().actions.find(b => b.title === 'Delete'); + await deleteBtn!.action(); + const call = updateDialogSpy.mock.calls[0][0]; + const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete'); + await confirmBtn.onClick(); + expect(deleteJobSpy).toHaveBeenLastCalledWith('test-job-id'); expect(historyPushSpy).toHaveBeenCalledTimes(1); expect(historyPushSpy).toHaveBeenLastCalledWith(RoutePage.EXPERIMENTS); tree.unmount(); }); it('shows snackbar after successful deletion', async () => { - const tree = shallow(); + const tree = shallow(); await TestUtils.flushPromises(); - const instance = tree.instance() as TestRecurringRunDetails; - await instance._deleteDialogClosed(true); + const deleteBtn = (tree.instance() as RecurringRunDetails) + .getInitialToolbarState().actions.find(b => b.title === 'Delete'); + await deleteBtn!.action(); + const call = updateDialogSpy.mock.calls[0][0]; + const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete'); + await confirmBtn.onClick(); expect(updateSnackbarSpy).toHaveBeenCalledTimes(1); expect(updateSnackbarSpy).toHaveBeenLastCalledWith({ - message: 'Successfully deleted recurring run: ' + fullTestJob.name, + message: 'Delete succeeded for this recurring run config', open: true, }); tree.unmount(); @@ -383,15 +379,19 @@ describe('RecurringRunDetails', () => { it('shows error dialog after failing deletion', async () => { TestUtils.makeErrorResponseOnce(deleteJobSpy, 'could not delete'); - const tree = shallow(); + const tree = shallow(); await TestUtils.flushPromises(); - const instance = tree.instance() as TestRecurringRunDetails; - await instance._deleteDialogClosed(true); + const deleteBtn = (tree.instance() as RecurringRunDetails) + .getInitialToolbarState().actions.find(b => b.title === 'Delete'); + await deleteBtn!.action(); + const call = updateDialogSpy.mock.calls[0][0]; + const confirmBtn = call.buttons.find((b: any) => b.text === 'Delete'); + await confirmBtn.onClick(); await TestUtils.flushPromises(); - expect(updateDialogSpy).toHaveBeenCalledTimes(1); + expect(updateDialogSpy).toHaveBeenCalledTimes(2); expect(updateDialogSpy).toHaveBeenLastCalledWith(expect.objectContaining({ - content: 'could not delete', - title: 'Failed to delete recurring run', + content: 'Failed to delete recurring run config: test-job-id with error: "could not delete"', + title: 'Failed to delete recurring run config', })); // Should not reroute expect(historyPushSpy).not.toHaveBeenCalled(); diff --git a/frontend/src/pages/RecurringRunDetails.tsx b/frontend/src/pages/RecurringRunDetails.tsx index bc2f50d68c4..b3de4c58cce 100644 --- a/frontend/src/pages/RecurringRunDetails.tsx +++ b/frontend/src/pages/RecurringRunDetails.tsx @@ -23,7 +23,7 @@ import { ApiJob } from '../apis/job'; import { Apis } from '../lib/Apis'; import { Page } from './Page'; import { RoutePage, RouteParams } from '../components/Router'; -import { ToolbarActionConfig, Breadcrumb, ToolbarProps } from '../components/Toolbar'; +import { Breadcrumb, ToolbarProps } from '../components/Toolbar'; import { classes } from 'typestyle'; import { commonCss, padding } from '../Css'; import { formatDateString, enabledDisplayString, errorToMessage } from '../lib/Utils'; @@ -44,19 +44,18 @@ class RecurringRunDetails extends Page<{}, RecurringRunConfigState> { } public getInitialToolbarState(): ToolbarProps { + const buttons = new Buttons(this.props, this.refresh.bind(this)); return { actions: [ - Buttons.refresh(this.refresh.bind(this)), - Buttons.enableRun(() => this._setEnabledState(true)), - Buttons.disableRun(() => this._setEnabledState(false)), - Buttons.delete(() => this.props.updateDialog({ - buttons: [ - { onClick: () => this._deleteDialogClosed(true), text: 'Delete' }, - { onClick: () => this._deleteDialogClosed(false), text: 'Cancel' }, - ], - onClose: () => this._deleteDialogClosed(false), - title: 'Delete this recurring run?', - })), + buttons.refresh(this.refresh.bind(this)), + buttons.enableRecurringRun(() => this.state.run ? this.state.run.id! : ''), + buttons.disableRecurringRun(() => this.state.run ? this.state.run.id! : ''), + buttons.delete( + () => this.state.run ? [this.state.run!.id!] : [], + 'recurring run config', + this._deleteCallback.bind(this), + true, + ), ], breadcrumbs: [], pageTitle: '', @@ -179,50 +178,12 @@ class RecurringRunDetails extends Page<{}, RecurringRunConfigState> { this.setState({ run }); } - protected async _setEnabledState(enabled: boolean): Promise { - if (this.state.run) { - const toolbarActions = [...this.props.toolbarProps.actions]; - - const buttonIndex = enabled ? 1 : 2; - const id = this.state.run.id!; - - toolbarActions[buttonIndex].busy = true; - this._updateToolbar(toolbarActions); - try { - await (enabled ? Apis.jobServiceApi.enableJob(id) : Apis.jobServiceApi.disableJob(id)); - this.refresh(); - } catch (err) { - const errorMessage = await errorToMessage(err); - this.showErrorDialog( - `Failed to ${enabled ? 'enable' : 'disable'} recurring run`, errorMessage); - } finally { - toolbarActions[buttonIndex].busy = false; - this._updateToolbar(toolbarActions); - } - } - } - - protected _updateToolbar(actions: ToolbarActionConfig[]): void { - this.props.updateToolbar({ actions }); - } - - protected async _deleteDialogClosed(deleteConfirmed: boolean): Promise { - if (deleteConfirmed) { - // TODO: Show spinner during wait. - try { - await Apis.jobServiceApi.deleteJob(this.state.run!.id!); - const breadcrumbs = this.props.toolbarProps.breadcrumbs; - const previousPage = breadcrumbs.length ? - breadcrumbs[breadcrumbs.length - 1].href : RoutePage.EXPERIMENTS; - this.props.history.push(previousPage); - this.props.updateSnackbar({ - message: `Successfully deleted recurring run: ${this.state.run!.name}`, - open: true, - }); - } catch (err) { - const errorMessage = await errorToMessage(err); - this.showErrorDialog('Failed to delete recurring run', errorMessage); - } + private _deleteCallback(_: string[], success: boolean): void { + if (success) { + const breadcrumbs = this.props.toolbarProps.breadcrumbs; + const previousPage = breadcrumbs.length ? + breadcrumbs[breadcrumbs.length - 1].href : RoutePage.EXPERIMENTS; + this.props.history.push(previousPage); } } } diff --git a/frontend/src/pages/RunDetails.tsx b/frontend/src/pages/RunDetails.tsx index 3efdde0476d..19e5ec2e501 100644 --- a/frontend/src/pages/RunDetails.tsx +++ b/frontend/src/pages/RunDetails.tsx @@ -35,9 +35,8 @@ import { Apis } from '../lib/Apis'; import { NodePhase, statusToIcon, hasFinished } from './Status'; import { OutputArtifactLoader } from '../lib/OutputArtifactLoader'; import { Page } from './Page'; -import { RoutePage, RouteParams, QUERY_PARAMS } from '../components/Router'; +import { RoutePage, RouteParams } from '../components/Router'; import { ToolbarProps } from '../components/Toolbar'; -import { URLParser } from '../lib/URLParser'; import { ViewerConfig } from '../components/viewers/Viewer'; import { Workflow } from '../../third_party/argo-ui/argo_template'; import { classes, stylesheet } from 'typestyle'; @@ -132,10 +131,11 @@ class RunDetails extends Page { } public getInitialToolbarState(): ToolbarProps { + const buttons = new Buttons(this.props, this.refresh.bind(this)); return { actions: [ - Buttons.cloneRun(this._cloneRun.bind(this)), - Buttons.refresh(this.refresh.bind(this)), + buttons.cloneRun(() => this.state.runMetadata ? [this.state.runMetadata!.id!] : [], true), + buttons.refresh(this.refresh.bind(this)), ], breadcrumbs: [{ displayName: 'Experiments', href: RoutePage.EXPERIMENTS }], pageTitle: this.props.runId!, @@ -225,13 +225,13 @@ class RunDetails extends Page {
-
- - - Runtime execution graph. Only steps that are currently running or have already completed are shown. +
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown. +
-
} {!graph && (
@@ -515,15 +515,6 @@ class RunDetails extends Page { this.setStateSafe({ sidepanelBusy: false }); } } - - private _cloneRun(): void { - if (this.state.runMetadata) { - const searchString = new URLParser(this.props).build({ - [QUERY_PARAMS.cloneFromRun]: this.state.runMetadata.id || '' - }); - this.props.history.push(RoutePage.NEW_RUN + searchString); - } - } } export default RunDetails; diff --git a/frontend/src/pages/__snapshots__/Compare.test.tsx.snap b/frontend/src/pages/__snapshots__/Compare.test.tsx.snap index 0a0556ff288..f907fbf36ba 100644 --- a/frontend/src/pages/__snapshots__/Compare.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/Compare.test.tsx.snap @@ -100,11 +100,7 @@ exports[`Compare creates a map of viewers 1`] = ` "search": "?runlist=run-with-workflow", } } - match={ - Object { - "params": Object {}, - } - } + match={Object {}} onError={[Function]} onSelectionChange={[Function]} runIdListMask={ @@ -315,11 +311,7 @@ exports[`Compare creates an extra aggregation plot for compatible viewers 1`] = "search": "?runlist=run1-id,run2-id", } } - match={ - Object { - "params": Object {}, - } - } + match={Object {}} onError={[Function]} onSelectionChange={[Function]} runIdListMask={ @@ -584,11 +576,7 @@ exports[`Compare displays parameters from multiple runs 1`] = ` "search": "?runlist=run1,run2", } } - match={ - Object { - "params": Object {}, - } - } + match={Object {}} onError={[Function]} onSelectionChange={[Function]} runIdListMask={ @@ -754,11 +742,7 @@ exports[`Compare displays run's parameters if the run has any 1`] = ` "search": "?runlist=run-with-parameters", } } - match={ - Object { - "params": Object {}, - } - } + match={Object {}} onError={[Function]} onSelectionChange={[Function]} runIdListMask={ @@ -914,11 +898,7 @@ exports[`Compare does not show viewers for deselected runs 1`] = ` "search": "?runlist=run-with-workflow-1,run-with-workflow-2", } } - match={ - Object { - "params": Object {}, - } - } + match={Object {}} onError={[Function]} onSelectionChange={[Function]} runIdListMask={ @@ -1053,11 +1033,7 @@ exports[`Compare expands all sections if they were collapsed 1`] = ` "search": "?runlist=run-with-workflow-1,run-with-workflow-2", } } - match={ - Object { - "params": Object {}, - } - } + match={Object {}} onError={[Function]} onSelectionChange={[Function]} runIdListMask={ @@ -1316,11 +1292,7 @@ exports[`Compare renders a page with multiple runs 1`] = ` "search": "?runlist=mock-run-1-id,mock-run-2-id,mock-run-3-id", } } - match={ - Object { - "params": Object {}, - } - } + match={Object {}} onError={[Function]} onSelectionChange={[Function]} runIdListMask={ @@ -1468,11 +1440,7 @@ exports[`Compare renders a page with no runs 1`] = ` "search": "", } } - match={ - Object { - "params": Object {}, - } - } + match={Object {}} onError={[Function]} onSelectionChange={[Function]} runIdListMask={Array []} diff --git a/frontend/src/pages/__snapshots__/ExperimentDetails.test.tsx.snap b/frontend/src/pages/__snapshots__/ExperimentDetails.test.tsx.snap index 54e48c81993..67c50f37959 100644 --- a/frontend/src/pages/__snapshots__/ExperimentDetails.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/ExperimentDetails.test.tsx.snap @@ -89,7 +89,7 @@ exports[`ExperimentDetails fetches this experiment's recurring runs 1`] = ` "outlined": true, "primary": true, "title": "Create run", - "tooltip": "Create a new run within this experiment", + "tooltip": "Create a new run", }, Object { "action": [Function], @@ -97,7 +97,7 @@ exports[`ExperimentDetails fetches this experiment's recurring runs 1`] = ` "id": "createNewRecurringRunBtn", "outlined": true, "title": "Create recurring run", - "tooltip": "Create a new recurring run in this experiment", + "tooltip": "Create a new recurring run", }, Object { "action": [Function], @@ -112,7 +112,7 @@ exports[`ExperimentDetails fetches this experiment's recurring runs 1`] = ` "disabled": true, "disabledTitle": "Select a run to clone", "id": "cloneBtn", - "title": "Clone", + "title": "Clone run", "tooltip": "Create a copy from this runs initial state", }, ] @@ -128,7 +128,7 @@ exports[`ExperimentDetails fetches this experiment's recurring runs 1`] = ` "push": [MockFunction], } } - location="" + location={Object {}} match={ Object { "params": Object { @@ -155,7 +155,7 @@ exports[`ExperimentDetails fetches this experiment's recurring runs 1`] = ` "href": "/experiments", }, ], - "pageTitle": "", + "pageTitle": "some-mock-experiment-id", } } updateBanner={ @@ -232,7 +232,7 @@ exports[`ExperimentDetails fetches this experiment's recurring runs 1`] = ` "push": [MockFunction], } } - location="" + location={Object {}} match={ Object { "params": Object { @@ -256,7 +256,7 @@ exports[`ExperimentDetails fetches this experiment's recurring runs 1`] = ` "href": "/experiments", }, ], - "pageTitle": "", + "pageTitle": "some-mock-experiment-id", } } updateBanner={ @@ -433,7 +433,7 @@ exports[`ExperimentDetails removes all description text after second newline and "outlined": true, "primary": true, "title": "Create run", - "tooltip": "Create a new run within this experiment", + "tooltip": "Create a new run", }, Object { "action": [Function], @@ -441,7 +441,7 @@ exports[`ExperimentDetails removes all description text after second newline and "id": "createNewRecurringRunBtn", "outlined": true, "title": "Create recurring run", - "tooltip": "Create a new recurring run in this experiment", + "tooltip": "Create a new recurring run", }, Object { "action": [Function], @@ -456,7 +456,7 @@ exports[`ExperimentDetails removes all description text after second newline and "disabled": true, "disabledTitle": "Select a run to clone", "id": "cloneBtn", - "title": "Clone", + "title": "Clone run", "tooltip": "Create a copy from this runs initial state", }, ] @@ -472,7 +472,7 @@ exports[`ExperimentDetails removes all description text after second newline and "push": [MockFunction], } } - location="" + location={Object {}} match={ Object { "params": Object { @@ -499,7 +499,7 @@ exports[`ExperimentDetails removes all description text after second newline and "href": "/experiments", }, ], - "pageTitle": "", + "pageTitle": "some-mock-experiment-id", } } updateBanner={ @@ -576,7 +576,7 @@ exports[`ExperimentDetails removes all description text after second newline and "push": [MockFunction], } } - location="" + location={Object {}} match={ Object { "params": Object { @@ -600,7 +600,7 @@ exports[`ExperimentDetails removes all description text after second newline and "href": "/experiments", }, ], - "pageTitle": "", + "pageTitle": "some-mock-experiment-id", } } updateBanner={ @@ -764,7 +764,7 @@ exports[`ExperimentDetails renders a page with no runs or recurring runs 1`] = ` "outlined": true, "primary": true, "title": "Create run", - "tooltip": "Create a new run within this experiment", + "tooltip": "Create a new run", }, Object { "action": [Function], @@ -772,7 +772,7 @@ exports[`ExperimentDetails renders a page with no runs or recurring runs 1`] = ` "id": "createNewRecurringRunBtn", "outlined": true, "title": "Create recurring run", - "tooltip": "Create a new recurring run in this experiment", + "tooltip": "Create a new recurring run", }, Object { "action": [Function], @@ -787,7 +787,7 @@ exports[`ExperimentDetails renders a page with no runs or recurring runs 1`] = ` "disabled": true, "disabledTitle": "Select a run to clone", "id": "cloneBtn", - "title": "Clone", + "title": "Clone run", "tooltip": "Create a copy from this runs initial state", }, ] @@ -803,7 +803,7 @@ exports[`ExperimentDetails renders a page with no runs or recurring runs 1`] = ` "push": [MockFunction], } } - location="" + location={Object {}} match={ Object { "params": Object { @@ -830,7 +830,7 @@ exports[`ExperimentDetails renders a page with no runs or recurring runs 1`] = ` "href": "/experiments", }, ], - "pageTitle": "", + "pageTitle": "some-mock-experiment-id", } } updateBanner={ @@ -907,7 +907,7 @@ exports[`ExperimentDetails renders a page with no runs or recurring runs 1`] = ` "push": [MockFunction], } } - location="" + location={Object {}} match={ Object { "params": Object { @@ -931,7 +931,7 @@ exports[`ExperimentDetails renders a page with no runs or recurring runs 1`] = ` "href": "/experiments", }, ], - "pageTitle": "", + "pageTitle": "some-mock-experiment-id", } } updateBanner={ @@ -1093,7 +1093,7 @@ exports[`ExperimentDetails uses an empty string if the experiment has no descrip "outlined": true, "primary": true, "title": "Create run", - "tooltip": "Create a new run within this experiment", + "tooltip": "Create a new run", }, Object { "action": [Function], @@ -1101,7 +1101,7 @@ exports[`ExperimentDetails uses an empty string if the experiment has no descrip "id": "createNewRecurringRunBtn", "outlined": true, "title": "Create recurring run", - "tooltip": "Create a new recurring run in this experiment", + "tooltip": "Create a new recurring run", }, Object { "action": [Function], @@ -1116,7 +1116,7 @@ exports[`ExperimentDetails uses an empty string if the experiment has no descrip "disabled": true, "disabledTitle": "Select a run to clone", "id": "cloneBtn", - "title": "Clone", + "title": "Clone run", "tooltip": "Create a copy from this runs initial state", }, ] @@ -1132,7 +1132,7 @@ exports[`ExperimentDetails uses an empty string if the experiment has no descrip "push": [MockFunction], } } - location="" + location={Object {}} match={ Object { "params": Object { @@ -1159,7 +1159,7 @@ exports[`ExperimentDetails uses an empty string if the experiment has no descrip "href": "/experiments", }, ], - "pageTitle": "", + "pageTitle": "some-mock-experiment-id", } } updateBanner={ @@ -1236,7 +1236,7 @@ exports[`ExperimentDetails uses an empty string if the experiment has no descrip "push": [MockFunction], } } - location="" + location={Object {}} match={ Object { "params": Object { @@ -1260,7 +1260,7 @@ exports[`ExperimentDetails uses an empty string if the experiment has no descrip "href": "/experiments", }, ], - "pageTitle": "", + "pageTitle": "some-mock-experiment-id", } } updateBanner={