mirror of https://github.com/portainer/portainer
refactor(edge/stacks): migrate envs table to react [EE-5613] (#9093)
parent
dfc1a7b1d7
commit
11571fd6ea
|
@ -66,15 +66,12 @@ angular
|
||||||
|
|
||||||
const stacksEdit = {
|
const stacksEdit = {
|
||||||
name: 'edge.stacks.edit',
|
name: 'edge.stacks.edit',
|
||||||
url: '/:stackId',
|
url: '/:stackId?tab&status',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'editEdgeStackView',
|
component: 'editEdgeStackView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
params: {
|
|
||||||
tab: 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const edgeJobs = {
|
const edgeJobs = {
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
<div class="datatable">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="toolBar">
|
|
||||||
<div class="toolBarTitle vertical-center"> <pr-icon icon="$ctrl.titleIcon"></pr-icon> {{ $ctrl.titleText }} </div>
|
|
||||||
<div class="searchBar vertical-center">
|
|
||||||
<pr-icon icon="'search'" class-name="'searchIcon'"></pr-icon>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="searchInput"
|
|
||||||
auto-focus
|
|
||||||
placeholder="Search..."
|
|
||||||
ng-model="$ctrl.state.textFilter"
|
|
||||||
ng-change="$ctrl.onTextFilterChange()"
|
|
||||||
ng-model-options="{ debounce: 300 }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table-hover nowrap-cells table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<table-column-header
|
|
||||||
col-title="'Name'"
|
|
||||||
can-sort="true"
|
|
||||||
is-sorted="$ctrl.state.orderBy === 'Name'"
|
|
||||||
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
|
|
||||||
ng-click="$ctrl.changeOrderBy('Name')"
|
|
||||||
></table-column-header>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<table-column-header
|
|
||||||
col-title="'Status'"
|
|
||||||
can-sort="true"
|
|
||||||
is-sorted="$ctrl.state.orderBy === 'Status'"
|
|
||||||
is-sorted-desc="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"
|
|
||||||
ng-click="$ctrl.changeOrderBy('Status')"
|
|
||||||
></table-column-header>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<table-column-header
|
|
||||||
col-title="'Error'"
|
|
||||||
can-sort="true"
|
|
||||||
is-sorted="$ctrl.state.orderBy === 'Error'"
|
|
||||||
is-sorted-desc="$ctrl.state.orderBy === 'Error' && $ctrl.state.reverseOrder"
|
|
||||||
ng-click="$ctrl.changeOrderBy('Error')"
|
|
||||||
></table-column-header>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
dir-paginate="item in $ctrl.state.filteredDataSet | itemsPerPage: $ctrl.state.paginatedItemLimit"
|
|
||||||
total-items="$ctrl.state.totalFilteredDataSet"
|
|
||||||
ng-class="{ active: item.Checked }"
|
|
||||||
>
|
|
||||||
<td>{{ item.Name }}</td>
|
|
||||||
<td>{{ $ctrl.endpointStatusLabel(item.Id) }}</td>
|
|
||||||
<td>{{ $ctrl.endpointStatusError(item.Id) }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="$ctrl.state.loading">
|
|
||||||
<td colspan="5" class="text-muted text-center">Loading...</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="!$ctrl.state.loading && $ctrl.state.filteredDataSet.length === 0">
|
|
||||||
<td colspan="5" class="text-muted text-center">No environment available.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="footer" ng-if="!$ctrl.state.loading">
|
|
||||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
|
||||||
<div class="paginationControls">
|
|
||||||
<form class="form-inline">
|
|
||||||
<span class="limitSelector">
|
|
||||||
<span style="margin-right: 5px"> Items per page </span>
|
|
||||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
|
|
||||||
<option value="0">All</option>
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="25">25</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
|
||||||
</span>
|
|
||||||
<dir-pagination-controls max-size="5" on-page-change="$ctrl.onPageChange(newPageNumber, oldPageNumber)"></dir-pagination-controls>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
|
@ -1,120 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
export class EdgeStackEndpointsDatatableController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor($async, $scope, $controller, DatatableService, PaginationService, Notifications) {
|
|
||||||
this.extendGenericController($controller, $scope);
|
|
||||||
this.DatatableService = DatatableService;
|
|
||||||
this.PaginationService = PaginationService;
|
|
||||||
this.Notifications = Notifications;
|
|
||||||
this.$async = $async;
|
|
||||||
|
|
||||||
this.state = Object.assign(this.state, {
|
|
||||||
orderBy: this.orderBy,
|
|
||||||
loading: true,
|
|
||||||
filteredDataSet: [],
|
|
||||||
totalFilteredDataset: 0,
|
|
||||||
pageNumber: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onPageChange = this.onPageChange.bind(this);
|
|
||||||
this.paginationChanged = this.paginationChanged.bind(this);
|
|
||||||
this.paginationChangedAsync = this.paginationChangedAsync.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
extendGenericController($controller, $scope) {
|
|
||||||
// extending the controller overrides the current controller functions
|
|
||||||
const $onInit = this.$onInit.bind(this);
|
|
||||||
const changePaginationLimit = this.changePaginationLimit.bind(this);
|
|
||||||
const onTextFilterChange = this.onTextFilterChange.bind(this);
|
|
||||||
angular.extend(this, $controller('GenericDatatableController', { $scope }));
|
|
||||||
this.$onInit = $onInit;
|
|
||||||
this.changePaginationLimit = changePaginationLimit;
|
|
||||||
this.onTextFilterChange = onTextFilterChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEndpointStatus(endpointId) {
|
|
||||||
return this.endpointsStatus[endpointId];
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointStatusLabel(endpointId) {
|
|
||||||
const status = this.getEndpointStatus(endpointId);
|
|
||||||
const details = (status && status.Details) || {};
|
|
||||||
|
|
||||||
return (details.Error && 'Error') || (details.Ok && 'Ok') || (details.ImagesPulled && 'Images pre-pulled') || (details.Acknowledged && 'Acknowledged') || 'Pending';
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointStatusError(endpointId) {
|
|
||||||
const status = this.getEndpointStatus(endpointId);
|
|
||||||
|
|
||||||
return status && status.Error ? status.Error : '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
this.setDefaults();
|
|
||||||
this.prepareTableFromDataset();
|
|
||||||
|
|
||||||
var storedOrder = this.DatatableService.getDataTableOrder(this.tableKey);
|
|
||||||
if (storedOrder !== null) {
|
|
||||||
this.state.reverseOrder = storedOrder.reverse;
|
|
||||||
this.state.orderBy = storedOrder.orderBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
var textFilter = this.DatatableService.getDataTableTextFilters(this.tableKey);
|
|
||||||
if (textFilter !== null) {
|
|
||||||
this.state.textFilter = textFilter;
|
|
||||||
this.onTextFilterChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
var storedFilters = this.DatatableService.getDataTableFilters(this.tableKey);
|
|
||||||
if (storedFilters !== null) {
|
|
||||||
this.filters = storedFilters;
|
|
||||||
}
|
|
||||||
if (this.filters && this.filters.state) {
|
|
||||||
this.filters.state.open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.paginationChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageChange(newPageNumber) {
|
|
||||||
this.state.pageNumber = newPageNumber;
|
|
||||||
this.paginationChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overridden
|
|
||||||
*/
|
|
||||||
changePaginationLimit() {
|
|
||||||
this.PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
|
||||||
this.paginationChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overridden
|
|
||||||
*/
|
|
||||||
onTextFilterChange() {
|
|
||||||
var filterValue = this.state.textFilter;
|
|
||||||
this.DatatableService.setDataTableTextFilters(this.tableKey, filterValue);
|
|
||||||
this.paginationChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
paginationChanged() {
|
|
||||||
this.$async(this.paginationChangedAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
async paginationChangedAsync() {
|
|
||||||
this.state.loading = true;
|
|
||||||
this.state.filteredDataSet = [];
|
|
||||||
const start = (this.state.pageNumber - 1) * this.state.paginatedItemLimit + 1;
|
|
||||||
try {
|
|
||||||
const { endpoints, totalCount } = await this.retrievePage(start, this.state.paginatedItemLimit, this.state.textFilter);
|
|
||||||
this.state.filteredDataSet = endpoints;
|
|
||||||
this.state.totalFilteredDataSet = totalCount;
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve environments');
|
|
||||||
} finally {
|
|
||||||
this.state.loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import { EdgeStackEndpointsDatatableController } from './edgeStackEndpointsDatatableController';
|
|
||||||
|
|
||||||
angular.module('portainer.edge').component('edgeStackEndpointsDatatable', {
|
|
||||||
templateUrl: './edgeStackEndpointsDatatable.html',
|
|
||||||
controller: EdgeStackEndpointsDatatableController,
|
|
||||||
bindings: {
|
|
||||||
titleText: '@',
|
|
||||||
titleIcon: '@',
|
|
||||||
tableKey: '@',
|
|
||||||
orderBy: '@',
|
|
||||||
reverseOrder: '<',
|
|
||||||
retrievePage: '<',
|
|
||||||
edgeStackId: '<',
|
|
||||||
endpointsStatus: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -12,9 +12,14 @@ import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { EdgeGroupAssociationTable } from '@/react/edge/components/EdgeGroupAssociationTable';
|
import { EdgeGroupAssociationTable } from '@/react/edge/components/EdgeGroupAssociationTable';
|
||||||
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
|
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
|
||||||
|
import { EnvironmentsDatatable } from '@/react/edge/edge-stacks/ItemView/EnvironmentsDatatable';
|
||||||
|
|
||||||
export const componentsModule = angular
|
export const componentsModule = angular
|
||||||
.module('portainer.edge.react.components', [])
|
.module('portainer.edge.react.components', [])
|
||||||
|
.component(
|
||||||
|
'edgeStackEnvironmentsDatatable',
|
||||||
|
r2a(withUIRouter(withReactQuery(EnvironmentsDatatable)), [])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'edgeGroupsSelector',
|
'edgeGroupsSelector',
|
||||||
r2a(withUIRouter(withReactQuery(EdgeGroupsSelector)), [
|
r2a(withUIRouter(withReactQuery(EdgeGroupsSelector)), [
|
||||||
|
|
|
@ -29,17 +29,7 @@
|
||||||
</uib-tab-heading>
|
</uib-tab-heading>
|
||||||
|
|
||||||
<div style="margin-top: 25px">
|
<div style="margin-top: 25px">
|
||||||
<edge-stack-endpoints-datatable
|
<edge-stack-environments-datatable></edge-stack-environments-datatable>
|
||||||
title-text="Environments Status"
|
|
||||||
dataset="$ctrl.endpoints"
|
|
||||||
title-icon="hard-drive"
|
|
||||||
table-key="edgeStackEndpoints"
|
|
||||||
order-by="Name"
|
|
||||||
retrieve-page="$ctrl.getPaginatedEndpoints"
|
|
||||||
edge-stack-id="$ctrl.stack.Id"
|
|
||||||
endpoints-status="$ctrl.stack.Status"
|
|
||||||
>
|
|
||||||
</edge-stack-endpoints-datatable>
|
|
||||||
</div>
|
</div>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
</uib-tabset>
|
</uib-tabset>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { getEnvironments } from '@/react/portainer/environments/environment.service';
|
|
||||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
||||||
import { EnvironmentType } from '@/react/portainer/environments/types';
|
import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||||
import { createWebhookId } from '@/portainer/helpers/webhookHelper';
|
import { createWebhookId } from '@/portainer/helpers/webhookHelper';
|
||||||
|
@ -28,7 +27,6 @@ export class EditEdgeStackViewController {
|
||||||
|
|
||||||
this.deployStack = this.deployStack.bind(this);
|
this.deployStack = this.deployStack.bind(this);
|
||||||
this.deployStackAsync = this.deployStackAsync.bind(this);
|
this.deployStackAsync = this.deployStackAsync.bind(this);
|
||||||
this.getPaginatedEndpoints = this.getPaginatedEndpoints.bind(this);
|
|
||||||
this.onEditorChange = this.onEditorChange.bind(this);
|
this.onEditorChange = this.onEditorChange.bind(this);
|
||||||
this.isEditorDirty = this.isEditorDirty.bind(this);
|
this.isEditorDirty = this.isEditorDirty.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -36,7 +34,7 @@ export class EditEdgeStackViewController {
|
||||||
async $onInit() {
|
async $onInit() {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
const { stackId, tab } = this.$state.params;
|
const { stackId, tab } = this.$state.params;
|
||||||
this.state.activeTab = tab;
|
this.state.activeTab = tab ? parseInt(tab, 10) : 0;
|
||||||
try {
|
try {
|
||||||
const [edgeGroups, model, file] = await Promise.all([this.EdgeGroupService.groups(), this.EdgeStackService.stack(stackId), this.EdgeStackService.stackFile(stackId)]);
|
const [edgeGroups, model, file] = await Promise.all([this.EdgeGroupService.groups(), this.EdgeStackService.stack(stackId), this.EdgeStackService.stackFile(stackId)]);
|
||||||
|
|
||||||
|
@ -110,20 +108,4 @@ export class EditEdgeStackViewController {
|
||||||
this.state.actionInProgress = false;
|
this.state.actionInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPaginatedEndpoints(lastId, limit, search) {
|
|
||||||
return this.$async(async () => {
|
|
||||||
try {
|
|
||||||
const query = {
|
|
||||||
search,
|
|
||||||
edgeStackId: this.stack.Id,
|
|
||||||
};
|
|
||||||
const { value, totalCount } = await getEnvironments({ start: lastId, limit, query });
|
|
||||||
|
|
||||||
return { endpoints: value, totalCount };
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve environment information');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,20 +18,22 @@ export function TableTitle({
|
||||||
className,
|
className,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('toolBar flex-col', className)}>
|
<>
|
||||||
<div className="flex w-full items-center gap-1 p-0">
|
<div className={clsx('toolBar flex-col', className)}>
|
||||||
<div className="toolBarTitle">
|
<div className="flex w-full items-center gap-1 p-0">
|
||||||
{icon && (
|
<div className="toolBarTitle">
|
||||||
<div className="widget-icon">
|
{icon && (
|
||||||
<Icon icon={icon} className="space-right" />
|
<div className="widget-icon">
|
||||||
</div>
|
<Icon icon={icon} className="space-right" />
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{label}
|
{label}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
{description}
|
{!!description && <div className="toolBar !pt-0">{description}</div>}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { useLogsStatus } from './useLogsStatus';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActionStatus({ environmentId }: Props) {
|
||||||
|
const {
|
||||||
|
params: { stackId: edgeStackId },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
|
||||||
|
const logsStatusQuery = useLogsStatus(edgeStackId, environmentId);
|
||||||
|
|
||||||
|
return <>{getStatusText(logsStatusQuery.data)}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status?: 'pending' | 'collected' | 'idle') {
|
||||||
|
switch (status) {
|
||||||
|
case 'collected':
|
||||||
|
return 'Logs available for download';
|
||||||
|
case 'pending':
|
||||||
|
return 'Logs marked for collection, please wait until the logs are available';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
import { Link } from '@@/Link';
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
|
import { LogsActions } from './LogsActions';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
environment: Environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnvironmentActions({ environment }: Props) {
|
||||||
|
const {
|
||||||
|
params: { stackId: edgeStackId },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-x-2">
|
||||||
|
{environment.Snapshots.length > 0 && (
|
||||||
|
<Link
|
||||||
|
to="edge.browse.containers"
|
||||||
|
params={{ environmentId: environment.Id, edgeStackId }}
|
||||||
|
className="!text-inherit hover:!no-underline"
|
||||||
|
>
|
||||||
|
<Button color="none" title="Browse Snapshot">
|
||||||
|
<Icon icon={Search} className="searchIcon" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{environment.Edge.AsyncMode && (
|
||||||
|
<LogsActions environmentId={environment.Id} edgeStackId={edgeStackId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
import { HardDrive } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { EdgeStackStatus, StatusType } from '@/react/edge/edge-stacks/types';
|
||||||
|
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||||
|
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
import { useParamState } from '@/react/hooks/useParamState';
|
||||||
|
|
||||||
|
import { Datatable } from '@@/datatables';
|
||||||
|
import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
|
||||||
|
import { PortainerSelect } from '@@/form-components/PortainerSelect';
|
||||||
|
|
||||||
|
import { useEdgeStack } from '../../queries/useEdgeStack';
|
||||||
|
|
||||||
|
import { EdgeStackEnvironment } from './types';
|
||||||
|
import { columns } from './columns';
|
||||||
|
|
||||||
|
export function EnvironmentsDatatable() {
|
||||||
|
const {
|
||||||
|
params: { stackId },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
const edgeStackQuery = useEdgeStack(stackId);
|
||||||
|
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [statusFilter, setStatusFilter] = useParamState<StatusType>(
|
||||||
|
'status',
|
||||||
|
parseStatusFilter
|
||||||
|
);
|
||||||
|
const tableState = useTableStateWithoutStorage('name');
|
||||||
|
const endpointsQuery = useEnvironmentList({
|
||||||
|
pageLimit: tableState.pageSize,
|
||||||
|
page,
|
||||||
|
search: tableState.search,
|
||||||
|
sort: tableState.sortBy.id as 'Group' | 'Name',
|
||||||
|
order: tableState.sortBy.desc ? 'desc' : 'asc',
|
||||||
|
edgeStackId: stackId,
|
||||||
|
edgeStackStatus: statusFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const environments: Array<EdgeStackEnvironment> = useMemo(
|
||||||
|
() =>
|
||||||
|
endpointsQuery.environments.map((env) => ({
|
||||||
|
...env,
|
||||||
|
StackStatus:
|
||||||
|
edgeStackQuery.data?.Status[env.Id] ||
|
||||||
|
({
|
||||||
|
Details: {
|
||||||
|
Pending: true,
|
||||||
|
Acknowledged: false,
|
||||||
|
ImagesPulled: false,
|
||||||
|
Error: false,
|
||||||
|
Ok: false,
|
||||||
|
RemoteUpdateSuccess: false,
|
||||||
|
Remove: false,
|
||||||
|
},
|
||||||
|
EndpointID: env.Id,
|
||||||
|
Error: '',
|
||||||
|
} satisfies EdgeStackStatus),
|
||||||
|
})),
|
||||||
|
[edgeStackQuery.data?.Status, endpointsQuery.environments]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Datatable
|
||||||
|
columns={columns}
|
||||||
|
isLoading={endpointsQuery.isLoading}
|
||||||
|
dataset={environments}
|
||||||
|
settingsManager={tableState}
|
||||||
|
title="Environments Status"
|
||||||
|
titleIcon={HardDrive}
|
||||||
|
onPageChange={setPage}
|
||||||
|
emptyContentLabel="No environment available."
|
||||||
|
disableSelect
|
||||||
|
description={
|
||||||
|
isBE && (
|
||||||
|
<div className="w-1/4">
|
||||||
|
<PortainerSelect<StatusType | undefined>
|
||||||
|
isClearable
|
||||||
|
bindToBody
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e || undefined)}
|
||||||
|
options={[
|
||||||
|
{ value: 'Pending', label: 'Pending' },
|
||||||
|
{ value: 'Acknowledged', label: 'Acknowledged' },
|
||||||
|
{ value: 'ImagesPulled', label: 'Images pre-pulled' },
|
||||||
|
{ value: 'Ok', label: 'Deployed' },
|
||||||
|
{ value: 'Error', label: 'Failed' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStatusFilter(status: string | undefined): StatusType | undefined {
|
||||||
|
switch (status) {
|
||||||
|
case 'Pending':
|
||||||
|
return 'Pending';
|
||||||
|
case 'Acknowledged':
|
||||||
|
return 'Acknowledged';
|
||||||
|
case 'ImagesPulled':
|
||||||
|
return 'ImagesPulled';
|
||||||
|
case 'Ok':
|
||||||
|
return 'Ok';
|
||||||
|
case 'Error':
|
||||||
|
return 'Error';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
|
import { EdgeStack } from '../../types';
|
||||||
|
|
||||||
|
import { useCollectLogsMutation } from './useCollectLogsMutation';
|
||||||
|
import { useDeleteLogsMutation } from './useDeleteLogsMutation';
|
||||||
|
import { useDownloadLogsMutation } from './useDownloadLogsMutation';
|
||||||
|
import { useLogsStatus } from './useLogsStatus';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
edgeStackId: EdgeStack['Id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogsActions({ environmentId, edgeStackId }: Props) {
|
||||||
|
const logsStatusQuery = useLogsStatus(edgeStackId, environmentId);
|
||||||
|
const collectLogsMutation = useCollectLogsMutation();
|
||||||
|
const downloadLogsMutation = useDownloadLogsMutation();
|
||||||
|
const deleteLogsMutation = useDeleteLogsMutation();
|
||||||
|
|
||||||
|
if (!logsStatusQuery.isSuccess) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = logsStatusQuery.data;
|
||||||
|
|
||||||
|
const collecting = collectLogsMutation.isLoading || status === 'pending';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button color="none" title="Retrieve logs" onClick={handleCollectLogs}>
|
||||||
|
<Icon
|
||||||
|
icon={clsx({
|
||||||
|
'file-text': !collecting,
|
||||||
|
loader: collecting,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="none"
|
||||||
|
title="Download logs"
|
||||||
|
disabled={status !== 'collected'}
|
||||||
|
onClick={handleDownloadLogs}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={clsx({
|
||||||
|
'download-cloud': !downloadLogsMutation.isLoading,
|
||||||
|
loader: downloadLogsMutation.isLoading,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="none"
|
||||||
|
title="Delete logs"
|
||||||
|
disabled={status !== 'collected'}
|
||||||
|
onClick={handleDeleteLogs}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={clsx({
|
||||||
|
delete: !deleteLogsMutation.isLoading,
|
||||||
|
loader: deleteLogsMutation.isLoading,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleCollectLogs() {
|
||||||
|
if (status === 'pending') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
collectLogsMutation.mutate(
|
||||||
|
{
|
||||||
|
edgeStackId,
|
||||||
|
environmentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
notifySuccess('Success', 'Logs Collection started');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownloadLogs() {
|
||||||
|
downloadLogsMutation.mutate({
|
||||||
|
edgeStackId,
|
||||||
|
environmentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteLogs() {
|
||||||
|
deleteLogsMutation.mutate(
|
||||||
|
{
|
||||||
|
edgeStackId,
|
||||||
|
environmentId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
notifySuccess('Success', 'Logs Deleted');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { CellContext, createColumnHelper } from '@tanstack/react-table';
|
||||||
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
|
import { EdgeStackStatus } from '../../types';
|
||||||
|
|
||||||
|
import { EnvironmentActions } from './EnvironmentActions';
|
||||||
|
import { ActionStatus } from './ActionStatus';
|
||||||
|
import { EdgeStackEnvironment } from './types';
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<EdgeStackEnvironment>();
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
columnHelper.accessor('Name', {
|
||||||
|
id: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor((env) => endpointStatusLabel(env.StackStatus), {
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor((env) => env.StackStatus.Error, {
|
||||||
|
id: 'error',
|
||||||
|
header: 'Error',
|
||||||
|
cell: ErrorCell,
|
||||||
|
}),
|
||||||
|
...(isBE
|
||||||
|
? [
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Actions',
|
||||||
|
cell({ row: { original: env } }) {
|
||||||
|
return <EnvironmentActions environment={env} />;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'actionStatus',
|
||||||
|
header: 'Action Status',
|
||||||
|
cell({ row: { original: env } }) {
|
||||||
|
return <ActionStatus environmentId={env.Id} />;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
|
function ErrorCell({ getValue }: CellContext<EdgeStackEnvironment, string>) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const value = getValue();
|
||||||
|
if (!value) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className="flex cursor-pointer"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
>
|
||||||
|
<div className="pt-0.5 pr-1">
|
||||||
|
<Icon icon={isExpanded ? ChevronDown : ChevronRight} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx('overflow-hidden whitespace-normal', {
|
||||||
|
'h-[1.5em]': isExpanded,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endpointStatusLabel(status: EdgeStackStatus) {
|
||||||
|
const details = (status && status.Details) || {};
|
||||||
|
|
||||||
|
const labels = [];
|
||||||
|
|
||||||
|
if (details.Acknowledged) {
|
||||||
|
labels.push('Acknowledged');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.ImagesPulled) {
|
||||||
|
labels.push('Images pre-pulled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.Ok) {
|
||||||
|
labels.push('Deployed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.Error) {
|
||||||
|
labels.push('Failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!labels.length) {
|
||||||
|
labels.push('Pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels.join(', ');
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { EnvironmentsDatatable } from './EnvironmentsDatatable';
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { EdgeStackStatus } from '../../types';
|
||||||
|
|
||||||
|
export type EdgeStackEnvironment = Environment & {
|
||||||
|
StackStatus: EdgeStackStatus;
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useMutation, useQueryClient } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { withError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { EdgeStack } from '../../types';
|
||||||
|
|
||||||
|
import { logsStatusQueryKey } from './useLogsStatus';
|
||||||
|
|
||||||
|
export function useCollectLogsMutation() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(collectLogs, {
|
||||||
|
onSuccess(data, variables) {
|
||||||
|
return queryClient.invalidateQueries(
|
||||||
|
logsStatusQueryKey(variables.edgeStackId, variables.environmentId)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
...withError('Unable to retrieve logs'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CollectLogs {
|
||||||
|
edgeStackId: EdgeStack['Id'];
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectLogs({ edgeStackId, environmentId }: CollectLogs) {
|
||||||
|
try {
|
||||||
|
await axios.put(`/edge_stacks/${edgeStackId}/logs/${environmentId}`);
|
||||||
|
} catch (error) {
|
||||||
|
throw parseAxiosError(error as Error, 'Unable to start logs collection');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { useMutation, useQueryClient } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { withError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { EdgeStack } from '../../types';
|
||||||
|
|
||||||
|
import { logsStatusQueryKey } from './useLogsStatus';
|
||||||
|
|
||||||
|
export function useDeleteLogsMutation() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(deleteLogs, {
|
||||||
|
onSuccess(data, variables) {
|
||||||
|
return queryClient.invalidateQueries(
|
||||||
|
logsStatusQueryKey(variables.edgeStackId, variables.environmentId)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
...withError('Unable to delete logs'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteLogs {
|
||||||
|
edgeStackId: EdgeStack['Id'];
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteLogs({ edgeStackId, environmentId }: DeleteLogs) {
|
||||||
|
try {
|
||||||
|
await axios.delete(`/edge_stacks/${edgeStackId}/logs/${environmentId}`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: {
|
||||||
|
Accept: 'text/yaml',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, '');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import { useMutation } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { mutationOptions, withError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { EdgeStack } from '../../types';
|
||||||
|
|
||||||
|
export function useDownloadLogsMutation() {
|
||||||
|
return useMutation(
|
||||||
|
downloadLogs,
|
||||||
|
mutationOptions(withError('Unable to download logs'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadLogs {
|
||||||
|
edgeStackId: EdgeStack['Id'];
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadLogs({ edgeStackId, environmentId }: DownloadLogs) {
|
||||||
|
try {
|
||||||
|
const { headers, data } = await axios.get<Blob>(
|
||||||
|
`/edge_stacks/${edgeStackId}/logs/${environmentId}/file`,
|
||||||
|
{
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: {
|
||||||
|
Accept: 'text/yaml',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const contentDispositionHeader = headers['content-disposition'];
|
||||||
|
const filename = contentDispositionHeader
|
||||||
|
.replace('attachment; filename=', '')
|
||||||
|
.trim();
|
||||||
|
saveAs(data, filename);
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, '');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EdgeStack } from '@/react/edge/edge-stacks/types';
|
||||||
|
|
||||||
|
import { queryKeys } from '../../queries/query-keys';
|
||||||
|
|
||||||
|
export function logsStatusQueryKey(
|
||||||
|
edgeStackId: EdgeStack['Id'],
|
||||||
|
environmentId: EnvironmentId
|
||||||
|
) {
|
||||||
|
return [...queryKeys.item(edgeStackId), 'logs', environmentId] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLogsStatus(
|
||||||
|
edgeStackId: EdgeStack['Id'],
|
||||||
|
environmentId: EnvironmentId
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
logsStatusQueryKey(edgeStackId, environmentId),
|
||||||
|
() => getLogsStatus(edgeStackId, environmentId),
|
||||||
|
{
|
||||||
|
refetchInterval(status) {
|
||||||
|
if (status === 'pending') {
|
||||||
|
return 30 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogsStatusResponse {
|
||||||
|
status: 'collected' | 'idle' | 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLogsStatus(
|
||||||
|
edgeStackId: EdgeStack['Id'],
|
||||||
|
environmentId: EnvironmentId
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<LogsStatusResponse>(
|
||||||
|
`/edge_stacks/${edgeStackId}/logs/${environmentId}`
|
||||||
|
);
|
||||||
|
return data.status;
|
||||||
|
} catch (error) {
|
||||||
|
throw parseAxiosError(error as Error, 'Unable to retrieve logs status');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,6 @@
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
|
||||||
|
|
||||||
import { EdgeStack } from '../types';
|
import { EdgeStack } from '../types';
|
||||||
|
|
||||||
export const queryKeys = {
|
export const queryKeys = {
|
||||||
base: () => ['edge-stacks'] as const,
|
base: () => ['edge-stacks'] as const,
|
||||||
item: (id: EdgeStack['Id']) => [...queryKeys.base(), id] as const,
|
item: (id: EdgeStack['Id']) => [...queryKeys.base(), id] as const,
|
||||||
logsStatus: (edgeStackId: EdgeStack['Id'], environmentId: EnvironmentId) =>
|
|
||||||
[...queryKeys.item(edgeStackId), 'logs', environmentId] as const,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { withError } from '@/react-tools/react-query';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
|
||||||
|
import { EdgeStack } from '../types';
|
||||||
|
|
||||||
|
import { buildUrl } from './buildUrl';
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
export function useEdgeStack(id?: EdgeStack['Id']) {
|
||||||
|
return useQuery(id ? queryKeys.item(id) : [], () => getEdgeStack(id), {
|
||||||
|
...withError('Failed loading Edge stack'),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEdgeStack(id?: EdgeStack['Id']) {
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<EdgeStack>(buildUrl(id));
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,8 @@ interface EdgeStackStatusDetails {
|
||||||
ImagesPulled: boolean;
|
ImagesPulled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StatusType = keyof EdgeStackStatusDetails;
|
||||||
|
|
||||||
export interface EdgeStackStatus {
|
export interface EdgeStackStatus {
|
||||||
Details: EdgeStackStatusDetails;
|
Details: EdgeStackStatusDetails;
|
||||||
Error: string;
|
Error: string;
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
export function useParamState<T>(
|
||||||
|
param: string,
|
||||||
|
parseParam: (param: string | undefined) => T | undefined
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
params: { [param]: paramValue },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const state = parseParam(paramValue);
|
||||||
|
|
||||||
|
return [
|
||||||
|
state,
|
||||||
|
(value: T | undefined) => {
|
||||||
|
router.stateService.go('.', { [param]: value });
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
}
|
|
@ -3,7 +3,10 @@ import { type EnvironmentGroupId } from '@/react/portainer/environments/environm
|
||||||
import { type TagId } from '@/portainer/tags/types';
|
import { type TagId } from '@/portainer/tags/types';
|
||||||
import { UserId } from '@/portainer/users/types';
|
import { UserId } from '@/portainer/users/types';
|
||||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||||
import { EdgeStack, EdgeStackStatus } from '@/react/edge/edge-stacks/types';
|
import {
|
||||||
|
EdgeStack,
|
||||||
|
StatusType as EdgeStackStatusType,
|
||||||
|
} from '@/react/edge/edge-stacks/types';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Environment,
|
Environment,
|
||||||
|
@ -21,7 +24,7 @@ export type EdgeStackEnvironmentsQueryParams =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
edgeStackId: EdgeStack['Id'];
|
edgeStackId: EdgeStack['Id'];
|
||||||
edgeStackStatus?: keyof EdgeStackStatus['Details'];
|
edgeStackStatus?: EdgeStackStatusType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface BaseEnvironmentsQueryParams {
|
export interface BaseEnvironmentsQueryParams {
|
||||||
|
|
Loading…
Reference in New Issue