From 8ab739adfd7ef7b018816f2e0f6e61baec1181d8 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 28 Aug 2023 14:40:58 +0200 Subject: [PATCH] refactor(docker/services): convert service tasks table to react [EE-4337] close [EE-4337] --- .../serviceTasksDatatable.html | 109 ------------------ .../serviceTasksDatatable.js | 15 --- .../serviceTasksDatatableController.js | 94 --------------- .../services-datatable/servicesDatatable.html | 11 +- .../tasks-datatable/tasksDatatable.html | 8 +- app/docker/filters/filters.js | 19 +-- app/docker/filters/utils.ts | 36 ++++++ app/docker/models/task.js | 14 --- app/docker/models/task.ts | 36 ++++++ app/docker/react/components/index.ts | 5 +- app/docker/react/components/services.ts | 21 ++++ .../components/datatables/NestedDatatable.tsx | 11 +- .../ContainerQuickActions.tsx | 69 +++-------- .../docker/proxy/queries/nodes/build-url.ts | 15 +++ .../docker/proxy/queries/nodes/query-keys.ts | 8 ++ .../docker/proxy/queries/nodes/useNodes.ts | 21 ++++ app/react/docker/proxy/queries/query-keys.ts | 6 + .../TasksDatatable/TasksDatatable.tsx | 21 ++++ .../TasksDatatable/columns/actions.tsx | 45 ++++++++ .../TasksDatatable/columns/helper.ts | 5 + .../TasksDatatable/columns/index.ts | 19 +++ .../TasksDatatable/columns/node.tsx | 32 +++++ .../TasksDatatable/columns/status.tsx | 27 +++++ .../TasksDatatable/columns/task.tsx | 47 ++++++++ .../ServicesDatatable/TasksDatatable/index.ts | 1 + .../ServicesDatatable/TasksDatatable/types.ts | 6 + .../services/common/TaskTableQuickActions.tsx | 46 ++++++++ package.json | 1 + yarn.lock | 90 ++++++++++++++- 29 files changed, 511 insertions(+), 327 deletions(-) delete mode 100644 app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html delete mode 100644 app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.js delete mode 100644 app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js delete mode 100644 app/docker/models/task.js create mode 100644 app/docker/models/task.ts create mode 100644 app/docker/react/components/services.ts create mode 100644 app/react/docker/proxy/queries/nodes/build-url.ts create mode 100644 app/react/docker/proxy/queries/nodes/query-keys.ts create mode 100644 app/react/docker/proxy/queries/nodes/useNodes.ts create mode 100644 app/react/docker/proxy/queries/query-keys.ts create mode 100644 app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/TasksDatatable.tsx create mode 100644 app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/actions.tsx create mode 100644 app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/helper.ts create mode 100644 app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/index.ts create mode 100644 app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/node.tsx create mode 100644 app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/status.tsx create mode 100644 app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/task.tsx create mode 100644 app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/index.ts create mode 100644 app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/types.ts create mode 100644 app/react/docker/services/common/TaskTableQuickActions.tsx diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html deleted file mode 100644 index e98d84bea..000000000 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html +++ /dev/null @@ -1,109 +0,0 @@ -
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - Filter - - - Filter - - - - -
-
TaskActions - - - - - -
- {{ item.Status.State }} - - {{ item.Id }} - {{ - item.Id - }} - - - - {{ item.Slot ? item.Slot : '-' }}{{ item.NodeId | tasknodename : $ctrl.nodes }}{{ item.Updated | getisodate }}
No task matching filter.
-
diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.js b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.js deleted file mode 100644 index 8bfdd2a72..000000000 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.js +++ /dev/null @@ -1,15 +0,0 @@ -angular.module('portainer.docker').component('serviceTasksDatatable', { - templateUrl: './serviceTasksDatatable.html', - controller: 'ServiceTasksDatatableController', - bindings: { - dataset: '<', - serviceId: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - nodes: '<', - agentProxy: '<', - textFilter: '=', - showTaskLogsButton: '<', - }, -}); diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js deleted file mode 100644 index 437732bdc..000000000 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js +++ /dev/null @@ -1,94 +0,0 @@ -import _ from 'lodash-es'; - -angular.module('portainer.docker').controller('ServiceTasksDatatableController', [ - '$scope', - '$controller', - 'DatatableService', - function ($scope, $controller, DatatableService) { - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - - var ctrl = this; - - this.state = Object.assign(this.state, { - showQuickActionStats: true, - showQuickActionLogs: true, - showQuickActionConsole: true, - showQuickActionInspect: true, - showQuickActionExec: true, - showQuickActionAttach: false, - }); - - this.filters = { - state: { - open: false, - enabled: false, - values: [], - }, - }; - - this.applyFilters = function (item) { - var filters = ctrl.filters; - for (var i = 0; i < filters.state.values.length; i++) { - var filter = filters.state.values[i]; - if (item.Status.State === filter.label && filter.display) { - return true; - } - } - return false; - }; - - this.onStateFilterChange = function () { - var filters = this.filters.state.values; - var filtered = false; - for (var i = 0; i < filters.length; i++) { - var filter = filters[i]; - if (!filter.display) { - filtered = true; - } - } - this.filters.state.enabled = filtered; - }; - - this.prepareTableFromDataset = function () { - var availableStateFilters = []; - for (var i = 0; i < this.dataset.length; i++) { - var item = this.dataset[i]; - availableStateFilters.push({ label: item.Status.State, display: true }); - } - this.filters.state.values = _.uniqBy(availableStateFilters, 'label'); - }; - - this.$onInit = function () { - this.setDefaults(); - this.prepareTableFromDataset(); - - this.state.orderBy = this.orderBy; - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); - if (storedOrder !== null) { - this.state.reverseOrder = storedOrder.reverse; - this.state.orderBy = storedOrder.orderBy; - } - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; - this.onTextFilterChange(); - } - - var storedFilters = DatatableService.getDataTableFilters(this.tableKey); - if (storedFilters !== null) { - this.filters = storedFilters; - } - if (this.filters && this.filters.state) { - this.filters.state.open = false; - } - - var storedSettings = DatatableService.getDataTableSettings(this.tableKey); - if (storedSettings !== null) { - this.settings = storedSettings; - this.settings.open = false; - } - this.onSettingsRepeaterChange(); - }; - }, -]); diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html index ddb0274bc..2e8d180b7 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.html +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.html @@ -231,16 +231,7 @@ - + diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatable.html b/app/docker/components/datatables/tasks-datatable/tasksDatatable.html index 7af8b6fd1..ea2a4e679 100644 --- a/app/docker/components/datatables/tasks-datatable/tasksDatatable.html +++ b/app/docker/components/datatables/tasks-datatable/tasksDatatable.html @@ -89,13 +89,7 @@ > - + = []) { return command.join(' '); } + +export function taskStatusBadge(text?: TaskState) { + const status = _.toLower(text); + if ( + [ + 'new', + 'allocated', + 'assigned', + 'accepted', + 'preparing', + 'ready', + 'starting', + 'remove', + ].includes(status) + ) { + return 'info'; + } + + if (['pending'].includes(status)) { + return 'warning'; + } + + if (['shutdown', 'failed', 'rejected', 'orphaned'].includes(status)) { + return 'danger'; + } + + if (['complete'].includes(status)) { + return 'primary'; + } + + if (['running'].includes(status)) { + return 'success'; + } + return 'default'; +} diff --git a/app/docker/models/task.js b/app/docker/models/task.js deleted file mode 100644 index 6e16b3a56..000000000 --- a/app/docker/models/task.js +++ /dev/null @@ -1,14 +0,0 @@ -export function TaskViewModel(data) { - this.Id = data.ID; - this.Created = data.CreatedAt; - this.Updated = data.UpdatedAt; - this.Slot = data.Slot; - this.Spec = data.Spec; - this.Status = data.Status; - this.DesiredState = data.DesiredState; - this.ServiceId = data.ServiceID; - this.NodeId = data.NodeID; - if (data.Status && data.Status.ContainerStatus && data.Status.ContainerStatus.ContainerID) { - this.ContainerId = data.Status.ContainerStatus.ContainerID; - } -} diff --git a/app/docker/models/task.ts b/app/docker/models/task.ts new file mode 100644 index 000000000..c67a41171 --- /dev/null +++ b/app/docker/models/task.ts @@ -0,0 +1,36 @@ +import { Task, TaskSpec, TaskState } from 'docker-types/generated/1.41'; + +export class TaskViewModel { + Id: string; + + Created: string; + + Updated: string; + + Slot: number; + + Spec?: TaskSpec; + + Status: Task['Status']; + + DesiredState: TaskState; + + ServiceId: string; + + NodeId: string; + + ContainerId: string = ''; + + constructor(data: Task) { + this.Id = data.ID || ''; + this.Created = data.CreatedAt || ''; + this.Updated = data.UpdatedAt || ''; + this.Slot = data.Slot || 0; + this.Spec = data.Spec; + this.Status = data.Status; + this.DesiredState = data.DesiredState || 'pending'; + this.ServiceId = data.ServiceID || ''; + this.NodeId = data.NodeID || ''; + this.ContainerId = data.Status?.ContainerStatus?.ContainerID || ''; + } +} diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index df5338c60..781281e7f 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -21,8 +21,10 @@ import { ConfigsDatatable } from '@/react/docker/configs/ListView/ConfigsDatatab import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowser'; import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser'; +import { servicesModule } from './services'; + const ngModule = angular - .module('portainer.docker.react.components', []) + .module('portainer.docker.react.components', [servicesModule]) .component('dockerfileDetails', r2a(DockerfileDetails, ['image'])) .component('dockerHealthStatus', r2a(HealthStatus, ['health'])) .component( @@ -32,7 +34,6 @@ const ngModule = angular 'nodeName', 'state', 'status', - 'taskId', ]) ) .component('templateListDropdown', TemplateListDropdownAngular) diff --git a/app/docker/react/components/services.ts b/app/docker/react/components/services.ts new file mode 100644 index 000000000..ec4634823 --- /dev/null +++ b/app/docker/react/components/services.ts @@ -0,0 +1,21 @@ +import angular from 'angular'; + +import { r2a } from '@/react-tools/react2angular'; +import { withUIRouter } from '@/react-tools/withUIRouter'; +import { TasksDatatable } from '@/react/docker/services/ListView/ServicesDatatable/TasksDatatable'; +import { withCurrentUser } from '@/react-tools/withCurrentUser'; +import { TaskTableQuickActions } from '@/react/docker/services/common/TaskTableQuickActions'; + +export const servicesModule = angular + .module('portainer.docker.react.components.services', []) + .component( + 'dockerServiceTasksDatatable', + r2a(withUIRouter(withCurrentUser(TasksDatatable)), ['dataset', 'search']) + ) + .component( + 'dockerTaskTableQuickActions', + r2a(withUIRouter(withCurrentUser(TaskTableQuickActions)), [ + 'state', + 'taskId', + ]) + ).name; diff --git a/app/react/components/datatables/NestedDatatable.tsx b/app/react/components/datatables/NestedDatatable.tsx index 1b0eaaa3b..6569435d5 100644 --- a/app/react/components/datatables/NestedDatatable.tsx +++ b/app/react/components/datatables/NestedDatatable.tsx @@ -23,6 +23,11 @@ interface Props { initialTableState?: Partial; isLoading?: boolean; initialSortBy?: BasicTableSettings['sortBy']; + + /** + * keyword to filter by + */ + search?: string; } export function NestedDatatable({ @@ -33,6 +38,7 @@ export function NestedDatatable({ initialTableState = {}, isLoading, initialSortBy, + search, }: Props) { const tableInstance = useReactTable({ columns, @@ -45,6 +51,9 @@ export function NestedDatatable({ enableColumnFilter: false, enableHiding: false, }, + state: { + globalFilter: search, + }, getRowId, autoResetExpanded: false, getCoreRowModel: getCoreRowModel(), @@ -55,7 +64,7 @@ export function NestedDatatable({ return ( - + tableInstance={tableInstance} isLoading={isLoading} diff --git a/app/react/docker/containers/components/ContainerQuickActions/ContainerQuickActions.tsx b/app/react/docker/containers/components/ContainerQuickActions/ContainerQuickActions.tsx index 81ac473d8..891f40a7a 100644 --- a/app/react/docker/containers/components/ContainerQuickActions/ContainerQuickActions.tsx +++ b/app/react/docker/containers/components/ContainerQuickActions/ContainerQuickActions.tsx @@ -9,7 +9,7 @@ import { Link } from '@@/Link'; import styles from './ContainerQuickActions.module.css'; -interface QuickActionsState { +export interface QuickActionsState { showQuickActionAttach: boolean; showQuickActionExec: boolean; showQuickActionInspect: boolean; @@ -17,31 +17,25 @@ interface QuickActionsState { showQuickActionStats: boolean; } -interface Props { - taskId?: string; - containerId?: string; - nodeName: string; - state: QuickActionsState; - status: ContainerStatus; -} - export function ContainerQuickActions({ - taskId, + status, containerId, nodeName, state, - status, -}: Props) { - if (taskId) { - return ; - } - - const isActive = [ - ContainerStatus.Starting, - ContainerStatus.Running, - ContainerStatus.Healthy, - ContainerStatus.Unhealthy, - ].includes(status); +}: { + containerId: string; + nodeName: string; + status: ContainerStatus; + state: QuickActionsState; +}) { + const isActive = + !!status && + [ + ContainerStatus.Starting, + ContainerStatus.Running, + ContainerStatus.Healthy, + ContainerStatus.Unhealthy, + ].includes(status); return (
@@ -107,34 +101,3 @@ export function ContainerQuickActions({
); } - -interface TaskProps { - taskId: string; - state: QuickActionsState; -} - -function TaskQuickActions({ taskId, state }: TaskProps) { - return ( -
- {state.showQuickActionLogs && ( - - - - - - )} - - {state.showQuickActionInspect && ( - - - - - - )} -
- ); -} diff --git a/app/react/docker/proxy/queries/nodes/build-url.ts b/app/react/docker/proxy/queries/nodes/build-url.ts new file mode 100644 index 000000000..2c422d18e --- /dev/null +++ b/app/react/docker/proxy/queries/nodes/build-url.ts @@ -0,0 +1,15 @@ +import { EnvironmentId } from '@/react/portainer/environments/types'; + +import { buildUrl as buildProxyUrl } from '../build-url'; + +export function buildUrl( + environmentId: EnvironmentId, + action?: string, + subAction = '' +) { + return buildProxyUrl( + environmentId, + 'nodes', + subAction ? `${action}/${subAction}` : action + ); +} diff --git a/app/react/docker/proxy/queries/nodes/query-keys.ts b/app/react/docker/proxy/queries/nodes/query-keys.ts new file mode 100644 index 000000000..3f599e1d8 --- /dev/null +++ b/app/react/docker/proxy/queries/nodes/query-keys.ts @@ -0,0 +1,8 @@ +import { EnvironmentId } from '@/react/portainer/environments/types'; + +import { queryKeys as proxyQueryKeys } from '../query-keys'; + +export const queryKeys = { + base: (environmentId: EnvironmentId) => + [...proxyQueryKeys.base(environmentId), 'nodes'] as const, +}; diff --git a/app/react/docker/proxy/queries/nodes/useNodes.ts b/app/react/docker/proxy/queries/nodes/useNodes.ts new file mode 100644 index 000000000..cc52ffae1 --- /dev/null +++ b/app/react/docker/proxy/queries/nodes/useNodes.ts @@ -0,0 +1,21 @@ +import { Node } from 'docker-types/generated/1.41'; +import { useQuery } from 'react-query'; + +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { EnvironmentId } from '@/react/portainer/environments/types'; + +import { buildUrl } from './build-url'; +import { queryKeys } from './query-keys'; + +export function useNodes(environmentId: EnvironmentId) { + return useQuery(queryKeys.base(environmentId), () => getNodes(environmentId)); +} + +async function getNodes(environmentId: EnvironmentId) { + try { + const { data } = await axios.get>(buildUrl(environmentId)); + return data; + } catch (error) { + throw parseAxiosError(error, 'Unable to retrieve nodes'); + } +} diff --git a/app/react/docker/proxy/queries/query-keys.ts b/app/react/docker/proxy/queries/query-keys.ts new file mode 100644 index 000000000..2439c835e --- /dev/null +++ b/app/react/docker/proxy/queries/query-keys.ts @@ -0,0 +1,6 @@ +import { EnvironmentId } from '@/react/portainer/environments/types'; + +export const queryKeys = { + base: (environmentId: EnvironmentId) => + [environmentId, 'docker', 'proxy'] as const, +}; diff --git a/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/TasksDatatable.tsx b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/TasksDatatable.tsx new file mode 100644 index 000000000..8f1e906fd --- /dev/null +++ b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/TasksDatatable.tsx @@ -0,0 +1,21 @@ +import { NestedDatatable } from '@@/datatables/NestedDatatable'; + +import { columns } from './columns'; +import { DecoratedTask } from './types'; + +export function TasksDatatable({ + dataset, + search, +}: { + dataset: DecoratedTask[]; + search?: string; +}) { + return ( + + ); +} diff --git a/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/actions.tsx b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/actions.tsx new file mode 100644 index 000000000..fe672dc74 --- /dev/null +++ b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/actions.tsx @@ -0,0 +1,45 @@ +import { CellContext } from '@tanstack/react-table'; + +import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions'; +import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment'; +import { isAgentEnvironment } from '@/react/portainer/environments/utils'; +import { QuickActionsState } from '@/react/docker/containers/components/ContainerQuickActions/ContainerQuickActions'; +import { TaskTableQuickActions } from '@/react/docker/services/common/TaskTableQuickActions'; + +import { DecoratedTask } from '../types'; + +import { columnHelper } from './helper'; + +export const actions = columnHelper.display({ + header: 'Actions', + cell: Cell, +}); + +function Cell({ + row: { original: item }, +}: CellContext) { + const environmentQuery = useCurrentEnvironment(); + + if (!environmentQuery.data) { + return null; + } + const state: QuickActionsState = { + showQuickActionAttach: true, + showQuickActionExec: true, + showQuickActionInspect: true, + showQuickActionLogs: true, + showQuickActionStats: true, + }; + const isAgent = isAgentEnvironment(environmentQuery.data.Type); + + return isAgent && item.Container ? ( + + ) : ( + + ); +} diff --git a/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/helper.ts b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/helper.ts new file mode 100644 index 000000000..34343b077 --- /dev/null +++ b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/helper.ts @@ -0,0 +1,5 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { DecoratedTask } from '../types'; + +export const columnHelper = createColumnHelper(); diff --git a/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/index.ts b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/index.ts new file mode 100644 index 000000000..aaca2fa3f --- /dev/null +++ b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/index.ts @@ -0,0 +1,19 @@ +import { isoDate } from '@/portainer/filters/filters'; + +import { actions } from './actions'; +import { columnHelper } from './helper'; +import { node } from './node'; +import { status } from './status'; +import { task } from './task'; + +export const columns = [ + status, + task, + actions, + columnHelper.accessor((item) => item.Slot || '-', { header: 'Slot' }), + node, + columnHelper.accessor('Updated', { + header: 'Last Update', + cell: ({ getValue }) => isoDate(getValue()), + }), +]; diff --git a/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/node.tsx b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/node.tsx new file mode 100644 index 000000000..56ae02877 --- /dev/null +++ b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/node.tsx @@ -0,0 +1,32 @@ +import { Node } from 'docker-types/generated/1.41'; +import { CellContext } from '@tanstack/react-table'; + +import { useNodes } from '@/react/docker/proxy/queries/nodes/useNodes'; +import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; + +import { DecoratedTask } from '../types'; + +import { columnHelper } from './helper'; + +export const node = columnHelper.accessor('NodeId', { + header: 'Node', + cell: Cell, +}); + +function Cell({ getValue }: CellContext) { + const environmentId = useEnvironmentId(); + + const nodesQuery = useNodes(environmentId); + + const nodes = nodesQuery.data || []; + return getNodeName(getValue(), nodes); +} + +function getNodeName(nodeId: string, nodes: Array) { + const node = nodes.find((node) => node.ID === nodeId); + if (node?.Description?.Hostname) { + return node.Description.Hostname; + } + + return ''; +} diff --git a/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/status.tsx b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/status.tsx new file mode 100644 index 000000000..7f1bc39ea --- /dev/null +++ b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/status.tsx @@ -0,0 +1,27 @@ +import clsx from 'clsx'; + +import { taskStatusBadge } from '@/docker/filters/utils'; + +import { multiple } from '@@/datatables/filter-types'; +import { filterHOC } from '@@/datatables/Filter'; + +import { columnHelper } from './helper'; + +export const status = columnHelper.accessor((item) => item.Status?.State, { + header: 'Status', + enableColumnFilter: true, + filterFn: multiple, + meta: { + filter: filterHOC('Filter by state'), + width: 100, + }, + cell({ getValue }) { + const value = getValue(); + + return ( + + {value} + + ); + }, +}); diff --git a/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/task.tsx b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/task.tsx new file mode 100644 index 000000000..1a94f5947 --- /dev/null +++ b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/columns/task.tsx @@ -0,0 +1,47 @@ +import { CellContext } from '@tanstack/react-table'; + +import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment'; +import { isAgentEnvironment } from '@/react/portainer/environments/utils'; + +import { Link } from '@@/Link'; + +import { DecoratedTask } from '../types'; + +import { columnHelper } from './helper'; + +export const task = columnHelper.accessor('Id', { + header: 'Task', + cell: Cell, +}); + +function Cell({ + getValue, + row: { original: item }, +}: CellContext) { + const environmentQuery = useCurrentEnvironment(); + + if (!environmentQuery.data) { + return null; + } + + const value = getValue(); + const isAgent = isAgentEnvironment(environmentQuery.data.Type); + + return isAgent && item.Container ? ( + + {value} + + ) : ( + + {value} + + ); +} diff --git a/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/index.ts b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/index.ts new file mode 100644 index 000000000..f95acf3ac --- /dev/null +++ b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/index.ts @@ -0,0 +1 @@ +export { TasksDatatable } from './TasksDatatable'; diff --git a/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/types.ts b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/types.ts new file mode 100644 index 000000000..fdff024a9 --- /dev/null +++ b/app/react/docker/services/ListView/ServicesDatatable/TasksDatatable/types.ts @@ -0,0 +1,6 @@ +import { TaskViewModel } from '@/docker/models/task'; +import { DockerContainer } from '@/react/docker/containers/types'; + +export type DecoratedTask = TaskViewModel & { + Container?: DockerContainer; +}; diff --git a/app/react/docker/services/common/TaskTableQuickActions.tsx b/app/react/docker/services/common/TaskTableQuickActions.tsx new file mode 100644 index 000000000..367c4bc27 --- /dev/null +++ b/app/react/docker/services/common/TaskTableQuickActions.tsx @@ -0,0 +1,46 @@ +import { FileText, Info } from 'lucide-react'; + +import { Authorized } from '@/react/hooks/useUser'; + +import { Icon } from '@@/Icon'; +import { Link } from '@@/Link'; + +interface State { + showQuickActionInspect: boolean; + showQuickActionLogs: boolean; +} + +export function TaskTableQuickActions({ + taskId, + state = { + showQuickActionInspect: true, + showQuickActionLogs: true, + }, +}: { + taskId: string; + state?: State; +}) { + return ( +
+ {state.showQuickActionLogs && ( + + + + + + )} + + {state.showQuickActionInspect && ( + + + + + + )} +
+ ); +} diff --git a/package.json b/package.json index 1c7c8d179..6d14dec98 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "codemirror": "^6.0.1", "core-js": "^3.19.3", "date-fns": "^2.29.3", + "docker-types": "^1.42.2", "fast-json-patch": "^3.1.1", "file-saver": "^2.0.5", "filesize": "~3.3.0", diff --git a/yarn.lock b/yarn.lock index 7d5d20d7d..22789dd84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,7 +15,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@apidevtools/json-schema-ref-parser@^9.0.6": +"@apidevtools/json-schema-ref-parser@9.0.9", "@apidevtools/json-schema-ref-parser@^9.0.6": version "9.0.9" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" integrity sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w== @@ -6807,6 +6807,13 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +busboy@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -6879,7 +6886,7 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.2.0: +camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -8099,6 +8106,14 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +docker-types@^1.42.2: + version "1.42.2" + resolved "https://registry.yarnpkg.com/docker-types/-/docker-types-1.42.2.tgz#40a3626abf99030abe306966d51b3fdae9c77408" + integrity sha512-Il8PAGTZpgRu8vMg+MnRTAD/FdEsTN2LYEFLHhhmiAWdGYkJHxDHWYSeBIIQMR6pJ/biHaF9qsTnYsJHX3OPTw== + dependencies: + openapi-typescript "5.4.1" + openapi-typescript-codegen "^0.24.0" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -9383,7 +9398,7 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@11.1.1, fs-extra@^11.1.0: +fs-extra@11.1.1, fs-extra@^11.1.0, fs-extra@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== @@ -9653,6 +9668,11 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" +globalyzer@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" + integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== + globby@^11.0.1: version "11.0.4" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" @@ -9699,6 +9719,11 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -11313,6 +11338,13 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-ref-parser@^9.0.9: + version "9.0.9" + resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#66ea538e7450b12af342fa3d5b8458bc1e1e013f" + integrity sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q== + dependencies: + "@apidevtools/json-schema-ref-parser" "9.0.9" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -11897,6 +11929,11 @@ mime@^2.0.3: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -12543,6 +12580,29 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openapi-typescript-codegen@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/openapi-typescript-codegen/-/openapi-typescript-codegen-0.24.0.tgz#b3e6ade5bae75cd47868e5e3e4dc3bcf899cadab" + integrity sha512-rSt8t1XbMWhv6Db7GUI24NNli7FU5kzHLxcE8BpzgGWRdWyWt9IB2YoLyPahxNrVA7yOaVgnXPkrcTDRMQtJYg== + dependencies: + camelcase "^6.3.0" + commander "^10.0.0" + fs-extra "^11.1.1" + handlebars "^4.7.7" + json-schema-ref-parser "^9.0.9" + +openapi-typescript@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/openapi-typescript/-/openapi-typescript-5.4.1.tgz#38b4b45244acc1361f3c444537833a9e9cb03bf6" + integrity sha512-AGB2QiZPz4rE7zIwV3dRHtoUC/CWHhUjuzGXvtmMQN2AFV8xCTLKcZUHLcdPQmt/83i22nRE7+TxXOXkK+gf4Q== + dependencies: + js-yaml "^4.1.0" + mime "^3.0.0" + prettier "^2.6.2" + tiny-glob "^0.2.9" + undici "^5.4.0" + yargs-parser "^21.0.1" + opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" @@ -13326,7 +13386,7 @@ prettier-plugin-tailwindcss@^0.2.6: resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.8.tgz#e9c0356680331f909a86fefe8fc2b247c21e23a2" integrity sha512-KgPcEnJeIijlMjsA6WwYgRs5rh3/q76oInqtMXBA/EMcamrcYJpyhtRhyX1ayT9hnHlHTuO8sIifHF10WuSDKg== -prettier@^2.8.0, prettier@^2.8.8: +prettier@^2.6.2, prettier@^2.8.0, prettier@^2.8.8: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -14870,6 +14930,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + strict-event-emitter@^0.2.4, strict-event-emitter@^0.2.6: version "0.2.8" resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz#b4e768927c67273c14c13d20e19d5e6c934b47ca" @@ -15344,6 +15409,14 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tiny-glob@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" + integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== + dependencies: + globalyzer "0.1.0" + globrex "^0.1.2" + tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -15634,6 +15707,13 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= +undici@^5.4.0: + version "5.23.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.23.0.tgz#e7bdb0ed42cebe7b7aca87ced53e6eaafb8f8ca0" + integrity sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg== + dependencies: + busboy "^1.6.0" + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -16451,7 +16531,7 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.9: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.0, yargs-parser@^21.1.1: +yargs-parser@^21.0.0, yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==