Skip to content

Commit

Permalink
Merge pull request #2753 from DaoDaoNoCode/jira-rhoaieng-4776
Browse files Browse the repository at this point in the history
Executions empty state and table
  • Loading branch information
openshift-merge-bot[bot] authored Apr 29, 2024
2 parents 9d58a69 + 155d5b0 commit 84ac502
Show file tree
Hide file tree
Showing 18 changed files with 603 additions and 8 deletions.
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

0 comments on commit 84ac502

Please sign in to comment.