Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Executions empty state and table #2753

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import UnauthorizedError from '~/pages/UnauthorizedError';
import { useUser } from '~/redux/selectors';
import {
globArtifactsAll,
globExecutionsAll,
globExperimentsAll,
globPipelineRunsAll,
globPipelinesAll,
Expand Down Expand Up @@ -38,6 +39,9 @@ const GlobalPipelineRunsRoutes = React.lazy(
const GlobalPipelineExperimentRoutes = React.lazy(
() => import('../pages/pipelines/GlobalPipelineExperimentsRoutes'),
);
const GlobalPipelineExecutionsRoutes = React.lazy(
() => import('../pages/pipelines/GlobalPipelineExecutionsRoutes'),
);

const GlobalArtifactsRoutes = React.lazy(() => import('../pages/pipelines/GlobalArtifactsRoutes'));

Expand Down Expand Up @@ -111,6 +115,7 @@ const AppRoutes: React.FC = () => {
<Route path={globPipelineRunsAll} element={<GlobalPipelineRunsRoutes />} />
<Route path={globExperimentsAll} element={<GlobalPipelineExperimentRoutes />} />
<Route path={globArtifactsAll} element={<GlobalArtifactsRoutes />} />
<Route path={globExecutionsAll} element={<GlobalPipelineExecutionsRoutes />} />

<Route path="/distributedWorkloads/*" element={<GlobalDistributedWorkloadsRoutes />} />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import useFetchState, {
export const useExecutionsFromMlmdContext = (
context: MlmdContext | null,
refreshRate?: number,
): FetchState<Execution[] | null> => {
): FetchState<Execution[]> => {
const { metadataStoreServiceClient } = usePipelinesAPI();

const call = React.useCallback<FetchStateCallbackPromise<Execution[] | null>>(async () => {
const call = React.useCallback<FetchStateCallbackPromise<Execution[]>>(async () => {
if (!context) {
return Promise.reject(new NotReadyError('No context'));
}
Expand All @@ -25,7 +25,7 @@ export const useExecutionsFromMlmdContext = (
return res.getExecutionsList();
}, [metadataStoreServiceClient, context]);

return useFetchState(call, null, {
return useFetchState(call, [], {
refreshRate,
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { useMlmdListContext, usePipelinesAPI } from '~/concepts/pipelines/context';
import { Execution, GetExecutionsRequest } from '~/third_party/mlmd';
import { ListOperationOptions } from '~/third_party/mlmd/generated/ml_metadata/proto/metadata_store_pb';
import useFetchState, { FetchState, FetchStateCallbackPromise } from '~/utilities/useFetchState';

export interface ExecutionsListResponse {
executions: Execution[];
nextPageToken: string;
}

export const useGetExecutionsList = (): FetchState<ExecutionsListResponse | null> => {
const { metadataStoreServiceClient } = usePipelinesAPI();
const { pageToken, maxResultSize, filterQuery } = useMlmdListContext();

const call = React.useCallback<FetchStateCallbackPromise<ExecutionsListResponse>>(async () => {
const request = new GetExecutionsRequest();
const listOperationOptions = new ListOperationOptions();
listOperationOptions.setOrderByField(
new ListOperationOptions.OrderByField().setField(ListOperationOptions.OrderByField.Field.ID),
);

if (filterQuery) {
listOperationOptions.setFilterQuery(filterQuery);
}
if (pageToken) {
listOperationOptions.setNextPageToken(pageToken);
}

listOperationOptions.setMaxResultSize(maxResultSize);
request.setOptions(listOperationOptions);

const response = await metadataStoreServiceClient.getExecutions(request);
const nextPageToken = response.getNextPageToken();
listOperationOptions.setNextPageToken(nextPageToken);

return { executions: response.getExecutionsList(), nextPageToken };
}, [filterQuery, maxResultSize, metadataStoreServiceClient, pageToken]);

return useFetchState(call, null);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { FAST_POLL_INTERVAL } from '~/utilities/const';

const useExecutionsForPipelineRun = (
run: PipelineRunKFv2 | null,
): [executions: Execution[] | null, loaded: boolean, error?: Error] => {
): [executions: Execution[], loaded: boolean, error?: Error] => {
const isFinished = isPipelineRunFinished(run);
const refreshRate = isFinished ? 0 : FAST_POLL_INTERVAL;
// contextError means mlmd service is not available, no need to check executions
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/concepts/pipelines/kfTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ export enum ArtifactType {
MARKDOWN = 'system.Markdown',
}

export enum ExecutionType {
CONTAINER_EXECUTION = 'system.ContainerExecution',
DAG_EXECUTION = 'system.DAGExecution',
}

export enum ExecutionStatus {
UNKNOWN = 'Unknown',
NEW = 'New',
RUNNING = 'Running',
COMPLETE = 'Complete',
FAILED = 'Failed',
CACHED = 'Cached',
CANCELED = 'Canceled',
}

/** @deprecated resource type is no longer a concept in v2 */
export enum ResourceTypeKF {
UNKNOWN_RESOURCE_TYPE = 'UNKNOWN_RESOURCE_TYPE',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { KubeFlowTaskTopology } from './pipelineTaskTypes';
export const usePipelineTaskTopology = (
spec?: PipelineSpecVariable,
runDetails?: RunDetailsKF,
executions?: Execution[] | null,
executions?: Execution[],
): KubeFlowTaskTopology => {
if (!spec) {
return { taskMap: {}, nodes: [] };
Expand Down Expand Up @@ -50,9 +50,10 @@ export const usePipelineTaskTopology = (
const executorLabel = component?.executorLabel;
const executor = executorLabel ? executors[executorLabel] : undefined;

const status = executions
? parseRuntimeInfoFromExecutions(taskId, executions)
: parseRuntimeInfoFromRunDetails(taskId, runDetails);
const status =
executions && executions.length !== 0
? parseRuntimeInfoFromExecutions(taskId, executions)
: parseRuntimeInfoFromRunDetails(taskId, runDetails);

const runAfter: string[] = taskValue.dependentTasks ?? [];

Expand Down
30 changes: 30 additions & 0 deletions frontend/src/pages/pipelines/GlobalPipelineExecutionsRoutes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import { Navigate, Route } from 'react-router-dom';
import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes';
import GlobalPipelineCoreLoader from '~/pages/pipelines/global/GlobalPipelineCoreLoader';
import { executionsBaseRoute } from '~/routes';
import {
executionsPageDescription,
executionsPageTitle,
} from '~/pages/pipelines/global/experiments/executions/const';
import GlobalExecutions from '~/pages/pipelines/global/experiments/executions/GlobalExecutions';

const GlobalPipelineExecutionsRoutes: React.FC = () => (
<ProjectsRoutes>
<Route
path="/:namespace?/*"
element={
<GlobalPipelineCoreLoader
title={executionsPageTitle}
description={executionsPageDescription}
getInvalidRedirectPath={executionsBaseRoute}
/>
}
>
<Route index element={<GlobalExecutions />} />
<Route path="*" element={<Navigate to="." />} />
</Route>
</ProjectsRoutes>
);

export default GlobalPipelineExecutionsRoutes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as React from 'react';
import {
Bullseye,
EmptyState,
EmptyStateBody,
EmptyStateHeader,
EmptyStateIcon,
Spinner,
} from '@patternfly/react-core';
import { ExclamationCircleIcon, PlusCircleIcon } from '@patternfly/react-icons';
import { useGetExecutionsList } from '~/concepts/pipelines/apiHooks/mlmd/useGetExecutionsList';
import ExecutionsTable from '~/pages/pipelines/global/experiments/executions/ExecutionsTable';
import { useMlmdListContext } from '~/concepts/pipelines/context';

const ExecutionsList: React.FC = () => {
const { filterQuery } = useMlmdListContext();
const [executionsResponse, isExecutionsLoaded, executionsError] = useGetExecutionsList();
const { executions, nextPageToken } = executionsResponse || { executions: [] };
const filterQueryRef = React.useRef(filterQuery);

if (executionsError) {
return (
<Bullseye>
<EmptyState>
<EmptyStateHeader
titleText="There was an issue loading executions"
icon={<EmptyStateIcon icon={ExclamationCircleIcon} />}
headingLevel="h2"
/>
<EmptyStateBody>{executionsError.message}</EmptyStateBody>
</EmptyState>
</Bullseye>
);
}

if (!isExecutionsLoaded) {
return (
<Bullseye>
<Spinner />
</Bullseye>
);
}

if (!executions.length && !filterQuery && filterQueryRef.current === filterQuery) {
return (
<EmptyState data-testid="global-no-executions">
<EmptyStateHeader
titleText="No executions"
icon={<EmptyStateIcon icon={PlusCircleIcon} />}
headingLevel="h4"
/>
<EmptyStateBody>
No experiments have been executed within this project. Select a different project, or
execute an experiment from the <b>Experiments and runs</b> page.
</EmptyStateBody>
</EmptyState>
);
}

return (
<ExecutionsTable
executions={executions}
nextPageToken={nextPageToken}
isLoaded={isExecutionsLoaded}
/>
);
};
export default ExecutionsList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as React from 'react';
import { TableBase } from '~/components/table';
import { Execution } from '~/third_party/mlmd';
import ExecutionsTableRow from '~/pages/pipelines/global/experiments/executions/ExecutionsTableRow';
import { executionColumns } from '~/pages/pipelines/global/experiments/executions/columns';
import { useMlmdListContext } from '~/concepts/pipelines/context';
import { initialFilterData } from '~/pages/pipelines/global/experiments/executions/const';
import ExecutionsTableToolbar from '~/pages/pipelines/global/experiments/executions/ExecutionsTableToolbar';
import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView';

interface ExecutionsTableProps {
executions: Execution[];
nextPageToken: string | undefined;
isLoaded: boolean;
}

const ExecutionsTable: React.FC<ExecutionsTableProps> = ({
executions,
nextPageToken,
isLoaded,
}) => {
const {
maxResultSize,
setPageToken: setRequestToken,
setMaxResultSize,
} = useMlmdListContext(nextPageToken);

const [page, setPage] = React.useState(1);
const [filterData, setFilterData] = React.useState(initialFilterData);
const [pageTokens, setPageTokens] = React.useState<Record<number, string>>({});

const onClearFilters = React.useCallback(() => setFilterData(initialFilterData), [setFilterData]);

const onNextPageClick = React.useCallback(
(_: React.SyntheticEvent<HTMLButtonElement>, nextPage: number) => {
if (nextPageToken) {
setPageTokens((prevTokens) => ({ ...prevTokens, [nextPage]: nextPageToken }));
setRequestToken(nextPageToken);
setPage(nextPage);
}
},
[nextPageToken, setRequestToken],
);

const onPrevPageClick = React.useCallback(
(_: React.SyntheticEvent<HTMLButtonElement>, prevPage: number) => {
if (pageTokens[prevPage]) {
setRequestToken(pageTokens[prevPage]);
setPage(prevPage);
} else {
setRequestToken(undefined);
}
},
[pageTokens, setRequestToken],
);

return (
<TableBase
variant="compact"
loading={!isLoaded}
enablePagination="compact"
data={executions}
columns={executionColumns}
data-testid="executions-list-table"
rowRenderer={(execution) => <ExecutionsTableRow key={execution.getId()} obj={execution} />}
toggleTemplate={() => <>{maxResultSize} per page </>}
toolbarContent={
<ExecutionsTableToolbar
filterData={filterData}
setFilterData={setFilterData}
onClearFilters={onClearFilters}
/>
}
page={page}
perPage={maxResultSize}
disableItemCount
onNextClick={onNextPageClick}
onPreviousClick={onPrevPageClick}
onPerPageSelect={(_, newSize) => {
setMaxResultSize(newSize);
}}
onSetPage={(_, newPage) => {
if (newPage < page || !isLoaded) {
setPage(newPage);
}
}}
emptyTableView={<DashboardEmptyTableView onClearFilters={onClearFilters} />}
id="executions-list-table"
/>
);
};

export default ExecutionsTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';
import { Td, Tr } from '@patternfly/react-table';
import { Execution } from '~/third_party/mlmd';
import ExecutionsTableRowStatusIcon from '~/pages/pipelines/global/experiments/executions/ExecutionsTableRowStatusIcon';

type ExecutionsTableRowProps = {
obj: Execution;
};

const ExecutionsTableRow: React.FC<ExecutionsTableRowProps> = ({ obj }) => (
<Tr>
<Td dataLabel="Executions">
{obj.getCustomPropertiesMap().get('task_name')?.getStringValue() || '(No name)'}
</Td>
<Td dataLabel="Status">
<ExecutionsTableRowStatusIcon status={obj.getLastKnownState()} />
</Td>
<Td dataLabel="ID">{obj.getId()}</Td>
<Td dataLabel="Type">{obj.getType()}</Td>
</Tr>
);

export default ExecutionsTableRow;
Loading
Loading