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,
 | 
			
		||||
  commandsTabValidation,
 | 
			
		||||
} 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(
 | 
			
		||||
  'portainer.docker.react.components.containers',
 | 
			
		||||
  []
 | 
			
		||||
);
 | 
			
		||||
const ngModule = angular
 | 
			
		||||
  .module('portainer.docker.react.components.containers', [])
 | 
			
		||||
  .component(
 | 
			
		||||
    'dockerContainerNetworksDatatable',
 | 
			
		||||
    r2a(withUIRouter(withCurrentUser(ContainerNetworksDatatable)), [
 | 
			
		||||
      'container',
 | 
			
		||||
      'dataset',
 | 
			
		||||
      'nodeName',
 | 
			
		||||
    ])
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
export const containersModule = ngModule.name;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -348,21 +348,15 @@
 | 
			
		|||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col-sm-12">
 | 
			
		||||
    <container-networks-datatable
 | 
			
		||||
      ng-if="container.NetworkSettings.Networks"
 | 
			
		||||
      title-text="Connected networks"
 | 
			
		||||
      title-icon="share-2"
 | 
			
		||||
      dataset="container.NetworkSettings.Networks"
 | 
			
		||||
      table-key="container-networks"
 | 
			
		||||
      container="container"
 | 
			
		||||
      available-networks="availableNetworks"
 | 
			
		||||
      join-network-action="containerJoinNetwork"
 | 
			
		||||
      join-network-action-in-progress="state.joinNetworkInProgress"
 | 
			
		||||
      leave-network-action="containerLeaveNetwork"
 | 
			
		||||
      leave-network-action-in-progress="state.leaveNetworkInProgress"
 | 
			
		||||
      node-name="nodeName"
 | 
			
		||||
    ></container-networks-datatable>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<docker-container-networks-datatable
 | 
			
		||||
  ng-if="container.NetworkSettings.Networks"
 | 
			
		||||
  dataset="container.NetworkSettings.Networks"
 | 
			
		||||
  container="container"
 | 
			
		||||
  available-networks="availableNetworks"
 | 
			
		||||
  on-join="(containerJoinNetwork)"
 | 
			
		||||
  join-in-progress="state.joinNetworkInProgress"
 | 
			
		||||
  on-leave="(containerLeaveNetwork)"
 | 
			
		||||
  leave-in-progress="state.leaveNetworkInProgress"
 | 
			
		||||
  node-name="nodeName"
 | 
			
		||||
>
 | 
			
		||||
</docker-container-networks-datatable>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -180,6 +180,7 @@ export const ngModule = angular
 | 
			
		|||
      'isMulti',
 | 
			
		||||
      'isClearable',
 | 
			
		||||
      'components',
 | 
			
		||||
      'isLoading',
 | 
			
		||||
    ])
 | 
			
		||||
  )
 | 
			
		||||
  .component(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ interface SharedProps extends AutomationTestingProps {
 | 
			
		|||
  disabled?: boolean;
 | 
			
		||||
  isClearable?: boolean;
 | 
			
		||||
  bindToBody?: boolean;
 | 
			
		||||
  isLoading?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface MultiProps<TValue> extends SharedProps {
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +83,7 @@ export function SingleSelect<TValue = string>({
 | 
			
		|||
  isClearable,
 | 
			
		||||
  bindToBody,
 | 
			
		||||
  components,
 | 
			
		||||
  isLoading,
 | 
			
		||||
}: SingleProps<TValue>) {
 | 
			
		||||
  const selectedValue =
 | 
			
		||||
    value || (typeof value === 'number' && value === 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +105,7 @@ export function SingleSelect<TValue = string>({
 | 
			
		|||
      isDisabled={disabled}
 | 
			
		||||
      menuPortalTarget={bindToBody ? document.body : undefined}
 | 
			
		||||
      components={components}
 | 
			
		||||
      isLoading={isLoading}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -142,6 +145,7 @@ export function MultiSelect<TValue = string>({
 | 
			
		|||
  isClearable,
 | 
			
		||||
  bindToBody,
 | 
			
		||||
  components,
 | 
			
		||||
  isLoading,
 | 
			
		||||
}: Omit<MultiProps<TValue>, 'isMulti'>) {
 | 
			
		||||
  const selectedOptions = findSelectedOptions(options, value);
 | 
			
		||||
  return (
 | 
			
		||||
| 
						 | 
				
			
			@ -161,6 +165,7 @@ export function MultiSelect<TValue = string>({
 | 
			
		|||
      isDisabled={disabled}
 | 
			
		||||
      menuPortalTarget={bindToBody ? document.body : undefined}
 | 
			
		||||
      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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 { SystemVersion } from 'docker-types/generated/1.41';
 | 
			
		||||
 | 
			
		||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
 | 
			
		||||
import { EnvironmentId } from '@/react/portainer/environments/types';
 | 
			
		||||
 | 
			
		||||
import { buildUrl } from './build-url';
 | 
			
		||||
 | 
			
		||||
export interface VersionResponse {
 | 
			
		||||
  ApiVersion: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getVersion(environmentId: EnvironmentId) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get<VersionResponse>(
 | 
			
		||||
    const { data } = await axios.get<SystemVersion>(
 | 
			
		||||
      buildUrl(environmentId, 'version')
 | 
			
		||||
    );
 | 
			
		||||
    return data;
 | 
			
		||||
| 
						 | 
				
			
			@ -20,9 +17,9 @@ export async function getVersion(environmentId: EnvironmentId) {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useVersion<TSelect = VersionResponse>(
 | 
			
		||||
export function useVersion<TSelect = SystemVersion>(
 | 
			
		||||
  environmentId: EnvironmentId,
 | 
			
		||||
  select?: (info: VersionResponse) => TSelect
 | 
			
		||||
  select?: (info: SystemVersion) => TSelect
 | 
			
		||||
) {
 | 
			
		||||
  return useQuery(
 | 
			
		||||
    ['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';
 | 
			
		||||
import { Authorized, useUser, isEnvironmentAdmin } from '@/react/hooks/useUser';
 | 
			
		||||
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 { DashboardLink } from './items/DashboardLink';
 | 
			
		||||
| 
						 | 
				
			
			@ -40,12 +40,9 @@ export function DockerSidebar({ environmentId, environment }: Props) {
 | 
			
		|||
    (info) => !!info.Swarm?.NodeID && !!info.Swarm?.ControlAvailable
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const envVersionQuery = useVersion(environmentId, (version) =>
 | 
			
		||||
    parseFloat(version.ApiVersion)
 | 
			
		||||
  );
 | 
			
		||||
  const apiVersion = useApiVersion(environmentId);
 | 
			
		||||
 | 
			
		||||
  const isSwarmManager = envInfoQuery.data;
 | 
			
		||||
  const apiVersion = envVersionQuery.data || 0;
 | 
			
		||||
 | 
			
		||||
  const setupSubMenuProps = isSwarmManager
 | 
			
		||||
    ? {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,5 @@
 | 
			
		|||
import { DefaultBodyType, PathParams, rest } from 'msw';
 | 
			
		||||
import { SystemInfo } from 'docker-types/generated/1.41';
 | 
			
		||||
 | 
			
		||||
import { VersionResponse } from '@/react/docker/proxy/queries/useVersion';
 | 
			
		||||
import { SystemInfo, SystemVersion } from 'docker-types/generated/1.41';
 | 
			
		||||
 | 
			
		||||
export const dockerHandlers = [
 | 
			
		||||
  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',
 | 
			
		||||
    (req, res, ctx) => res(ctx.json({ ApiVersion: '1.24' }))
 | 
			
		||||
  ),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue