mirror of https://github.com/portainer/portainer
refactor(docker/containers): migrate networks table to react [EE-4665] (#10069)
parent
776f6a62c3
commit
b15812a74d
|
@ -1,117 +0,0 @@
|
||||||
<div class="datatable">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="{{ $ctrl.titleIcon }}" title-text="{{ $ctrl.titleText }}"></rd-widget-header>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="actionBar">
|
|
||||||
<form class="form-horizontal">
|
|
||||||
<div class="row" authorization="DockerNetworkConnect">
|
|
||||||
<label for="container_network" class="col-sm-3 col-lg-2 control-label text-left">Join a network</label>
|
|
||||||
<div class="col-sm-5 col-lg-4">
|
|
||||||
<select class="form-control" ng-model="$ctrl.selectedNetwork" id="container_network">
|
|
||||||
<option selected disabled hidden value="">Select a network</option>
|
|
||||||
<option ng-repeat="net in $ctrl.availableNetworks | orderBy: 'Name'" ng-value="net.Id">{{ net.Name }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
ng-disabled="$ctrl.joinNetworkActionInProgress || !$ctrl.selectedNetwork"
|
|
||||||
ng-click="$ctrl.joinNetworkAction($ctrl.container, $ctrl.selectedNetwork)"
|
|
||||||
button-spinner="$ctrl.joinNetworkActionInProgress"
|
|
||||||
>
|
|
||||||
<span ng-hide="$ctrl.joinNetworkActionInProgress">Join network</span>
|
|
||||||
<span ng-show="$ctrl.joinNetworkActionInProgress">Joining network...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table-hover nowrap-cells table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Network</th>
|
|
||||||
<th>
|
|
||||||
IP Address
|
|
||||||
<a ng-click="$ctrl.expandAll()" ng-if="$ctrl.hasExpandableItems()">
|
|
||||||
<pr-icon ng-if="$ctrl.state.expandAll" icon="'chevron-down'"></pr-icon>
|
|
||||||
<pr-icon ng-if="!$ctrl.state.expandAll" icon="'chevron-right'"></pr-icon>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<th>Gateway</th>
|
|
||||||
<th>MAC Address</th>
|
|
||||||
<th authorization="DockerNetworkDisconnect">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
dir-paginate-start="(key, value) in $ctrl.dataset | itemsPerPage: $ctrl.state.paginatedItemLimit"
|
|
||||||
ng-class="{ active: item.Checked }"
|
|
||||||
ng-click="$ctrl.expandItem(value, !value.Expanded)"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-none" ng-if="$ctrl.itemCanExpand(value)" type="button">
|
|
||||||
<pr-icon ng-if="value.Expanded" icon="'chevron-down'" class-name="'mr-1'"></pr-icon>
|
|
||||||
<pr-icon ng-if="!value.Expanded" icon="'chevron-right'" class-name="'mr-1'"></pr-icon>
|
|
||||||
</button>
|
|
||||||
<a ui-sref="docker.networks.network({ id: key, nodeName: $ctrl.nodeName })">{{ key }}</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ value.IPAddress || '-' }}</td>
|
|
||||||
<td>{{ value.Gateway || '-' }}</td>
|
|
||||||
<td>{{ value.MacAddress || '-' }}</td>
|
|
||||||
<td authorization="DockerNetworkDisconnect">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-xs btn-dangerlight vertical-center !ml-0 h-fit"
|
|
||||||
ng-disabled="$ctrl.leaveNetworkActionInProgress || $ctrl.container.IsPortainer"
|
|
||||||
button-spinner="$ctrl.leaveNetworkActionInProgress"
|
|
||||||
ng-click="$ctrl.leaveNetworkAction($ctrl.container, key)"
|
|
||||||
>
|
|
||||||
<span ng-if="!$ctrl.leaveNetworkActionInProgress" class="vertical-center !ml-0"> <pr-icon icon="'trash-2'"></pr-icon> Leave network</span>
|
|
||||||
<span ng-if="$ctrl.leaveNetworkActionInProgress">Leaving network...</span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
dir-paginate-end
|
|
||||||
ng-show="$ctrl.itemCanExpand(value) && value.Expanded"
|
|
||||||
ng-class="{ 'datatable-highlighted': value.Highlighted, 'datatable-unhighlighted': !value.Highlighted }"
|
|
||||||
>
|
|
||||||
<td colspan="1"></td>
|
|
||||||
<td colspan="1">
|
|
||||||
{{ value.GlobalIPv6Address }}
|
|
||||||
</td>
|
|
||||||
<td colspan="3">
|
|
||||||
{{ value.IPv6Gateway || '-' }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="!$ctrl.dataset">
|
|
||||||
<td colspan="5" class="text-muted text-center">Loading...</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="$ctrl.dataset.length === 0">
|
|
||||||
<td colspan="5" class="text-muted text-center">No network available.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="footer" ng-if="$ctrl.dataset">
|
|
||||||
<div class="paginationControls">
|
|
||||||
<form class="form-inline">
|
|
||||||
<span class="limitSelector vertical-center">
|
|
||||||
<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"></dir-pagination-controls>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
|
@ -1,17 +0,0 @@
|
||||||
angular.module('portainer.docker').component('containerNetworksDatatable', {
|
|
||||||
templateUrl: './containerNetworksDatatable.html',
|
|
||||||
controller: 'ContainerNetworksDatatableController',
|
|
||||||
bindings: {
|
|
||||||
titleText: '@',
|
|
||||||
titleIcon: '@',
|
|
||||||
dataset: '<',
|
|
||||||
tableKey: '@',
|
|
||||||
container: '<',
|
|
||||||
availableNetworks: '<',
|
|
||||||
joinNetworkAction: '<',
|
|
||||||
joinNetworkActionInProgress: '<',
|
|
||||||
leaveNetworkActionInProgress: '<',
|
|
||||||
leaveNetworkAction: '<',
|
|
||||||
nodeName: '<',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,82 +0,0 @@
|
||||||
import _ from 'lodash-es';
|
|
||||||
|
|
||||||
angular.module('portainer.docker').controller('ContainerNetworksDatatableController', [
|
|
||||||
'$scope',
|
|
||||||
'$controller',
|
|
||||||
'DatatableService',
|
|
||||||
function ($scope, $controller, DatatableService) {
|
|
||||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
|
||||||
this.state = Object.assign(this.state, {
|
|
||||||
expandedItems: [],
|
|
||||||
expandAll: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.expandItem = function (item, expanded) {
|
|
||||||
if (!this.itemCanExpand(item)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.Expanded = expanded;
|
|
||||||
if (!expanded) {
|
|
||||||
item.Highlighted = false;
|
|
||||||
}
|
|
||||||
if (!item.Expanded) {
|
|
||||||
this.state.expandAll = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.itemCanExpand = function (item) {
|
|
||||||
return item.GlobalIPv6Address !== '';
|
|
||||||
};
|
|
||||||
|
|
||||||
this.hasExpandableItems = function () {
|
|
||||||
return _.filter(this.dataset, (item) => this.itemCanExpand(item)).length;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.expandAll = function () {
|
|
||||||
this.state.expandAll = !this.state.expandAll;
|
|
||||||
_.forEach(this.dataset, (item) => {
|
|
||||||
if (this.itemCanExpand(item)) {
|
|
||||||
this.expandItem(item, this.state.expandAll);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
_.forEach(this.dataset, (item) => {
|
|
||||||
item.Expanded = true;
|
|
||||||
item.Highlighted = true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -9,11 +9,20 @@ import {
|
||||||
CommandsTabValues,
|
CommandsTabValues,
|
||||||
commandsTabValidation,
|
commandsTabValidation,
|
||||||
} from '@/react/docker/containers/CreateView/CommandsTab';
|
} from '@/react/docker/containers/CreateView/CommandsTab';
|
||||||
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
|
import { ContainerNetworksDatatable } from '@/react/docker/containers/ItemView/ContainerNetworksDatatable';
|
||||||
|
|
||||||
const ngModule = angular.module(
|
const ngModule = angular
|
||||||
'portainer.docker.react.components.containers',
|
.module('portainer.docker.react.components.containers', [])
|
||||||
[]
|
.component(
|
||||||
);
|
'dockerContainerNetworksDatatable',
|
||||||
|
r2a(withUIRouter(withCurrentUser(ContainerNetworksDatatable)), [
|
||||||
|
'container',
|
||||||
|
'dataset',
|
||||||
|
'nodeName',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
export const containersModule = ngModule.name;
|
export const containersModule = ngModule.name;
|
||||||
|
|
||||||
|
|
|
@ -348,21 +348,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<docker-container-networks-datatable
|
||||||
<div class="col-sm-12">
|
|
||||||
<container-networks-datatable
|
|
||||||
ng-if="container.NetworkSettings.Networks"
|
ng-if="container.NetworkSettings.Networks"
|
||||||
title-text="Connected networks"
|
|
||||||
title-icon="share-2"
|
|
||||||
dataset="container.NetworkSettings.Networks"
|
dataset="container.NetworkSettings.Networks"
|
||||||
table-key="container-networks"
|
|
||||||
container="container"
|
container="container"
|
||||||
available-networks="availableNetworks"
|
available-networks="availableNetworks"
|
||||||
join-network-action="containerJoinNetwork"
|
on-join="(containerJoinNetwork)"
|
||||||
join-network-action-in-progress="state.joinNetworkInProgress"
|
join-in-progress="state.joinNetworkInProgress"
|
||||||
leave-network-action="containerLeaveNetwork"
|
on-leave="(containerLeaveNetwork)"
|
||||||
leave-network-action-in-progress="state.leaveNetworkInProgress"
|
leave-in-progress="state.leaveNetworkInProgress"
|
||||||
node-name="nodeName"
|
node-name="nodeName"
|
||||||
></container-networks-datatable>
|
>
|
||||||
</div>
|
</docker-container-networks-datatable>
|
||||||
</div>
|
|
||||||
|
|
|
@ -180,6 +180,7 @@ export const ngModule = angular
|
||||||
'isMulti',
|
'isMulti',
|
||||||
'isClearable',
|
'isClearable',
|
||||||
'components',
|
'components',
|
||||||
|
'isLoading',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -26,6 +26,7 @@ interface SharedProps extends AutomationTestingProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isClearable?: boolean;
|
isClearable?: boolean;
|
||||||
bindToBody?: boolean;
|
bindToBody?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MultiProps<TValue> extends SharedProps {
|
interface MultiProps<TValue> extends SharedProps {
|
||||||
|
@ -82,6 +83,7 @@ export function SingleSelect<TValue = string>({
|
||||||
isClearable,
|
isClearable,
|
||||||
bindToBody,
|
bindToBody,
|
||||||
components,
|
components,
|
||||||
|
isLoading,
|
||||||
}: SingleProps<TValue>) {
|
}: SingleProps<TValue>) {
|
||||||
const selectedValue =
|
const selectedValue =
|
||||||
value || (typeof value === 'number' && value === 0)
|
value || (typeof value === 'number' && value === 0)
|
||||||
|
@ -103,6 +105,7 @@ export function SingleSelect<TValue = string>({
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
menuPortalTarget={bindToBody ? document.body : undefined}
|
menuPortalTarget={bindToBody ? document.body : undefined}
|
||||||
components={components}
|
components={components}
|
||||||
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -142,6 +145,7 @@ export function MultiSelect<TValue = string>({
|
||||||
isClearable,
|
isClearable,
|
||||||
bindToBody,
|
bindToBody,
|
||||||
components,
|
components,
|
||||||
|
isLoading,
|
||||||
}: Omit<MultiProps<TValue>, 'isMulti'>) {
|
}: Omit<MultiProps<TValue>, 'isMulti'>) {
|
||||||
const selectedOptions = findSelectedOptions(options, value);
|
const selectedOptions = findSelectedOptions(options, value);
|
||||||
return (
|
return (
|
||||||
|
@ -161,6 +165,7 @@ export function MultiSelect<TValue = string>({
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
menuPortalTarget={bindToBody ? document.body : undefined}
|
menuPortalTarget={bindToBody ? document.body : undefined}
|
||||||
components={components}
|
components={components}
|
||||||
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { Form, Formik } from 'formik';
|
||||||
|
import { SchemaOf, object, string } from 'yup';
|
||||||
|
import { useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { useAuthorizations } from '@/react/hooks/useUser';
|
||||||
|
import { useConnectContainerMutation } from '@/react/docker/networks/queries/useConnectContainer';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
|
import { LoadingButton } from '@@/buttons';
|
||||||
|
|
||||||
|
import { NetworkSelector } from '../../components/NetworkSelector';
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
networkId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConnectNetworkForm({
|
||||||
|
nodeName,
|
||||||
|
containerId,
|
||||||
|
selectedNetworks,
|
||||||
|
}: {
|
||||||
|
nodeName?: string;
|
||||||
|
containerId: string;
|
||||||
|
selectedNetworks: string[];
|
||||||
|
}) {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const authorized = useAuthorizations('DockerNetworkConnect');
|
||||||
|
const connectMutation = useConnectContainerMutation(environmentId);
|
||||||
|
const router = useRouter();
|
||||||
|
if (!authorized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik<FormValues>
|
||||||
|
initialValues={{ networkId: '' }}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validationSchema={validation}
|
||||||
|
>
|
||||||
|
{({ values, errors, setFieldValue }) => (
|
||||||
|
<Form className="form-horizontal w-full">
|
||||||
|
<FormControl
|
||||||
|
label="Join a network"
|
||||||
|
className="!mb-0"
|
||||||
|
errors={errors.networkId}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-full">
|
||||||
|
<NetworkSelector
|
||||||
|
value={values.networkId}
|
||||||
|
onChange={(value) => setFieldValue('networkId', value)}
|
||||||
|
hiddenNetworks={selectedNetworks}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<LoadingButton
|
||||||
|
loadingText="Joining network..."
|
||||||
|
isLoading={connectMutation.isLoading}
|
||||||
|
>
|
||||||
|
Join Network
|
||||||
|
</LoadingButton>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSubmit({ networkId }: { networkId: string }) {
|
||||||
|
connectMutation.mutate(
|
||||||
|
{ containerId, networkId, nodeName },
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
router.stateService.reload();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validation(): SchemaOf<FormValues> {
|
||||||
|
return object({
|
||||||
|
networkId: string().required('Please select a network'),
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Share2 } from 'lucide-react';
|
||||||
|
import { EndpointSettings, NetworkSettings } from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
|
import { createPersistedStore } from '@@/datatables/types';
|
||||||
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||||
|
import { withMeta } from '@@/datatables/extend-options/withMeta';
|
||||||
|
|
||||||
|
import { DockerContainer } from '../../types';
|
||||||
|
|
||||||
|
import { TableNetwork } from './types';
|
||||||
|
import { columns } from './columns';
|
||||||
|
import { ConnectNetworkForm } from './ConnectNetworkForm';
|
||||||
|
|
||||||
|
const storageKey = 'container-networks';
|
||||||
|
const store = createPersistedStore(storageKey, 'name');
|
||||||
|
|
||||||
|
export function ContainerNetworksDatatable({
|
||||||
|
dataset,
|
||||||
|
container,
|
||||||
|
nodeName,
|
||||||
|
}: {
|
||||||
|
dataset: NetworkSettings['Networks'];
|
||||||
|
container: DockerContainer;
|
||||||
|
nodeName?: string;
|
||||||
|
}) {
|
||||||
|
const tableState = useTableState(store, storageKey);
|
||||||
|
|
||||||
|
const networks: Array<TableNetwork> = Object.entries(dataset || {})
|
||||||
|
.filter(isNetworkDefined)
|
||||||
|
.map(([id, network]) => ({
|
||||||
|
...network,
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExpandableDatatable<TableNetwork>
|
||||||
|
columns={columns}
|
||||||
|
dataset={networks}
|
||||||
|
settingsManager={tableState}
|
||||||
|
title="Connected Networks"
|
||||||
|
titleIcon={Share2}
|
||||||
|
disableSelect
|
||||||
|
getRowCanExpand={(row) => !!row.original.GlobalIPv6Address}
|
||||||
|
isLoading={!dataset}
|
||||||
|
renderSubRow={({ original: item }) => (
|
||||||
|
<tr className="datatable-highlighted">
|
||||||
|
<td colSpan={2} />
|
||||||
|
<td>{item.GlobalIPv6Address}</td>
|
||||||
|
<td colSpan={3}>{item.IPv6Gateway || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
description={
|
||||||
|
<ConnectNetworkForm
|
||||||
|
containerId={container.Id}
|
||||||
|
nodeName={nodeName}
|
||||||
|
selectedNetworks={networks.map((n) => n.id)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
extendTableOptions={withMeta({
|
||||||
|
table: 'container-networks',
|
||||||
|
containerId: container.Id,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNetworkDefined(
|
||||||
|
value: [string, EndpointSettings | undefined]
|
||||||
|
): value is [string, EndpointSettings] {
|
||||||
|
return value.length > 1 && !!value[1];
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
import { useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { Authorized } from '@/react/hooks/useUser';
|
||||||
|
import { useDisconnectContainer } from '@/react/docker/networks/queries';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
import { LoadingButton } from '@@/buttons';
|
||||||
|
|
||||||
|
import { TableNetwork, isContainerNetworkTableMeta } from './types';
|
||||||
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
|
export const actions = columnHelper.display({
|
||||||
|
header: 'Actions',
|
||||||
|
cell: Cell,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Cell({
|
||||||
|
row,
|
||||||
|
table: {
|
||||||
|
options: { meta },
|
||||||
|
},
|
||||||
|
}: CellContext<TableNetwork, unknown>) {
|
||||||
|
const router = useRouter();
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const disconnectMutation = useDisconnectContainer();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Authorized authorizations="DockerNetworkDisconnect">
|
||||||
|
<LoadingButton
|
||||||
|
color="dangerlight"
|
||||||
|
isLoading={disconnectMutation.isLoading}
|
||||||
|
loadingText="Leaving network..."
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Leave network
|
||||||
|
</LoadingButton>
|
||||||
|
</Authorized>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (!isContainerNetworkTableMeta(meta)) {
|
||||||
|
throw new Error('Invalid row meta');
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectMutation.mutate(
|
||||||
|
{
|
||||||
|
environmentId,
|
||||||
|
networkId: row.original.id,
|
||||||
|
containerId: meta.containerId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
router.stateService.reload();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { buildExpandColumn } from '@@/datatables/expand-column';
|
||||||
|
import { buildNameColumn } from '@@/datatables/buildNameColumn';
|
||||||
|
|
||||||
|
import { TableNetwork } from './types';
|
||||||
|
import { columnHelper } from './helper';
|
||||||
|
import { actions } from './actions';
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
buildExpandColumn<TableNetwork>(),
|
||||||
|
{
|
||||||
|
...buildNameColumn<TableNetwork>('name', 'docker.networks.network'),
|
||||||
|
header: 'Network',
|
||||||
|
},
|
||||||
|
columnHelper.accessor((item) => item.IPAddress || '-', {
|
||||||
|
header: 'IP Address',
|
||||||
|
id: 'ip',
|
||||||
|
enableSorting: false,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor((item) => item.Gateway || '-', {
|
||||||
|
header: 'Gateway',
|
||||||
|
id: 'gateway',
|
||||||
|
enableSorting: false,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor((item) => item.MacAddress || '-', {
|
||||||
|
header: 'MAC Address',
|
||||||
|
id: 'macAddress',
|
||||||
|
enableSorting: false,
|
||||||
|
}),
|
||||||
|
actions,
|
||||||
|
];
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { TableNetwork } from './types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<TableNetwork>();
|
|
@ -0,0 +1 @@
|
||||||
|
export { ContainerNetworksDatatable } from './ContainerNetworksDatatable';
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { TableMeta } from '@tanstack/react-table';
|
||||||
|
import { EndpointSettings } from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
|
export type TableNetwork = EndpointSettings & { id: string; name: string };
|
||||||
|
|
||||||
|
export type ContainerNetworkTableMeta = TableMeta<TableNetwork> & {
|
||||||
|
table: 'container-networks';
|
||||||
|
containerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isContainerNetworkTableMeta(
|
||||||
|
meta?: TableMeta<TableNetwork>
|
||||||
|
): meta is ContainerNetworkTableMeta {
|
||||||
|
return !!meta && meta.table === 'container-networks';
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useNetworks } from '@/react/docker/networks/queries/useNetworks';
|
||||||
|
import { DockerNetwork } from '@/react/docker/networks/types';
|
||||||
|
import { useIsSwarm } from '@/react/docker/proxy/queries/useInfo';
|
||||||
|
import { useApiVersion } from '@/react/docker/proxy/queries/useVersion';
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
import { Option, PortainerSelect } from '@@/form-components/PortainerSelect';
|
||||||
|
|
||||||
|
export function NetworkSelector({
|
||||||
|
onChange,
|
||||||
|
additionalOptions = [],
|
||||||
|
value,
|
||||||
|
hiddenNetworks = [],
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
additionalOptions?: Array<Option<string>>;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
hiddenNetworks?: string[];
|
||||||
|
}) {
|
||||||
|
const networksQuery = useNetworksForSelector({
|
||||||
|
select(networks) {
|
||||||
|
return networks.map((n) => ({ label: n.Name, value: n.Name }));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const networks = networksQuery.data;
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
(networks || [])
|
||||||
|
.concat(additionalOptions)
|
||||||
|
.filter((n) => !hiddenNetworks.includes(n.value))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||||
|
[additionalOptions, hiddenNetworks, networks]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortainerSelect
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
options={options}
|
||||||
|
isLoading={networksQuery.isLoading}
|
||||||
|
bindToBody
|
||||||
|
placeholder="Select a network"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNetworksForSelector<T = DockerNetwork[]>({
|
||||||
|
select,
|
||||||
|
}: {
|
||||||
|
select?(networks: Array<DockerNetwork>): T;
|
||||||
|
} = {}) {
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
|
||||||
|
const isSwarmQuery = useIsSwarm(environmentId);
|
||||||
|
const dockerApiVersion = useApiVersion(environmentId);
|
||||||
|
|
||||||
|
return useNetworks(
|
||||||
|
environmentId,
|
||||||
|
{
|
||||||
|
local: true,
|
||||||
|
swarmAttachable: isSwarmQuery && dockerApiVersion >= 1.25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
select,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { buildUrl as buildDockerUrl } from '../../proxy/queries/build-url';
|
||||||
|
import { NetworkId } from '../types';
|
||||||
|
|
||||||
|
export function buildUrl(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
{ id, action }: { id?: NetworkId; action?: string } = {}
|
||||||
|
) {
|
||||||
|
let baseUrl = 'networks';
|
||||||
|
if (id) {
|
||||||
|
baseUrl += `/${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
baseUrl += `/${action}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildDockerUrl(environmentId, baseUrl);
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { queryKeys as dockerQueryKeys } from '../../queries/utils';
|
||||||
|
|
||||||
|
import { NetworksQuery } from './types';
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
base: (environmentId: EnvironmentId) =>
|
||||||
|
[...dockerQueryKeys.root(environmentId), 'networks'] as const,
|
||||||
|
list: (environmentId: EnvironmentId, query: NetworksQuery) =>
|
||||||
|
[...queryKeys.base(environmentId), 'list', query] as const,
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
interface Filters {
|
||||||
|
/* dangling=<boolean> When set to true (or 1), returns all networks that are not in use by a container. When set to false (or 0), only networks that are in use by one or more containers are returned. */
|
||||||
|
dangling?: boolean[];
|
||||||
|
// Matches a network's driver
|
||||||
|
driver?: string[];
|
||||||
|
// Matches all or part of a network ID
|
||||||
|
id?: string[];
|
||||||
|
// `label=<key>` or `label=<key>=<value>` of a network label.
|
||||||
|
label?: string[];
|
||||||
|
// Matches all or part of a network name.
|
||||||
|
name?: string[];
|
||||||
|
// Filters networks by scope (swarm, global, or local).
|
||||||
|
scope?: ('swarm' | 'global' | 'local')[];
|
||||||
|
// Filters networks by type. The custom keyword returns all user-defined networks.
|
||||||
|
type?: ('custom' | 'builtin')[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworksQuery {
|
||||||
|
local?: boolean;
|
||||||
|
swarm?: boolean;
|
||||||
|
swarmAttachable?: boolean;
|
||||||
|
filters?: Filters;
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { EndpointSettings } from 'docker-types/generated/1.41';
|
||||||
|
import { AxiosRequestHeaders } from 'axios';
|
||||||
|
import { useMutation, useQueryClient } from 'react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import {
|
||||||
|
mutationOptions,
|
||||||
|
withError,
|
||||||
|
withInvalidate,
|
||||||
|
} from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { queryKeys as dockerQueryKeys } from '../../queries/utils';
|
||||||
|
|
||||||
|
import { buildUrl } from './buildUrl';
|
||||||
|
|
||||||
|
interface ConnectContainerPayload {
|
||||||
|
Container: string;
|
||||||
|
EndpointConfig?: EndpointSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConnectContainerMutation(environmentId: EnvironmentId) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
(params: Omit<ConnectContainer, 'environmentId'>) =>
|
||||||
|
connectContainer({ ...params, environmentId }),
|
||||||
|
mutationOptions(
|
||||||
|
withError('Failed connecting container to network'),
|
||||||
|
withInvalidate(queryClient, [dockerQueryKeys.containers(environmentId)])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectContainer {
|
||||||
|
environmentId: EnvironmentId;
|
||||||
|
networkId: string;
|
||||||
|
containerId: string;
|
||||||
|
aliases?: EndpointSettings['Aliases'];
|
||||||
|
nodeName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function connectContainer({
|
||||||
|
environmentId,
|
||||||
|
containerId,
|
||||||
|
networkId,
|
||||||
|
aliases,
|
||||||
|
nodeName,
|
||||||
|
}: ConnectContainer) {
|
||||||
|
const payload: ConnectContainerPayload = {
|
||||||
|
Container: containerId,
|
||||||
|
};
|
||||||
|
if (aliases) {
|
||||||
|
payload.EndpointConfig = {
|
||||||
|
Aliases: aliases,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: AxiosRequestHeaders = {};
|
||||||
|
|
||||||
|
if (nodeName) {
|
||||||
|
headers['X-PortainerAgent-Target'] = nodeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
buildUrl(environmentId, { id: networkId, action: 'connect' }),
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err as Error, 'Unable to connect container');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { buildUrl } from '../../proxy/queries/build-url';
|
||||||
|
import { DockerNetwork } from '../types';
|
||||||
|
|
||||||
|
import { queryKeys } from './queryKeys';
|
||||||
|
import { NetworksQuery } from './types';
|
||||||
|
|
||||||
|
export function useNetworks<T = Array<DockerNetwork>>(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
query: NetworksQuery,
|
||||||
|
{
|
||||||
|
enabled = true,
|
||||||
|
onSuccess,
|
||||||
|
select,
|
||||||
|
}: {
|
||||||
|
enabled?: boolean;
|
||||||
|
onSuccess?(networks: T): void;
|
||||||
|
select?(networks: Array<DockerNetwork>): T;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
queryKeys.list(environmentId, query),
|
||||||
|
() => getNetworks(environmentId, query),
|
||||||
|
{ enabled, onSuccess, select }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNetworks(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
{ local, swarm, swarmAttachable, filters }: NetworksQuery
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<Array<DockerNetwork>>(
|
||||||
|
buildUrl(environmentId, 'networks'),
|
||||||
|
filters && {
|
||||||
|
params: {
|
||||||
|
filters,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return !local && !swarm && !swarmAttachable
|
||||||
|
? data
|
||||||
|
: data.filter(
|
||||||
|
(network) =>
|
||||||
|
(local && network.Scope === 'local') ||
|
||||||
|
(swarm && network.Scope === 'swarm') ||
|
||||||
|
(swarmAttachable &&
|
||||||
|
network.Scope === 'swarm' &&
|
||||||
|
network.Attachable === true)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err as Error, 'Unable to retrieve networks');
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,3 +41,14 @@ export function useIsSwarm(environmentId: EnvironmentId) {
|
||||||
|
|
||||||
return !!query.data;
|
return !!query.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useSystemLimits(environmentId: EnvironmentId) {
|
||||||
|
const infoQuery = useInfo(environmentId);
|
||||||
|
|
||||||
|
const maxCpu = infoQuery.data?.NCPU || 32;
|
||||||
|
const maxMemory = infoQuery.data?.MemTotal
|
||||||
|
? Math.floor(infoQuery.data.MemTotal / 1000 / 1000)
|
||||||
|
: 32768;
|
||||||
|
|
||||||
|
return { maxCpu, maxMemory };
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
import { SystemVersion } from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { buildUrl } from './build-url';
|
import { buildUrl } from './build-url';
|
||||||
|
|
||||||
export interface VersionResponse {
|
|
||||||
ApiVersion: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getVersion(environmentId: EnvironmentId) {
|
export async function getVersion(environmentId: EnvironmentId) {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<VersionResponse>(
|
const { data } = await axios.get<SystemVersion>(
|
||||||
buildUrl(environmentId, 'version')
|
buildUrl(environmentId, 'version')
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
|
@ -20,9 +17,9 @@ export async function getVersion(environmentId: EnvironmentId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useVersion<TSelect = VersionResponse>(
|
export function useVersion<TSelect = SystemVersion>(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
select?: (info: VersionResponse) => TSelect
|
select?: (info: SystemVersion) => TSelect
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery(
|
||||||
['environment', environmentId, 'docker', 'version'],
|
['environment', environmentId, 'docker', 'version'],
|
||||||
|
@ -32,3 +29,8 @@ export function useVersion<TSelect = VersionResponse>(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useApiVersion(environmentId: EnvironmentId) {
|
||||||
|
const query = useVersion(environmentId, (info) => info.ApiVersion);
|
||||||
|
return query.data ? parseFloat(query.data) : 0;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
|
||||||
|
|
||||||
export const queryKeys = {
|
|
||||||
root: (environmentId: EnvironmentId) => ['docker', environmentId] as const,
|
|
||||||
};
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { DockerContainer } from '@/react/docker/containers/types';
|
||||||
|
import { EdgeStack } from '@/react/edge/edge-stacks/types';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { buildDockerSnapshotUrl, queryKeys as rootQueryKeys } from './root';
|
||||||
|
|
||||||
|
export interface ContainersQueryParams {
|
||||||
|
edgeStackId?: EdgeStack['Id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
...rootQueryKeys,
|
||||||
|
containers: (environmentId: EnvironmentId) =>
|
||||||
|
[...queryKeys.snapshot(environmentId), 'containers'] as const,
|
||||||
|
containersQuery: (
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
params: ContainersQueryParams
|
||||||
|
) => [...queryKeys.containers(environmentId), params] as const,
|
||||||
|
container: (
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
containerId: DockerContainer['Id']
|
||||||
|
) => [...queryKeys.containers(environmentId), containerId] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildDockerSnapshotContainersUrl(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
containerId?: DockerContainer['Id']
|
||||||
|
) {
|
||||||
|
let url = `${buildDockerSnapshotUrl(environmentId)}/containers`;
|
||||||
|
|
||||||
|
if (containerId) {
|
||||||
|
url += `/${containerId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { queryKeys as containerQueryKeys } from './container';
|
||||||
|
import { queryKeys as rootQueryKeys } from './root';
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
...rootQueryKeys,
|
||||||
|
...containerQueryKeys,
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildDockerSnapshotContainersUrl,
|
||||||
|
type ContainersQueryParams,
|
||||||
|
} from './container';
|
||||||
|
|
||||||
|
export { buildDockerSnapshotUrl } from './root';
|
|
@ -17,7 +17,7 @@ import {
|
||||||
} from '@/react/portainer/environments/types';
|
} from '@/react/portainer/environments/types';
|
||||||
import { Authorized, useUser, isEnvironmentAdmin } from '@/react/hooks/useUser';
|
import { Authorized, useUser, isEnvironmentAdmin } from '@/react/hooks/useUser';
|
||||||
import { useInfo } from '@/react/docker/proxy/queries/useInfo';
|
import { useInfo } from '@/react/docker/proxy/queries/useInfo';
|
||||||
import { useVersion } from '@/react/docker/proxy/queries/useVersion';
|
import { useApiVersion } from '@/react/docker/proxy/queries/useVersion';
|
||||||
|
|
||||||
import { SidebarItem } from './SidebarItem';
|
import { SidebarItem } from './SidebarItem';
|
||||||
import { DashboardLink } from './items/DashboardLink';
|
import { DashboardLink } from './items/DashboardLink';
|
||||||
|
@ -40,12 +40,9 @@ export function DockerSidebar({ environmentId, environment }: Props) {
|
||||||
(info) => !!info.Swarm?.NodeID && !!info.Swarm?.ControlAvailable
|
(info) => !!info.Swarm?.NodeID && !!info.Swarm?.ControlAvailable
|
||||||
);
|
);
|
||||||
|
|
||||||
const envVersionQuery = useVersion(environmentId, (version) =>
|
const apiVersion = useApiVersion(environmentId);
|
||||||
parseFloat(version.ApiVersion)
|
|
||||||
);
|
|
||||||
|
|
||||||
const isSwarmManager = envInfoQuery.data;
|
const isSwarmManager = envInfoQuery.data;
|
||||||
const apiVersion = envVersionQuery.data || 0;
|
|
||||||
|
|
||||||
const setupSubMenuProps = isSwarmManager
|
const setupSubMenuProps = isSwarmManager
|
||||||
? {
|
? {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { DefaultBodyType, PathParams, rest } from 'msw';
|
import { DefaultBodyType, PathParams, rest } from 'msw';
|
||||||
import { SystemInfo } from 'docker-types/generated/1.41';
|
import { SystemInfo, SystemVersion } from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
import { VersionResponse } from '@/react/docker/proxy/queries/useVersion';
|
|
||||||
|
|
||||||
export const dockerHandlers = [
|
export const dockerHandlers = [
|
||||||
rest.get<DefaultBodyType, PathParams, SystemInfo>(
|
rest.get<DefaultBodyType, PathParams, SystemInfo>(
|
||||||
|
@ -16,7 +14,7 @@ export const dockerHandlers = [
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
rest.get<DefaultBodyType, PathParams, VersionResponse>(
|
rest.get<DefaultBodyType, PathParams, SystemVersion>(
|
||||||
'/api/endpoints/:endpointId/docker/version',
|
'/api/endpoints/:endpointId/docker/version',
|
||||||
(req, res, ctx) => res(ctx.json({ ApiVersion: '1.24' }))
|
(req, res, ctx) => res(ctx.json({ ApiVersion: '1.24' }))
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue