mirror of
https://github.com/portainer/portainer.git
synced 2025-08-06 14:25:31 +02:00
fix(app/edge-jobs): edge job results page crash at scale (#954)
This commit is contained in:
parent
d306d7a983
commit
a472de1919
27 changed files with 2595 additions and 107 deletions
|
@ -1,18 +1,16 @@
|
|||
import { List } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||
import { queryOptionsFromTableState } from '@/react/common/api/listQueryParams';
|
||||
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { withMeta } from '@@/datatables/extend-options/withMeta';
|
||||
import { mergeOptions } from '@@/datatables/extend-options/mergeOptions';
|
||||
|
||||
import { EdgeJob, JobResult, LogsStatus } from '../../types';
|
||||
import { EdgeJob, LogsStatus } from '../../types';
|
||||
import { useJobResults } from '../../queries/jobResults/useJobResults';
|
||||
|
||||
import { columns } from './columns';
|
||||
import { columns, sortOptions } from './columns';
|
||||
import { createStore } from './datatable-store';
|
||||
|
||||
const tableKey = 'edge-job-results';
|
||||
|
@ -22,8 +20,9 @@ export function ResultsDatatable({ jobId }: { jobId: EdgeJob['Id'] }) {
|
|||
const tableState = useTableState(store, tableKey);
|
||||
|
||||
const jobResultsQuery = useJobResults(jobId, {
|
||||
...queryOptionsFromTableState({ ...tableState }, sortOptions),
|
||||
refetchInterval(dataset) {
|
||||
const anyCollecting = dataset?.some(
|
||||
const anyCollecting = dataset?.data.some(
|
||||
(r) => r.LogsStatus === LogsStatus.Pending
|
||||
);
|
||||
|
||||
|
@ -35,40 +34,17 @@ export function ResultsDatatable({ jobId }: { jobId: EdgeJob['Id'] }) {
|
|||
},
|
||||
});
|
||||
|
||||
const environmentIds = jobResultsQuery.data?.map(
|
||||
(result) => result.EndpointId
|
||||
);
|
||||
|
||||
const environmentsQuery = useEnvironmentList(
|
||||
{ endpointIds: environmentIds },
|
||||
{ enabled: !!environmentIds && !jobResultsQuery.isLoading }
|
||||
);
|
||||
|
||||
const dataset = useMemo(
|
||||
() =>
|
||||
jobResultsQuery.isLoading || environmentsQuery.isLoading
|
||||
? []
|
||||
: associateEndpointsToResults(
|
||||
jobResultsQuery.data || [],
|
||||
environmentsQuery.environments
|
||||
),
|
||||
[
|
||||
environmentsQuery.environments,
|
||||
environmentsQuery.isLoading,
|
||||
jobResultsQuery.data,
|
||||
jobResultsQuery.isLoading,
|
||||
]
|
||||
);
|
||||
const dataset = jobResultsQuery.data?.data || [];
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
disableSelect
|
||||
columns={columns}
|
||||
dataset={dataset}
|
||||
isLoading={jobResultsQuery.isLoading || environmentsQuery.isLoading}
|
||||
title="Results"
|
||||
titleIcon={List}
|
||||
columns={columns}
|
||||
disableSelect
|
||||
dataset={dataset}
|
||||
settingsManager={tableState}
|
||||
isLoading={jobResultsQuery.isLoading}
|
||||
extendTableOptions={mergeOptions(
|
||||
withMeta({
|
||||
table: 'edge-job-results',
|
||||
|
@ -76,21 +52,11 @@ export function ResultsDatatable({ jobId }: { jobId: EdgeJob['Id'] }) {
|
|||
})
|
||||
)}
|
||||
data-cy="edge-job-results-datatable"
|
||||
isServerSidePagination
|
||||
page={tableState.page}
|
||||
onPageChange={tableState.setPage}
|
||||
onSearchChange={() => tableState.setPage(0)}
|
||||
totalCount={jobResultsQuery.data?.totalCount || 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function associateEndpointsToResults(
|
||||
results: Array<JobResult>,
|
||||
environments: Array<Environment>
|
||||
) {
|
||||
return results.map((result) => {
|
||||
const environment = environments.find(
|
||||
(environment) => environment.Id === result.EndpointId
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
Endpoint: environment,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import { CellContext, createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { sortOptionsFromColumns } from '@/react/common/api/sort.types';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { LogsStatus } from '../../types';
|
||||
import { JobResult, LogsStatus } from '../../types';
|
||||
import { useDownloadLogsMutation } from '../../queries/jobResults/useDownloadLogsMutation';
|
||||
import { useClearLogsMutation } from '../../queries/jobResults/useClearLogsMutation';
|
||||
import { useCollectLogsMutation } from '../../queries/jobResults/useCollectLogsMutation';
|
||||
|
||||
import { DecoratedJobResult, getTableMeta } from './types';
|
||||
import { getTableMeta } from './types';
|
||||
|
||||
const columnHelper = createColumnHelper<DecoratedJobResult>();
|
||||
const columnHelper = createColumnHelper<JobResult>();
|
||||
|
||||
export const columns = [
|
||||
columnHelper.accessor('Endpoint.Name', {
|
||||
columnHelper.accessor('EndpointName', {
|
||||
header: 'Environment',
|
||||
meta: {
|
||||
className: 'w-1/2',
|
||||
|
@ -30,7 +32,7 @@ export const columns = [
|
|||
function ActionsCell({
|
||||
row: { original: item },
|
||||
table,
|
||||
}: CellContext<DecoratedJobResult, unknown>) {
|
||||
}: CellContext<JobResult, unknown>) {
|
||||
const tableMeta = getTableMeta(table.options.meta);
|
||||
const id = tableMeta.jobId;
|
||||
|
||||
|
@ -51,13 +53,13 @@ function ActionsCell({
|
|||
<>
|
||||
<Button
|
||||
onClick={() => downloadLogsMutation.mutate(item.EndpointId)}
|
||||
data-cy={`edge-job-download-logs-${item.Endpoint?.Name}`}
|
||||
data-cy={`edge-job-download-logs-${item.EndpointName}`}
|
||||
>
|
||||
Download logs
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => clearLogsMutations.mutate(item.EndpointId)}
|
||||
data-cy={`edge-job-clear-logs-${item.Endpoint?.Name}`}
|
||||
data-cy={`edge-job-clear-logs-${item.EndpointName}`}
|
||||
>
|
||||
Clear logs
|
||||
</Button>
|
||||
|
@ -68,10 +70,12 @@ function ActionsCell({
|
|||
return (
|
||||
<Button
|
||||
onClick={() => collectLogsMutation.mutate(item.EndpointId)}
|
||||
data-cy={`edge-job-retrieve-logs-${item.Endpoint?.Name}`}
|
||||
data-cy={`edge-job-retrieve-logs-${item.EndpointName}`}
|
||||
>
|
||||
Retrieve logs
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const sortOptions = sortOptionsFromColumns(columns);
|
||||
|
|
|
@ -3,12 +3,18 @@ import {
|
|||
createPersistedStore,
|
||||
BasicTableSettings,
|
||||
RefreshableTableSettings,
|
||||
BackendPaginationTableSettings,
|
||||
backendPaginationSettings,
|
||||
} from '@@/datatables/types';
|
||||
|
||||
interface TableSettings extends BasicTableSettings, RefreshableTableSettings {}
|
||||
interface TableSettings
|
||||
extends BasicTableSettings,
|
||||
RefreshableTableSettings,
|
||||
BackendPaginationTableSettings {}
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return createPersistedStore<TableSettings>(storageKey, undefined, (set) => ({
|
||||
...refreshableSettings(set),
|
||||
...backendPaginationSettings(set),
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import { Environment } from '@/react/portainer/environments/types';
|
||||
|
||||
import { EdgeJob, JobResult } from '../../types';
|
||||
|
||||
export interface DecoratedJobResult extends JobResult {
|
||||
Endpoint?: Environment;
|
||||
}
|
||||
import { EdgeJob } from '../../types';
|
||||
|
||||
interface TableMeta {
|
||||
table: 'edge-job-results';
|
||||
|
|
|
@ -1,35 +1,54 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import {
|
||||
PaginatedResults,
|
||||
withPaginationHeaders,
|
||||
} from '@/react/common/api/pagination.types';
|
||||
import {
|
||||
BaseQueryOptions,
|
||||
BaseQueryParams,
|
||||
queryParamsFromQueryOptions,
|
||||
} from '@/react/common/api/listQueryParams';
|
||||
|
||||
import { EdgeJob, JobResult } from '../../types';
|
||||
import { sortOptions } from '../../ItemView/ResultsDatatable/columns';
|
||||
|
||||
import { queryKeys } from './query-keys';
|
||||
import { buildUrl } from './build-url';
|
||||
|
||||
type QueryOptions = BaseQueryOptions<typeof sortOptions>;
|
||||
|
||||
type RefetchInterval =
|
||||
| number
|
||||
| false
|
||||
| ((data: PaginatedResults<Array<JobResult>> | undefined) => number | false);
|
||||
|
||||
export function useJobResults(
|
||||
id: EdgeJob['Id'],
|
||||
{
|
||||
refetchInterval,
|
||||
...query
|
||||
}: {
|
||||
refetchInterval?:
|
||||
| number
|
||||
| false
|
||||
| ((data: Array<JobResult> | undefined) => number | false);
|
||||
} = {}
|
||||
refetchInterval?: RefetchInterval;
|
||||
} & QueryOptions = {}
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.base(id),
|
||||
queryFn: () => getJobResults(id),
|
||||
queryKey: [...queryKeys.base(id), query],
|
||||
queryFn: () => getJobResults(id, queryParamsFromQueryOptions(query)),
|
||||
refetchInterval,
|
||||
});
|
||||
}
|
||||
|
||||
async function getJobResults(id: EdgeJob['Id']) {
|
||||
try {
|
||||
const { data } = await axios.get<Array<JobResult>>(buildUrl({ id }));
|
||||
type QueryParams = BaseQueryParams<typeof sortOptions>;
|
||||
|
||||
return data;
|
||||
async function getJobResults(id: EdgeJob['Id'], params?: QueryParams) {
|
||||
try {
|
||||
const response = await axios.get<Array<JobResult>>(
|
||||
`edge_jobs/${id}/tasks`,
|
||||
{ params }
|
||||
);
|
||||
|
||||
return withPaginationHeaders(response);
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err, 'Failed fetching edge job results');
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentId,
|
||||
} from '@/react/portainer/environments/types';
|
||||
|
||||
export interface EdgeJob {
|
||||
Id: number;
|
||||
|
@ -28,5 +31,6 @@ interface EndpointMeta {
|
|||
export interface JobResult {
|
||||
Id: string;
|
||||
EndpointId: EnvironmentId;
|
||||
EndpointName: Environment['Name'];
|
||||
LogsStatus: LogsStatus;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue