mirror of https://github.com/portainer/portainer
refactor(docker/services): convert service tasks table to react [EE-4674] (#10188)
parent
c47a804c97
commit
c3d266931f
|
@ -1,109 +0,0 @@
|
|||
<div class="inner-datatable">
|
||||
<table class="table-condensed table-hover nowrap-cells table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open" class="w-[10%]">
|
||||
<div class="flex">
|
||||
<table-column-header
|
||||
col-title="'Status'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Status.State'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Status.State' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Status.State')"
|
||||
></table-column-header>
|
||||
<span class="space-left">
|
||||
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled"
|
||||
>Filter
|
||||
<pr-icon icon="'filter'"></pr-icon>
|
||||
</span>
|
||||
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled"
|
||||
>Filter
|
||||
<pr-icon icon="'check'"></pr-icon>
|
||||
</span>
|
||||
</span>
|
||||
<div class="dropdown-menu" uib-dropdown-menu>
|
||||
<div class="tableMenu">
|
||||
<div class="menuHeader"> Filter by state </div>
|
||||
<div class="menuContent">
|
||||
<div class="md-checkbox" ng-repeat="filter in $ctrl.filters.state.values track by $index">
|
||||
<input id="filter_state_{{ $ctrl.serviceId }}_{{ $index }}" type="checkbox" ng-model="filter.display" ng-change="$ctrl.onStateFilterChange()" />
|
||||
<label for="filter_state_{{ $ctrl.serviceId }}_{{ $index }}">{{ filter.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th style="width: 22%">Task</th>
|
||||
<th>Actions</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Slot'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Slot'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Slot' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Slot')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Node'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'NodeId'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'NodeId' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('NodeId')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
<th>
|
||||
<table-column-header
|
||||
col-title="'Last Update'"
|
||||
can-sort="true"
|
||||
is-sorted="$ctrl.state.orderBy === 'Updated'"
|
||||
is-sorted-desc="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"
|
||||
ng-click="$ctrl.changeOrderBy('Updated')"
|
||||
></table-column-header>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))"
|
||||
>
|
||||
<td class="text-center">
|
||||
<span class="label label-{{ item.Status.State | taskstatusbadge }} space-right">{{ item.Status.State }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="!$ctrl.agentProxy || !item.Container" ui-sref="docker.tasks.task({id: item.Id})" class="monospaced">{{ item.Id }}</a>
|
||||
<a ng-if="$ctrl.agentProxy && item.Container" ui-sref="docker.containers.container({ id: item.Container.Id, nodeName: item.Container.NodeName })" class="monospaced">{{
|
||||
item.Id
|
||||
}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<container-quick-actions
|
||||
ng-if="!$ctrl.agentProxy || !item.Container"
|
||||
container-id="item.ContainerId"
|
||||
task-id="item.Id"
|
||||
status="item.Status.State"
|
||||
state="$ctrl.state"
|
||||
></container-quick-actions>
|
||||
<container-quick-actions
|
||||
ng-if="$ctrl.agentProxy && item.Container"
|
||||
container-id="item.Container.Id"
|
||||
node-name="item.Container.NodeName"
|
||||
status="item.Status.State"
|
||||
state="$ctrl.state"
|
||||
></container-quick-actions>
|
||||
</td>
|
||||
<td>{{ item.Slot ? item.Slot : '-' }}</td>
|
||||
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
|
||||
<td>{{ item.Updated | getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="5" class="text-muted text-center">No task matching filter.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -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: '<',
|
||||
},
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -210,16 +210,7 @@
|
|||
<tr dir-paginate-end ng-show="item.Expanded">
|
||||
<td></td>
|
||||
<td colspan="8">
|
||||
<service-tasks-datatable
|
||||
dataset="item.Tasks"
|
||||
service-id="item.Id"
|
||||
table-key="service-tasks"
|
||||
order-by="Status.State"
|
||||
nodes="$ctrl.nodes"
|
||||
agent-proxy="$ctrl.agentProxy"
|
||||
show-task-logs-button="$ctrl.showTaskLogsButton"
|
||||
text-filter="$ctrl.state.textFilter"
|
||||
></service-tasks-datatable>
|
||||
<docker-service-tasks-datatable dataset="item.Tasks" search="$ctrl.state.textFilter"></docker-service-tasks-datatable>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
|
|
|
@ -89,13 +89,7 @@
|
|||
>
|
||||
</td>
|
||||
<td>
|
||||
<container-quick-actions
|
||||
ng-if="!$ctrl.agentProxy || !item.Container"
|
||||
container-id="item.ContainerId"
|
||||
task-id="item.Id"
|
||||
status="item.Status.State"
|
||||
state="$ctrl.state"
|
||||
></container-quick-actions>
|
||||
<task-table-quick-actions ng-if="!$ctrl.agentProxy || !item.Container" task-id="item.Id" state="$ctrl.state"></task-table-quick-actions>
|
||||
<container-quick-actions
|
||||
ng-if="$ctrl.agentProxy && item.Container"
|
||||
container-id="item.Container.Id"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import _ from 'lodash-es';
|
||||
import { joinCommand, trimSHA } from './utils';
|
||||
import { joinCommand, taskStatusBadge, trimSHA } from './utils';
|
||||
|
||||
function includeString(text, values) {
|
||||
return values.some(function (val) {
|
||||
|
@ -49,22 +49,7 @@ angular
|
|||
})
|
||||
.filter('taskstatusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
var status = _.toLower(text);
|
||||
var labelStyle = 'default';
|
||||
if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'preparing', 'ready', 'starting', 'remove'])) {
|
||||
labelStyle = 'info';
|
||||
} else if (includeString(status, ['pending'])) {
|
||||
labelStyle = 'warning';
|
||||
} else if (includeString(status, ['shutdown', 'failed', 'rejected', 'orphaned'])) {
|
||||
labelStyle = 'danger';
|
||||
} else if (includeString(status, ['complete'])) {
|
||||
labelStyle = 'primary';
|
||||
} else if (includeString(status, ['running'])) {
|
||||
labelStyle = 'success';
|
||||
}
|
||||
return labelStyle;
|
||||
};
|
||||
return taskStatusBadge;
|
||||
})
|
||||
.filter('taskhaslogs', function () {
|
||||
'use strict';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import _ from 'lodash';
|
||||
import { TaskState } from 'docker-types/generated/1.41';
|
||||
|
||||
export function trimSHA(imageName: string) {
|
||||
if (!imageName) {
|
||||
|
@ -17,3 +18,38 @@ export function joinCommand(command: null | Array<string> = []) {
|
|||
|
||||
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';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 || '';
|
||||
}
|
||||
}
|
|
@ -25,9 +25,13 @@ import { ScaleServiceButton } from '@/react/docker/services/ListView/ServicesDat
|
|||
import { SecretsDatatable } from '@/react/docker/secrets/ListView/SecretsDatatable';
|
||||
|
||||
import { containersModule } from './containers';
|
||||
import { servicesModule } from './services';
|
||||
|
||||
const ngModule = angular
|
||||
.module('portainer.docker.react.components', [containersModule])
|
||||
.module('portainer.docker.react.components', [
|
||||
containersModule,
|
||||
servicesModule,
|
||||
])
|
||||
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
|
||||
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
|
||||
.component(
|
||||
|
@ -37,7 +41,6 @@ const ngModule = angular
|
|||
'nodeName',
|
||||
'state',
|
||||
'status',
|
||||
'taskId',
|
||||
])
|
||||
)
|
||||
.component('templateListDropdown', TemplateListDropdownAngular)
|
||||
|
|
|
@ -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;
|
|
@ -23,6 +23,11 @@ interface Props<D extends DefaultType> {
|
|||
initialTableState?: Partial<TableState>;
|
||||
isLoading?: boolean;
|
||||
initialSortBy?: BasicTableSettings['sortBy'];
|
||||
|
||||
/**
|
||||
* keyword to filter by
|
||||
*/
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export function NestedDatatable<D extends DefaultType>({
|
||||
|
@ -33,6 +38,7 @@ export function NestedDatatable<D extends DefaultType>({
|
|||
initialTableState = {},
|
||||
isLoading,
|
||||
initialSortBy,
|
||||
search,
|
||||
}: Props<D>) {
|
||||
const tableInstance = useReactTable<D>({
|
||||
columns,
|
||||
|
@ -45,6 +51,9 @@ export function NestedDatatable<D extends DefaultType>({
|
|||
enableColumnFilter: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
state: {
|
||||
globalFilter: search,
|
||||
},
|
||||
getRowId,
|
||||
autoResetExpanded: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
|
@ -55,7 +64,7 @@ export function NestedDatatable<D extends DefaultType>({
|
|||
|
||||
return (
|
||||
<NestedTable>
|
||||
<Table.Container>
|
||||
<Table.Container noWidget>
|
||||
<DatatableContent<D>
|
||||
tableInstance={tableInstance}
|
||||
isLoading={isLoading}
|
||||
|
|
|
@ -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,26 +17,20 @@ 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 <TaskQuickActions taskId={taskId} state={state} />;
|
||||
}
|
||||
|
||||
const isActive = [
|
||||
}: {
|
||||
containerId: string;
|
||||
nodeName: string;
|
||||
status: ContainerStatus;
|
||||
state: QuickActionsState;
|
||||
}) {
|
||||
const isActive =
|
||||
!!status &&
|
||||
[
|
||||
ContainerStatus.Starting,
|
||||
ContainerStatus.Running,
|
||||
ContainerStatus.Healthy,
|
||||
|
@ -107,34 +101,3 @@ export function ContainerQuickActions({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TaskProps {
|
||||
taskId: string;
|
||||
state: QuickActionsState;
|
||||
}
|
||||
|
||||
function TaskQuickActions({ taskId, state }: TaskProps) {
|
||||
return (
|
||||
<div className={clsx('space-x-1', styles.root)}>
|
||||
{state.showQuickActionLogs && (
|
||||
<Authorized authorizations="DockerTaskLogs">
|
||||
<Link
|
||||
to="docker.tasks.task.logs"
|
||||
params={{ id: taskId }}
|
||||
title="Logs"
|
||||
>
|
||||
<Icon icon={FileText} className="space-right" />
|
||||
</Link>
|
||||
</Authorized>
|
||||
)}
|
||||
|
||||
{state.showQuickActionInspect && (
|
||||
<Authorized authorizations="DockerTaskInspect">
|
||||
<Link to="docker.tasks.task" params={{ id: taskId }} title="Inspect">
|
||||
<Icon icon={Info} className="space-right" />
|
||||
</Link>
|
||||
</Authorized>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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<Array<Node>>(buildUrl(environmentId));
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error, 'Unable to retrieve nodes');
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<NestedDatatable
|
||||
columns={columns}
|
||||
dataset={dataset}
|
||||
search={search}
|
||||
emptyContentLabel="No task matching filter."
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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<DecoratedTask, unknown>) {
|
||||
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 ? (
|
||||
<ContainerQuickActions
|
||||
containerId={item.Container.Id}
|
||||
nodeName={item.Container.NodeName}
|
||||
status={item.Container.Status}
|
||||
state={state}
|
||||
/>
|
||||
) : (
|
||||
<TaskTableQuickActions taskId={item.Id} />
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { DecoratedTask } from '../types';
|
||||
|
||||
export const columnHelper = createColumnHelper<DecoratedTask>();
|
|
@ -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()),
|
||||
}),
|
||||
];
|
|
@ -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<DecoratedTask, string>) {
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
const nodesQuery = useNodes(environmentId);
|
||||
|
||||
const nodes = nodesQuery.data || [];
|
||||
return getNodeName(getValue(), nodes);
|
||||
}
|
||||
|
||||
function getNodeName(nodeId: string, nodes: Array<Node>) {
|
||||
const node = nodes.find((node) => node.ID === nodeId);
|
||||
if (node?.Description?.Hostname) {
|
||||
return node.Description.Hostname;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
|
@ -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 (
|
||||
<span className={clsx('label', `label-${taskStatusBadge(value)}`)}>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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<DecoratedTask, string>) {
|
||||
const environmentQuery = useCurrentEnvironment();
|
||||
|
||||
if (!environmentQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = getValue();
|
||||
const isAgent = isAgentEnvironment(environmentQuery.data.Type);
|
||||
|
||||
return isAgent && item.Container ? (
|
||||
<Link
|
||||
to="docker.containers.container"
|
||||
params={{ id: item.Container.Id, nodeName: item.Container.NodeName }}
|
||||
className="monospaced"
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to="docker.tasks.task"
|
||||
params={{ id: item.Id }}
|
||||
className="monospaced"
|
||||
>
|
||||
{value}
|
||||
</Link>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { TasksDatatable } from './TasksDatatable';
|
|
@ -0,0 +1,6 @@
|
|||
import { TaskViewModel } from '@/docker/models/task';
|
||||
import { DockerContainer } from '@/react/docker/containers/types';
|
||||
|
||||
export type DecoratedTask = TaskViewModel & {
|
||||
Container?: DockerContainer;
|
||||
};
|
|
@ -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 (
|
||||
<div className="inline-flex space-x-1">
|
||||
{state.showQuickActionLogs && (
|
||||
<Authorized authorizations="DockerTaskLogs">
|
||||
<Link
|
||||
to="docker.tasks.task.logs"
|
||||
params={{ id: taskId }}
|
||||
title="Logs"
|
||||
>
|
||||
<Icon icon={FileText} className="space-right" />
|
||||
</Link>
|
||||
</Authorized>
|
||||
)}
|
||||
|
||||
{state.showQuickActionInspect && (
|
||||
<Authorized authorizations="DockerTaskInspect">
|
||||
<Link to="docker.tasks.task" params={{ id: taskId }} title="Inspect">
|
||||
<Icon icon={Info} className="space-right" />
|
||||
</Link>
|
||||
</Authorized>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue