From f7366d9788a9a7649bfc39b496f737a904265d09 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 4 Sep 2023 19:07:29 +0100 Subject: [PATCH] refactor(docker/containers): migrate commands tab to react [EE-5208] (#10085) --- app/docker/helpers/containerHelper.js | 13 - app/docker/helpers/containers.ts | 9 + app/docker/helpers/splitargs.test.ts | 68 + app/docker/helpers/splitargs.ts | 114 ++ app/docker/react/components/containers.ts | 26 + app/docker/react/components/index.ts | 4 +- app/docker/react/views/containers.ts | 2 +- app/docker/services/system.service.ts | 76 -- .../console/containerConsoleController.js | 3 +- .../create/createContainerController.js | 152 +-- .../containers/create/createcontainer.html | 1114 +++++++---------- app/portainer/services/api/templateService.js | 3 +- app/portainer/services/axios.ts | 2 +- .../endpoints/edit/endpointController.js | 2 +- .../FormControl/FormControl.tsx | 4 +- .../form-components/Input/Input.tsx | 4 +- .../InputGroup/InputGroupButtonWrapper.tsx | 27 +- .../form-components/InputGroup/index.ts | 13 + .../CreateView/CommandsTab/CommandsTab.tsx | 105 ++ .../CommandsTab/ConsoleSettings.tsx | 95 ++ .../CreateView/CommandsTab/LoggerConfig.tsx | 138 ++ .../CommandsTab/OverridableInput.tsx | 48 + .../CreateView/CommandsTab/index.ts | 14 + .../CreateView/CommandsTab/toRequest.ts | 60 + .../CreateView/CommandsTab/toViewModel.tsx | 70 ++ .../CreateView/CommandsTab/types.ts | 11 + .../CreateView/CommandsTab/validation.ts | 16 + .../docker/containers/CreateView/types.ts | 10 + .../docker/containers/ListView/ListView.tsx | 2 +- .../docker/containers/queries/container.ts | 116 ++ .../docker/containers/queries/containers.ts | 4 +- app/react/docker/containers/types/response.ts | 74 +- app/react/docker/containers/utils.ts | 4 +- app/react/docker/proxy/queries/useInfo.ts | 43 + .../docker/proxy/queries/useServicePlugins.ts | 114 ++ app/react/docker/proxy/queries/useVersion.ts | 34 + app/react/docker/queries/utils/root.ts | 19 + app/react/docker/types.ts | 4 + app/react/sidebar/DockerSidebar.tsx | 3 +- app/setup-tests/setup-handlers/docker.ts | 18 +- package.json | 2 +- yarn.lock | 94 +- 42 files changed, 1783 insertions(+), 951 deletions(-) create mode 100644 app/docker/helpers/containers.ts create mode 100644 app/docker/helpers/splitargs.test.ts create mode 100644 app/docker/helpers/splitargs.ts create mode 100644 app/docker/react/components/containers.ts delete mode 100644 app/docker/services/system.service.ts create mode 100644 app/react/docker/containers/CreateView/CommandsTab/CommandsTab.tsx create mode 100644 app/react/docker/containers/CreateView/CommandsTab/ConsoleSettings.tsx create mode 100644 app/react/docker/containers/CreateView/CommandsTab/LoggerConfig.tsx create mode 100644 app/react/docker/containers/CreateView/CommandsTab/OverridableInput.tsx create mode 100644 app/react/docker/containers/CreateView/CommandsTab/index.ts create mode 100644 app/react/docker/containers/CreateView/CommandsTab/toRequest.ts create mode 100644 app/react/docker/containers/CreateView/CommandsTab/toViewModel.tsx create mode 100644 app/react/docker/containers/CreateView/CommandsTab/types.ts create mode 100644 app/react/docker/containers/CreateView/CommandsTab/validation.ts create mode 100644 app/react/docker/containers/CreateView/types.ts create mode 100644 app/react/docker/containers/queries/container.ts create mode 100644 app/react/docker/proxy/queries/useInfo.ts create mode 100644 app/react/docker/proxy/queries/useServicePlugins.ts create mode 100644 app/react/docker/proxy/queries/useVersion.ts create mode 100644 app/react/docker/queries/utils/root.ts diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js index ca38e9aab..90446c197 100644 --- a/app/docker/helpers/containerHelper.js +++ b/app/docker/helpers/containerHelper.js @@ -1,5 +1,4 @@ import _ from 'lodash-es'; -import splitargs from 'splitargs/src/splitargs'; const portPattern = /^([1-9]|[1-5]?[0-9]{2,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/m; @@ -65,18 +64,6 @@ angular.module('portainer.docker').factory('ContainerHelper', [ 'use strict'; var helper = {}; - helper.commandStringToArray = function (command) { - return splitargs(command); - }; - - helper.commandArrayToString = function (array) { - return array - .map(function (elem) { - return "'" + elem + "'"; - }) - .join(' '); - }; - helper.configFromContainer = function (container) { var config = container.Config; // HostConfig diff --git a/app/docker/helpers/containers.ts b/app/docker/helpers/containers.ts new file mode 100644 index 000000000..e857c472b --- /dev/null +++ b/app/docker/helpers/containers.ts @@ -0,0 +1,9 @@ +import { splitargs } from './splitargs'; + +export function commandStringToArray(command: string) { + return splitargs(command); +} + +export function commandArrayToString(array: string[]) { + return array.map((elem) => `'${elem}'`).join(' '); +} diff --git a/app/docker/helpers/splitargs.test.ts b/app/docker/helpers/splitargs.test.ts new file mode 100644 index 000000000..dbdcc7962 --- /dev/null +++ b/app/docker/helpers/splitargs.test.ts @@ -0,0 +1,68 @@ +/** + * Created by elgs on 7/2/14. + */ + +import { splitargs } from './splitargs'; + +describe('splitargs Suite', () => { + beforeEach(() => {}); + afterEach(() => {}); + + it('should split double quoted string', () => { + const i = " I said 'I am sorry.', and he said \"it doesn't matter.\" "; + const o = splitargs(i); + expect(7).toBe(o.length); + expect(o[0]).toBe('I'); + expect(o[1]).toBe('said'); + expect(o[2]).toBe('I am sorry.,'); + expect(o[3]).toBe('and'); + expect(o[4]).toBe('he'); + expect(o[5]).toBe('said'); + expect(o[6]).toBe("it doesn't matter."); + }); + + it('should split pure double quoted string', () => { + const i = 'I said "I am sorry.", and he said "it doesn\'t matter."'; + const o = splitargs(i); + expect(o).toHaveLength(7); + expect(o[0]).toBe('I'); + expect(o[1]).toBe('said'); + expect(o[2]).toBe('I am sorry.,'); + expect(o[3]).toBe('and'); + expect(o[4]).toBe('he'); + expect(o[5]).toBe('said'); + expect(o[6]).toBe("it doesn't matter."); + }); + + it('should split single quoted string', () => { + const i = 'I said "I am sorry.", and he said "it doesn\'t matter."'; + const o = splitargs(i); + expect(o).toHaveLength(7); + expect(o[0]).toBe('I'); + expect(o[1]).toBe('said'); + expect(o[2]).toBe('I am sorry.,'); + expect(o[3]).toBe('and'); + expect(o[4]).toBe('he'); + expect(o[5]).toBe('said'); + expect(o[6]).toBe("it doesn't matter."); + }); + + it('should split pure single quoted string', () => { + const i = "I said 'I am sorry.', and he said \"it doesn't matter.\""; + const o = splitargs(i); + expect(o).toHaveLength(7); + expect(o[0]).toBe('I'); + expect(o[1]).toBe('said'); + expect(o[2]).toBe('I am sorry.,'); + expect(o[3]).toBe('and'); + expect(o[4]).toBe('he'); + expect(o[5]).toBe('said'); + expect(o[6]).toBe("it doesn't matter."); + }); + + it('should split to 4 empty strings', () => { + const i = ',,,'; + const o = splitargs(i, ',', true); + expect(o).toHaveLength(4); + }); +}); diff --git a/app/docker/helpers/splitargs.ts b/app/docker/helpers/splitargs.ts new file mode 100644 index 000000000..9cbcc8c08 --- /dev/null +++ b/app/docker/helpers/splitargs.ts @@ -0,0 +1,114 @@ +/** + +Splits strings into tokens by given separator except treating quoted part as a single token. + + +#Usage +```javascript +var splitargs = require('splitargs'); + +var i1 = "I said 'I am sorry.', and he said \"it doesn't matter.\""; +var o1 = splitargs(i1); +console.log(o1); + +[ 'I', + 'said', + 'I am sorry.,', + 'and', + 'he', + 'said', + 'it doesn\'t matter.' ] + + +var i2 = "I said \"I am sorry.\", and he said \"it doesn't matter.\""; +var o2 = splitargs(i2); +console.log(o2); + +[ 'I', + 'said', + 'I am sorry.,', + 'and', + 'he', + 'said', + 'it doesn\'t matter.' ] + + +var i3 = 'I said "I am sorry.", and he said "it doesn\'t matter."'; +var o3 = splitargs(i3); +console.log(o3); + +[ 'I', + 'said', + 'I am sorry.,', + 'and', + 'he', + 'said', + 'it doesn\'t matter.' ] + + +var i4 = 'I said \'I am sorry.\', and he said "it doesn\'t matter."'; +var o4 = splitargs(i4); +console.log(o4); + +[ 'I', + 'said', + 'I am sorry.,', + 'and', + 'he', + 'said', + 'it doesn\'t matter.' ] + ``` + */ + +export function splitargs( + input: string, + sep?: RegExp | string, + keepQuotes = false +) { + const separator = sep || /\s/g; + let singleQuoteOpen = false; + let doubleQuoteOpen = false; + let tokenBuffer = []; + const ret = []; + + const arr = input.split(''); + for (let i = 0; i < arr.length; ++i) { + const element = arr[i]; + const matches = element.match(separator); + // TODO rewrite without continue + /* eslint-disable no-continue */ + if (element === "'" && !doubleQuoteOpen) { + if (keepQuotes) { + tokenBuffer.push(element); + } + singleQuoteOpen = !singleQuoteOpen; + continue; + } else if (element === '"' && !singleQuoteOpen) { + if (keepQuotes) { + tokenBuffer.push(element); + } + doubleQuoteOpen = !doubleQuoteOpen; + continue; + } + /* eslint-enable no-continue */ + + if (!singleQuoteOpen && !doubleQuoteOpen && matches) { + if (tokenBuffer.length > 0) { + ret.push(tokenBuffer.join('')); + tokenBuffer = []; + } else if (sep) { + ret.push(element); + } + } else { + tokenBuffer.push(element); + } + } + + if (tokenBuffer.length > 0) { + ret.push(tokenBuffer.join('')); + } else if (sep) { + ret.push(''); + } + + return ret; +} diff --git a/app/docker/react/components/containers.ts b/app/docker/react/components/containers.ts new file mode 100644 index 000000000..7a3b6d5a6 --- /dev/null +++ b/app/docker/react/components/containers.ts @@ -0,0 +1,26 @@ +import angular from 'angular'; +import { ComponentProps } from 'react'; + +import { withUIRouter } from '@/react-tools/withUIRouter'; +import { withReactQuery } from '@/react-tools/withReactQuery'; +import { withFormValidation } from '@/react-tools/withFormValidation'; +import { + CommandsTab, + CommandsTabValues, + commandsTabValidation, +} from '@/react/docker/containers/CreateView/CommandsTab'; + +const ngModule = angular.module( + 'portainer.docker.react.components.containers', + [] +); + +export const containersModule = ngModule.name; + +withFormValidation, CommandsTabValues>( + ngModule, + withUIRouter(withReactQuery(CommandsTab)), + 'dockerCreateContainerCommandsTab', + ['apiVersion'], + commandsTabValidation +); diff --git a/app/docker/react/components/index.ts b/app/docker/react/components/index.ts index 4ad27f5bb..edf9adc49 100644 --- a/app/docker/react/components/index.ts +++ b/app/docker/react/components/index.ts @@ -22,8 +22,10 @@ import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowse import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser'; import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable'; +import { containersModule } from './containers'; + const ngModule = angular - .module('portainer.docker.react.components', []) + .module('portainer.docker.react.components', [containersModule]) .component('dockerfileDetails', r2a(DockerfileDetails, ['image'])) .component('dockerHealthStatus', r2a(HealthStatus, ['health'])) .component( diff --git a/app/docker/react/views/containers.ts b/app/docker/react/views/containers.ts index a01bb2a91..b3aa3ef42 100644 --- a/app/docker/react/views/containers.ts +++ b/app/docker/react/views/containers.ts @@ -8,7 +8,7 @@ import { withReactQuery } from '@/react-tools/withReactQuery'; import { withUIRouter } from '@/react-tools/withUIRouter'; export const containersModule = angular - .module('portainer.docker.containers', []) + .module('portainer.docker.react.views.containers', []) .component( 'containersView', r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), ['endpoint']) diff --git a/app/docker/services/system.service.ts b/app/docker/services/system.service.ts deleted file mode 100644 index b7a16aaf9..000000000 --- a/app/docker/services/system.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useQuery } from 'react-query'; - -import { EnvironmentId } from '@/react/portainer/environments/types'; -import axios, { parseAxiosError } from '@/portainer/services/axios'; - -export interface VersionResponse { - ApiVersion: string; -} - -export async function getVersion(environmentId: EnvironmentId) { - try { - const { data } = await axios.get( - buildUrl(environmentId, 'version') - ); - return data; - } catch (err) { - throw parseAxiosError(err as Error, 'Unable to retrieve version'); - } -} - -export interface InfoResponse { - Swarm?: { - NodeID: string; - ControlAvailable: boolean; - }; -} - -export async function getInfo(environmentId: EnvironmentId) { - try { - const { data } = await axios.get( - buildUrl(environmentId, 'info') - ); - return data; - } catch (err) { - throw parseAxiosError(err as Error, 'Unable to retrieve version'); - } -} - -export function useInfo( - environmentId: EnvironmentId, - select?: (info: InfoResponse) => TSelect -) { - return useQuery( - ['environment', environmentId, 'docker', 'info'], - () => getInfo(environmentId), - { - select, - } - ); -} -export function useVersion( - environmentId: EnvironmentId, - select?: (info: VersionResponse) => TSelect -) { - return useQuery( - ['environment', environmentId, 'docker', 'version'], - () => getVersion(environmentId), - { - select, - } - ); -} - -function buildUrl( - environmentId: EnvironmentId, - action: string, - subAction = '' -) { - let url = `/endpoints/${environmentId}/docker/${action}`; - - if (subAction) { - url += `/${subAction}`; - } - - return url; -} diff --git a/app/docker/views/containers/console/containerConsoleController.js b/app/docker/views/containers/console/containerConsoleController.js index 2c7d14520..575c2fd18 100644 --- a/app/docker/views/containers/console/containerConsoleController.js +++ b/app/docker/views/containers/console/containerConsoleController.js @@ -1,5 +1,6 @@ import { Terminal } from 'xterm'; import { baseHref } from '@/portainer/helpers/pathHelper'; +import { commandStringToArray } from '@/docker/helpers/containers'; angular.module('portainer.docker').controller('ContainerConsoleController', [ '$scope', @@ -101,7 +102,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [ AttachStderr: true, Tty: true, User: $scope.formValues.user, - Cmd: ContainerHelper.commandStringToArray(command), + Cmd: commandStringToArray(command), }; ContainerService.createExec(execConfig) diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 20c0148ae..75f13d9d2 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -7,9 +7,10 @@ import { confirmDestructive } from '@@/modals/confirm'; import { FeatureId } from '@/react/portainer/feature-flags/enums'; import { buildConfirmButton } from '@@/modals/utils'; -import { ContainerCapabilities, ContainerCapability } from '../../../models/containerCapabilities'; -import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel'; -import { ContainerDetailsViewModel } from '../../../models/container'; +import { commandsTabUtils } from '@/react/docker/containers/CreateView/CommandsTab'; +import { ContainerCapabilities, ContainerCapability } from '@/docker/models/containerCapabilities'; +import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel'; +import { ContainerDetailsViewModel } from '@/docker/models/container'; import './createcontainer.css'; @@ -20,11 +21,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [ '$state', '$timeout', '$transition$', - '$filter', '$analytics', 'Container', 'ContainerHelper', - 'Image', 'ImageHelper', 'Volume', 'NetworkService', @@ -37,7 +36,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ 'RegistryService', 'SystemService', 'SettingsService', - 'PluginService', 'HttpRequestHelper', 'endpoint', function ( @@ -47,11 +45,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $state, $timeout, $transition$, - $filter, $analytics, Container, ContainerHelper, - Image, ImageHelper, Volume, NetworkService, @@ -64,7 +60,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ RegistryService, SystemService, SettingsService, - PluginService, HttpRequestHelper, endpoint ) { @@ -80,7 +75,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ selectedGPUs: ['all'], capabilities: ['compute', 'utility'], }, - Console: 'none', Volumes: [], NetworkContainer: null, Labels: [], @@ -95,15 +89,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [ MemoryLimit: 0, MemoryReservation: 0, ShmSize: 64, - CmdMode: 'default', - EntrypointMode: 'default', Env: [], NodeName: null, capabilities: [], Sysctls: [], - LogDriverName: '', - LogDriverOpts: [], RegistryModel: new PorImageRegistryModel(), + commands: commandsTabUtils.getDefaultViewModel(), }; $scope.extraNetworks = {}; @@ -114,6 +105,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ mode: '', pullImageValidity: true, settingUnlimitedResources: false, + containerIsLoaded: false, }; $scope.onAlwaysPullChange = onAlwaysPullChange; @@ -121,6 +113,13 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.handleAutoRemoveChange = handleAutoRemoveChange; $scope.handlePrivilegedChange = handlePrivilegedChange; $scope.handleInitChange = handleInitChange; + $scope.handleCommandsChange = handleCommandsChange; + + function handleCommandsChange(commands) { + return $scope.$evalAsync(() => { + $scope.formValues.commands = commands; + }); + } function onAlwaysPullChange(checked) { return $scope.$evalAsync(() => { @@ -179,10 +178,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.config = { Image: '', Env: [], - Cmd: '', + Cmd: null, MacAddress: '', ExposedPorts: {}, - Entrypoint: '', + Entrypoint: null, + WorkingDir: '', + User: '', HostConfig: { RestartPolicy: { Name: 'no', @@ -201,6 +202,10 @@ angular.module('portainer.docker').controller('CreateContainerController', [ CapAdd: [], CapDrop: [], Sysctls: {}, + LogConfig: { + Type: '', + Config: {}, + }, }, NetworkingConfig: { EndpointsConfig: {}, @@ -262,14 +267,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.formValues.Sysctls.splice(index, 1); }; - $scope.addLogDriverOpt = function () { - $scope.formValues.LogDriverOpts.push({ name: '', value: '' }); - }; - - $scope.removeLogDriverOpt = function (index) { - $scope.formValues.LogDriverOpts.splice(index, 1); - }; - $scope.fromContainerMultipleNetworks = false; function prepareImageConfig(config) { @@ -284,36 +281,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ config.HostConfig.PortBindings = bindings; } - function prepareConsole(config) { - var value = $scope.formValues.Console; - var openStdin = true; - var tty = true; - if (value === 'tty') { - openStdin = false; - } else if (value === 'interactive') { - tty = false; - } else if (value === 'none') { - openStdin = false; - tty = false; - } - config.OpenStdin = openStdin; - config.Tty = tty; - } - - function prepareCmd(config) { - if (_.isEmpty(config.Cmd) || $scope.formValues.CmdMode == 'default') { - delete config.Cmd; - } else { - config.Cmd = ContainerHelper.commandStringToArray(config.Cmd); - } - } - - function prepareEntrypoint(config) { - if ($scope.formValues.EntrypointMode == 'default' || (_.isEmpty(config.Cmd) && _.isEmpty(config.Entrypoint))) { - config.Entrypoint = null; - } - } - function prepareEnvironmentVariables(config) { config.Env = envVarsUtils.convertToArrayOfStrings($scope.formValues.Env); } @@ -447,23 +414,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ } } - function prepareLogDriver(config) { - var logOpts = {}; - if ($scope.formValues.LogDriverName) { - config.HostConfig.LogConfig = { Type: $scope.formValues.LogDriverName }; - if ($scope.formValues.LogDriverName !== 'none') { - $scope.formValues.LogDriverOpts.forEach(function (opt) { - if (opt.name) { - logOpts[opt.name] = opt.value; - } - }); - if (Object.keys(logOpts).length !== 0 && logOpts.constructor === Object) { - config.HostConfig.LogConfig.Config = logOpts; - } - } - } - } - function prepareCapabilities(config) { var allowed = $scope.formValues.capabilities.filter(function (item) { return item.allowed === true; @@ -511,40 +461,22 @@ angular.module('portainer.docker').controller('CreateContainerController', [ function prepareConfiguration() { var config = angular.copy($scope.config); - prepareCmd(config); - prepareEntrypoint(config); + config = commandsTabUtils.toRequest(config, $scope.formValues.commands); + prepareNetworkConfig(config); prepareImageConfig(config); preparePortBindings(config); - prepareConsole(config); prepareEnvironmentVariables(config); prepareVolumes(config); prepareLabels(config); prepareDevices(config); prepareResources(config); - prepareLogDriver(config); prepareCapabilities(config); prepareSysctls(config); prepareGPUOptions(config); return config; } - function loadFromContainerCmd() { - if ($scope.config.Cmd) { - $scope.config.Cmd = ContainerHelper.commandArrayToString($scope.config.Cmd); - $scope.formValues.CmdMode = 'override'; - } - } - - function loadFromContainerEntrypoint() { - if (_.has($scope.config, 'Entrypoint')) { - if ($scope.config.Entrypoint == null) { - $scope.config.Entrypoint = ''; - } - $scope.formValues.EntrypointMode = 'override'; - } - } - function loadFromContainerPortBindings() { const bindings = ContainerHelper.sortAndCombinePorts($scope.config.HostConfig.PortBindings); $scope.config.HostConfig.PortBindings = bindings; @@ -641,18 +573,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ } } - function loadFromContainerConsole() { - if ($scope.config.OpenStdin && $scope.config.Tty) { - $scope.formValues.Console = 'both'; - } else if (!$scope.config.OpenStdin && $scope.config.Tty) { - $scope.formValues.Console = 'tty'; - } else if ($scope.config.OpenStdin && !$scope.config.Tty) { - $scope.formValues.Console = 'interactive'; - } else if (!$scope.config.OpenStdin && !$scope.config.Tty) { - $scope.formValues.Console = 'none'; - } - } - function loadFromContainerDevices() { var path = []; for (var dev in $scope.config.HostConfig.Devices) { @@ -765,15 +685,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.fromContainer = fromContainer; $scope.state.mode = 'duplicate'; $scope.config = ContainerHelper.configFromContainer(fromContainer.Model); - loadFromContainerCmd(d); - loadFromContainerEntrypoint(d); - loadFromContainerLogging(d); + + $scope.formValues.commands = commandsTabUtils.toViewModel(d); + loadFromContainerPortBindings(d); loadFromContainerVolumes(d); loadFromContainerNetworkConfig(d); loadFromContainerEnvironmentVariables(d); loadFromContainerLabels(d); - loadFromContainerConsole(d); loadFromContainerDevices(d); loadFromContainerDeviceRequests(d); loadFromContainerImageConfig(d); @@ -781,22 +700,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [ loadFromContainerCapabilities(d); loadFromContainerSysctls(d); }) + .then(() => { + $scope.state.containerIsLoaded = true; + }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve container'); }); } - function loadFromContainerLogging(config) { - var logConfig = config.HostConfig.LogConfig; - $scope.formValues.LogDriverName = logConfig.Type; - $scope.formValues.LogDriverOpts = _.map(logConfig.Config, function (value, name) { - return { - name: name, - value: value, - }; - }); - } - async function initView() { var nodeName = $transition$.params().nodeName; $scope.formValues.NodeName = nodeName; @@ -845,6 +756,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ if ($transition$.params().from) { loadFromContainerSpec(); } else { + $scope.state.containerIsLoaded = true; $scope.fromContainer = {}; $scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : []; } @@ -872,10 +784,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers; $scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers; - - PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) { - $scope.availableLoggingDrivers = loggingDrivers; - }); } function validateForm(accessControlData, isAdmin) { diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 841c5e913..f50030b33 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -202,645 +202,501 @@
  • Capabilities
  • -
    - -
    -
    - -
    - -
    -
    -
    - - -
    - -
    -
    -
    - - -
    - -
    -
    -
    - - -
    - -
    -
    -
    - - -
    - -
    - -
    - -
    - -
    -
    - - -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - - -
    Logging
    - -
    - -
    - -
    -
    -

    - Logging driver that will override the default docker daemon driver. Select Default logging driver if you don't want to override it. Supported logging drivers - can be found in the Docker documentation. -

    -
    -
    - - -
    -
    - - - add logging driver option - -
    - -
    -
    -
    - option - -
    -
    - value - -
    - -
    -
    - -
    - -
    -
    - - -
    -
    - -
    -
    - - - map additional volume - -
    - -
    -
    - -
    - -
    - container - -
    - - -
    -
    - - -
    - -
    - -
    - - -
    - - -
    - volume - -
    - - -
    - host - -
    - - -
    -
    - - -
    -
    - -
    - -
    -
    - -
    -
    - -
    - - -
    -
    -
    -
    - You don't have any shared networks. Head over to the networks view to create one. -
    -
    - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    -
    - - - add additional entry - -
    - -
    -
    -
    - value - -
    - -
    -
    - -
    - -
    -
    - - -
    -
    - -
    -
    - - add label -
    - -
    -
    -
    - name - -
    -
    - value - -
    - -
    -
    - -
    - -
    -
    - - -
    -
    - +
    +
    + +
    +
    -
    - - -
    -
    -
    -
    - -
    - - - - + + +
    + + +
    +
    + + + map additional volume +
    -
    -
    - -
    - - -
    -
    -
    Runtime
    - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - - -
    - -
    - -
    -
    - -
    -
    - -
    -
    - - add device -
    - -
    -
    -
    - host - -
    -
    - container - -
    - -
    -
    - -
    - - -
    -
    - - add sysctl -
    - -
    -
    -
    - name - -
    -
    - value - -
    - -
    -
    - -
    - - -
    - -
    - -
    -
    -

    Size of /dev/shm (MB)

    -
    -
    - - -
    -
    GPU
    - - - -
    - - -
    -
    Resources
    - -
    - -
    - -
    -
    - -
    -
    -
    -
    -
    -
    -

    - Value must be between 0 and {{ state.sliderMaxMemory }}. -

    + +
    +
    + +
    + +
    + container + +
    + + +
    +
    + + +
    + +
    + +
    + + +
    + + +
    + volume + +
    + + +
    + host + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    - - -
    - -
    - -
    -
    - -
    -
    -
    -
    -
    -
    -

    - Value must be between 0 and {{ state.sliderMaxMemory }}. -

    -
    -
    -
    - - -
    - -
    - -
    -
    - - - -
    + + +
    + + +
    +
    +
    - -
    -
    -

    - - Updating any resource value to ‘unlimited' will redeploy this container. -

    + You don't have any shared networks. Head over to the networks view to create one.
    - + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    +
    + + + add additional entry + +
    + +
    +
    +
    + value + +
    + +
    +
    + +
    + +
    +
    + + +
    +
    + +
    +
    + + add label +
    + +
    +
    +
    + name + +
    +
    + value + +
    + +
    +
    + +
    + +
    +
    + + +
    +
    +
    - +
    + + +
    +
    +
    +
    + +
    + + + + +
    +
    +
    +
    +
    + + +
    +
    +
    Runtime
    + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + + + add device + +
    + +
    +
    +
    + host + +
    +
    + container + +
    + +
    +
    + +
    + + +
    +
    + + add sysctl +
    + +
    +
    +
    + name + +
    +
    + value + +
    + +
    +
    + +
    + + +
    + +
    + +
    +
    +

    Size of /dev/shm (MB)

    +
    +
    + + +
    +
    GPU
    + + +
    + +
    +
    Resources
    + +
    + +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +

    + Value must be between 0 and {{ state.sliderMaxMemory }}. +

    +
    +
    +
    + + +
    + +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +

    + Value must be between 0 and {{ state.sliderMaxMemory }}. +

    +
    +
    +
    + + +
    + +
    + +
    +
    + + +
    +
    + +
    +
    +

    + + Updating any resource value to ‘unlimited' will redeploy this container. +

    +
    +
    + +
    +
    +
    + + +
    + +
    +
    - - -
    - -
    -
    diff --git a/app/portainer/services/api/templateService.js b/app/portainer/services/api/templateService.js index e108e92be..52f0f8cf6 100644 --- a/app/portainer/services/api/templateService.js +++ b/app/portainer/services/api/templateService.js @@ -1,3 +1,4 @@ +import { commandStringToArray } from '@/docker/helpers/containers'; import { DockerHubViewModel } from 'Portainer/models/dockerhub'; import { TemplateViewModel } from '../../models/template'; @@ -60,7 +61,7 @@ function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, Cont configuration.name = containerName; configuration.Hostname = template.Hostname; configuration.Env = TemplateHelper.EnvToStringArray(template.Env); - configuration.Cmd = ContainerHelper.commandStringToArray(template.Command); + configuration.Cmd = commandStringToArray(template.Command); var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports); configuration.HostConfig.PortBindings = portConfiguration.bindings; configuration.ExposedPorts = portConfiguration.exposedPorts; diff --git a/app/portainer/services/axios.ts b/app/portainer/services/axios.ts index 1f33df70b..0f4cbee36 100644 --- a/app/portainer/services/axios.ts +++ b/app/portainer/services/axios.ts @@ -65,7 +65,7 @@ export function parseAxiosError( let resultMsg = msg; if (isAxiosError(err)) { - const { error, details } = parseError(err as AxiosError); + const { error, details } = parseError(err); resultErr = error; if (msg && details) { resultMsg = `${msg}: ${details}`; diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 0195332fe..048cfb015 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -10,7 +10,7 @@ import { isEdgeEnvironment, isDockerAPIEnvironment } from '@/react/portainer/env import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; import { confirmDisassociate } from '@/react/portainer/environments/ItemView/ConfirmDisassociateModel'; import { buildConfirmButton } from '@@/modals/utils'; -import { getInfo } from '@/docker/services/system.service'; +import { getInfo } from '@/react/docker/proxy/queries/useInfo'; angular.module('portainer.app').controller('EndpointController', EndpointController); diff --git a/app/react/components/form-components/FormControl/FormControl.tsx b/app/react/components/form-components/FormControl/FormControl.tsx index 017f627e1..36e362b86 100644 --- a/app/react/components/form-components/FormControl/FormControl.tsx +++ b/app/react/components/form-components/FormControl/FormControl.tsx @@ -66,7 +66,7 @@ function sizeClassLabel(size?: Size) { case 'medium': return 'col-sm-4 col-lg-3'; case 'xsmall': - return 'col-sm-2'; + return 'col-sm-1'; case 'vertical': return ''; default: @@ -81,7 +81,7 @@ function sizeClassChildren(size?: Size) { case 'medium': return 'col-sm-8 col-lg-9'; case 'xsmall': - return 'col-sm-10'; + return 'col-sm-11'; case 'vertical': return ''; default: diff --git a/app/react/components/form-components/Input/Input.tsx b/app/react/components/form-components/Input/Input.tsx index c1e087625..2108b314a 100644 --- a/app/react/components/form-components/Input/Input.tsx +++ b/app/react/components/form-components/Input/Input.tsx @@ -13,7 +13,9 @@ export function Input({ className, mRef: ref, ...props -}: InputHTMLAttributes & { mRef?: Ref }) { +}: InputHTMLAttributes & { + mRef?: Ref; +}) { return ( + * + * + * + * + * + * ``` + */ export function InputGroupButtonWrapper({ children, }: PropsWithChildren) { useInputGroupContext(); - return ( - button]:!ml-0', - // the button should be rounded at the end (right) if it's the last child and start (left) if it's the first child - // if the button is in the middle of the group, it shouldn't be rounded - '[&:first-child>button]:!rounded-l-[5px] [&:last-child>button]:!rounded-r-[5px] [&>button]:!rounded-none' - )} - > - {children} - - ); + return {children}; } diff --git a/app/react/components/form-components/InputGroup/index.ts b/app/react/components/form-components/InputGroup/index.ts index f18ebf301..771fe1eb3 100644 --- a/app/react/components/form-components/InputGroup/index.ts +++ b/app/react/components/form-components/InputGroup/index.ts @@ -6,6 +6,19 @@ import { InputGroupButtonWrapper } from './InputGroupButtonWrapper'; interface InputGroupSubComponents { Addon: typeof InputGroupAddon; + /** + * Should wrap all buttons inside a InputGroup + * + * example: + * ``` + * + * + * + * + * + * + * ``` + */ ButtonWrapper: typeof InputGroupButtonWrapper; Input: typeof Input; className: string | undefined; diff --git a/app/react/docker/containers/CreateView/CommandsTab/CommandsTab.tsx b/app/react/docker/containers/CreateView/CommandsTab/CommandsTab.tsx new file mode 100644 index 000000000..d9cd18484 --- /dev/null +++ b/app/react/docker/containers/CreateView/CommandsTab/CommandsTab.tsx @@ -0,0 +1,105 @@ +import { FormikErrors } from 'formik'; +import { useState } from 'react'; + +import { FormControl } from '@@/form-components/FormControl'; +import { Input } from '@@/form-components/Input'; + +import { ConsoleSettings } from './ConsoleSettings'; +import { LoggerConfig } from './LoggerConfig'; +import { OverridableInput } from './OverridableInput'; +import { Values } from './types'; + +export function CommandsTab({ + apiVersion, + values, + onChange, + errors, +}: { + apiVersion: number; + values: Values; + onChange: (values: Values) => void; + errors?: FormikErrors; +}) { + const [controlledValues, setControlledValues] = useState(values); + + return ( +
    + + handleChange({ cmd })} + id="command-input" + placeholder="e.g. '-logtostderr' '--housekeeping_interval=5s' or /usr/bin/nginx -t -c /mynginx.conf" + /> + + + + handleChange({ entrypoint })} + id="entrypoint-input" + placeholder="e.g. /bin/sh -c" + /> + + +
    + + handleChange({ workingDir: e.target.value })} + placeholder="e.g. /myapp" + /> + + + handleChange({ user: e.target.value })} + placeholder="e.g. nginx" + /> + +
    + + handleChange({ console })} + /> + + + handleChange({ + logConfig, + }) + } + errors={errors?.logConfig} + /> +
    + ); + + function handleChange(newValues: Partial) { + onChange({ ...values, ...newValues }); + setControlledValues((values) => ({ ...values, ...newValues })); + } +} diff --git a/app/react/docker/containers/CreateView/CommandsTab/ConsoleSettings.tsx b/app/react/docker/containers/CreateView/CommandsTab/ConsoleSettings.tsx new file mode 100644 index 000000000..e182f7962 --- /dev/null +++ b/app/react/docker/containers/CreateView/CommandsTab/ConsoleSettings.tsx @@ -0,0 +1,95 @@ +import { ReactNode } from 'react'; +import { mixed } from 'yup'; +import { ContainerConfig } from 'docker-types/generated/1.41'; + +import { FormControl } from '@@/form-components/FormControl'; + +const consoleSettingTypes = ['tty', 'interactive', 'both', 'none'] as const; + +export type ConsoleSetting = (typeof consoleSettingTypes)[number]; + +export type ConsoleConfig = Pick; + +export function ConsoleSettings({ + value, + onChange, +}: { + value: ConsoleSetting; + onChange(value: ConsoleSetting): void; +}) { + return ( + + + Interactive & TTY (-i -t) + + } + selected={value} + /> + + Interactive (-i) + + } + selected={value} + /> + + TTY (-t) + + } + selected={value} + /> + None} + selected={value} + /> + + ); + + function handleChange(value: ConsoleSetting) { + onChange(value); + } +} + +function Item({ + value, + selected, + onChange, + label, +}: { + value: ConsoleSetting; + selected: ConsoleSetting; + onChange(value: ConsoleSetting): void; + label: ReactNode; +}) { + return ( + + ); +} + +export function validation() { + return mixed() + .oneOf([...consoleSettingTypes]) + .default('none'); +} diff --git a/app/react/docker/containers/CreateView/CommandsTab/LoggerConfig.tsx b/app/react/docker/containers/CreateView/CommandsTab/LoggerConfig.tsx new file mode 100644 index 000000000..042fcdc46 --- /dev/null +++ b/app/react/docker/containers/CreateView/CommandsTab/LoggerConfig.tsx @@ -0,0 +1,138 @@ +import { FormikErrors } from 'formik'; +import { array, object, SchemaOf, string } from 'yup'; +import _ from 'lodash'; + +import { useLoggingPlugins } from '@/react/docker/proxy/queries/useServicePlugins'; +import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; + +import { FormControl } from '@@/form-components/FormControl'; +import { FormSection } from '@@/form-components/FormSection'; +import { InputGroup } from '@@/form-components/InputGroup'; +import { InputList, ItemProps } from '@@/form-components/InputList'; +import { PortainerSelect } from '@@/form-components/PortainerSelect'; +import { TextTip } from '@@/Tip/TextTip'; +import { FormError } from '@@/form-components/FormError'; + +export interface LogConfig { + type: string; + options: Array<{ option: string; value: string }>; +} + +export function LoggerConfig({ + value, + onChange, + apiVersion, + errors, +}: { + value: LogConfig; + onChange: (value: LogConfig) => void; + apiVersion: number; + errors?: FormikErrors; +}) { + const envId = useEnvironmentId(); + + const pluginsQuery = useLoggingPlugins(envId, apiVersion < 1.25); + + if (!pluginsQuery.data) { + return null; + } + + const isDisabled = !value.type || value.type === 'none'; + + const pluginOptions = [ + { label: 'Default logging driver', value: '' }, + ...pluginsQuery.data.map((p) => ({ label: p, value: p })), + { label: 'none', value: 'none' }, + ]; + + return ( + + + onChange({ ...value, type: type || '' })} + options={pluginOptions} + /> + + + + Logging driver that will override the default docker daemon driver. + Select Default logging driver if you don't want to override it. + Supported logging drivers can be found + + in the Docker documentation + + . + + + handleChange({ options })} + value={value.options} + item={Item} + itemBuilder={() => ({ option: '', value: '' })} + disabled={isDisabled} + errors={errors?.options} + /> + + ); + + function handleChange(partial: Partial) { + onChange({ ...value, ...partial }); + } +} + +function Item({ + item: { option, value }, + onChange, + error, +}: ItemProps<{ option: string; value: string }>) { + return ( +
    +
    + + option + handleChange({ option: e.target.value })} + placeholder="e.g. FOO" + /> + + + value + handleChange({ value: e.target.value })} + placeholder="e.g bar" + /> + +
    + {error && {_.first(Object.values(error))}} +
    + ); + + function handleChange(partial: Partial<{ option: string; value: string }>) { + onChange({ option, value, ...partial }); + } +} + +export function validation(): SchemaOf { + return object({ + options: array().of( + object({ + option: string().required('Option is required'), + value: string().required('Value is required'), + }) + ), + type: string().default('none'), + }); +} diff --git a/app/react/docker/containers/CreateView/CommandsTab/OverridableInput.tsx b/app/react/docker/containers/CreateView/CommandsTab/OverridableInput.tsx new file mode 100644 index 000000000..672d50646 --- /dev/null +++ b/app/react/docker/containers/CreateView/CommandsTab/OverridableInput.tsx @@ -0,0 +1,48 @@ +import clsx from 'clsx'; + +import { Button } from '@@/buttons'; +import { InputGroup } from '@@/form-components/InputGroup'; + +export function OverridableInput({ + value, + onChange, + id, + placeholder, +}: { + value: string | null; + onChange: (value: string | null) => void; + id: string; + placeholder: string; +}) { + const override = value !== null; + + return ( + + + + + + onChange(e.target.value)} + id={id} + placeholder={placeholder} + /> + + ); +} diff --git a/app/react/docker/containers/CreateView/CommandsTab/index.ts b/app/react/docker/containers/CreateView/CommandsTab/index.ts new file mode 100644 index 000000000..b0fcb0f93 --- /dev/null +++ b/app/react/docker/containers/CreateView/CommandsTab/index.ts @@ -0,0 +1,14 @@ +import { validation } from './validation'; +import { toRequest } from './toRequest'; +import { toViewModel, getDefaultViewModel } from './toViewModel'; + +export { CommandsTab } from './CommandsTab'; +export { validation as commandsTabValidation } from './validation'; +export { type Values as CommandsTabValues } from './types'; + +export const commandsTabUtils = { + toRequest, + toViewModel, + validation, + getDefaultViewModel, +}; diff --git a/app/react/docker/containers/CreateView/CommandsTab/toRequest.ts b/app/react/docker/containers/CreateView/CommandsTab/toRequest.ts new file mode 100644 index 000000000..99b59d19e --- /dev/null +++ b/app/react/docker/containers/CreateView/CommandsTab/toRequest.ts @@ -0,0 +1,60 @@ +import { commandStringToArray } from '@/docker/helpers/containers'; + +import { CreateContainerRequest } from '../types'; + +import { Values } from './types'; +import { LogConfig } from './LoggerConfig'; +import { ConsoleConfig, ConsoleSetting } from './ConsoleSettings'; + +export function toRequest( + oldConfig: CreateContainerRequest, + values: Values +): CreateContainerRequest { + const config = { + ...oldConfig, + + HostConfig: { + ...oldConfig.HostConfig, + LogConfig: getLogConfig(values.logConfig), + }, + User: values.user, + WorkingDir: values.workingDir, + ...getConsoleConfig(values.console), + }; + + if (values.cmd) { + config.Cmd = commandStringToArray(values.cmd); + } + + if (values.entrypoint) { + config.Entrypoint = commandStringToArray(values.entrypoint); + } + + return config; + + function getLogConfig( + value: LogConfig + ): CreateContainerRequest['HostConfig']['LogConfig'] { + return { + Type: value.type, + Config: Object.fromEntries( + value.options.map(({ option, value }) => [option, value]) + ), + // docker types - requires union while it should allow also custom string for custom plugins + } as CreateContainerRequest['HostConfig']['LogConfig']; + } + + function getConsoleConfig(value: ConsoleSetting): ConsoleConfig { + switch (value) { + case 'both': + return { OpenStdin: true, Tty: true }; + case 'interactive': + return { OpenStdin: true, Tty: false }; + case 'tty': + return { OpenStdin: false, Tty: true }; + case 'none': + default: + return { OpenStdin: false, Tty: false }; + } + } +} diff --git a/app/react/docker/containers/CreateView/CommandsTab/toViewModel.tsx b/app/react/docker/containers/CreateView/CommandsTab/toViewModel.tsx new file mode 100644 index 000000000..34474fa66 --- /dev/null +++ b/app/react/docker/containers/CreateView/CommandsTab/toViewModel.tsx @@ -0,0 +1,70 @@ +import { HostConfig } from 'docker-types/generated/1.41'; + +import { commandArrayToString } from '@/docker/helpers/containers'; + +import { ContainerJSON } from '../../queries/container'; + +import { ConsoleConfig, ConsoleSetting } from './ConsoleSettings'; +import { LogConfig } from './LoggerConfig'; +import { Values } from './types'; + +export function getDefaultViewModel(): Values { + return { + cmd: null, + entrypoint: null, + user: '', + workingDir: '', + console: 'none', + logConfig: getLogConfig(), + }; +} + +export function toViewModel(config: ContainerJSON): Values { + if (!config.Config) { + return getDefaultViewModel(); + } + + return { + cmd: config.Config.Cmd ? commandArrayToString(config.Config.Cmd) : null, + entrypoint: config.Config.Entrypoint + ? commandArrayToString(config.Config.Entrypoint) + : null, + user: config.Config.User || '', + workingDir: config.Config.WorkingDir || '', + console: config ? getConsoleSetting(config.Config) : 'none', + logConfig: getLogConfig(config.HostConfig?.LogConfig), + }; +} + +function getLogConfig(value?: HostConfig['LogConfig']): LogConfig { + if (!value || !value.Type) { + return { + type: 'none', + options: [], + }; + } + + return { + type: value.Type, + options: Object.entries(value.Config || {}).map(([option, value]) => ({ + option, + value, + })), + }; +} + +function getConsoleSetting(value: ConsoleConfig): ConsoleSetting { + if (value.OpenStdin && value.Tty) { + return 'both'; + } + + if (!value.OpenStdin && value.Tty) { + return 'tty'; + } + + if (value.OpenStdin && !value.Tty) { + return 'interactive'; + } + + return 'none'; +} diff --git a/app/react/docker/containers/CreateView/CommandsTab/types.ts b/app/react/docker/containers/CreateView/CommandsTab/types.ts new file mode 100644 index 000000000..d08319042 --- /dev/null +++ b/app/react/docker/containers/CreateView/CommandsTab/types.ts @@ -0,0 +1,11 @@ +import { ConsoleSetting } from './ConsoleSettings'; +import { LogConfig } from './LoggerConfig'; + +export interface Values { + cmd: string | null; + entrypoint: string | null; + workingDir: string; + user: string; + console: ConsoleSetting; + logConfig: LogConfig; +} diff --git a/app/react/docker/containers/CreateView/CommandsTab/validation.ts b/app/react/docker/containers/CreateView/CommandsTab/validation.ts new file mode 100644 index 000000000..e693d87b9 --- /dev/null +++ b/app/react/docker/containers/CreateView/CommandsTab/validation.ts @@ -0,0 +1,16 @@ +import { object, SchemaOf, string } from 'yup'; + +import { validation as consoleValidation } from './ConsoleSettings'; +import { validation as logConfigValidation } from './LoggerConfig'; +import { Values } from './types'; + +export function validation(): SchemaOf { + return object({ + cmd: string().nullable().default(''), + entrypoint: string().nullable().default(''), + logConfig: logConfigValidation(), + console: consoleValidation(), + user: string().default(''), + workingDir: string().default(''), + }); +} diff --git a/app/react/docker/containers/CreateView/types.ts b/app/react/docker/containers/CreateView/types.ts new file mode 100644 index 000000000..14a8b3609 --- /dev/null +++ b/app/react/docker/containers/CreateView/types.ts @@ -0,0 +1,10 @@ +import { + ContainerConfig, + HostConfig, + NetworkingConfig, +} from 'docker-types/generated/1.41'; + +export interface CreateContainerRequest extends ContainerConfig { + HostConfig: HostConfig; + NetworkingConfig: NetworkingConfig; +} diff --git a/app/react/docker/containers/ListView/ListView.tsx b/app/react/docker/containers/ListView/ListView.tsx index fbfd75c8f..12dcf32a8 100644 --- a/app/react/docker/containers/ListView/ListView.tsx +++ b/app/react/docker/containers/ListView/ListView.tsx @@ -1,4 +1,4 @@ -import { useInfo } from '@/docker/services/system.service'; +import { useInfo } from '@/react/docker/proxy/queries/useInfo'; import { Environment } from '@/react/portainer/environments/types'; import { isAgentEnvironment } from '@/react/portainer/environments/utils'; diff --git a/app/react/docker/containers/queries/container.ts b/app/react/docker/containers/queries/container.ts new file mode 100644 index 000000000..c0ca5783f --- /dev/null +++ b/app/react/docker/containers/queries/container.ts @@ -0,0 +1,116 @@ +import { useQuery } from 'react-query'; +import { + ContainerConfig, + ContainerState, + GraphDriverData, + HostConfig, + MountPoint, + NetworkSettings, +} from 'docker-types/generated/1.41'; + +import { PortainerResponse } from '@/react/docker/types'; +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { ContainerId } from '@/react/docker/containers/types'; +import { EnvironmentId } from '@/react/portainer/environments/types'; +import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel'; + +import { urlBuilder } from '../containers.service'; + +import { queryKeys } from './query-keys'; + +export interface ContainerJSON { + /** + * The ID of the container + */ + Id?: string; + /** + * The time the container was created + */ + Created?: string; + /** + * The path to the command being run + */ + Path?: string; + /** + * The arguments to the command being run + */ + Args?: Array; + State?: ContainerState; + /** + * The container's image ID + */ + Image?: string; + ResolvConfPath?: string; + HostnamePath?: string; + HostsPath?: string; + LogPath?: string; + Name?: string; + RestartCount?: number; + Driver?: string; + Platform?: string; + MountLabel?: string; + ProcessLabel?: string; + AppArmorProfile?: string; + /** + * IDs of exec instances that are running in the container. + */ + ExecIDs?: Array | null; + HostConfig?: HostConfig; + GraphDriver?: GraphDriverData; + /** + * The size of files that have been created or changed by this + * container. + * + */ + SizeRw?: number; + /** + * The total size of all the files in this container. + */ + SizeRootFs?: number; + Mounts?: Array; + Config?: ContainerConfig; + NetworkSettings?: NetworkSettings; +} + +export function useContainer( + environmentId: EnvironmentId, + containerId: ContainerId +) { + return useQuery( + queryKeys.container(environmentId, containerId), + () => getContainer(environmentId, containerId), + { + meta: { + title: 'Failure', + message: 'Unable to retrieve container', + }, + } + ); +} + +export type ContainerResponse = PortainerResponse; + +async function getContainer( + environmentId: EnvironmentId, + containerId: ContainerId +) { + try { + const { data } = await axios.get( + urlBuilder(environmentId, containerId, 'json') + ); + return parseViewModel(data); + } catch (error) { + throw parseAxiosError(error as Error, 'Unable to retrieve container'); + } +} + +export function parseViewModel(response: ContainerResponse) { + const resourceControl = + response.Portainer?.ResourceControl && + new ResourceControlViewModel(response?.Portainer?.ResourceControl); + + return { + ...response, + ResourceControl: resourceControl, + }; +} diff --git a/app/react/docker/containers/queries/containers.ts b/app/react/docker/containers/queries/containers.ts index 8f1a301cd..2ae04163e 100644 --- a/app/react/docker/containers/queries/containers.ts +++ b/app/react/docker/containers/queries/containers.ts @@ -9,7 +9,7 @@ import { withGlobalError } from '@/react-tools/react-query'; import { urlBuilder } from '../containers.service'; import { DockerContainerResponse } from '../types/response'; -import { parseViewModel } from '../utils'; +import { parseListViewModel } from '../utils'; import { Filters } from './types'; import { queryKeys } from './query-keys'; @@ -58,7 +58,7 @@ async function getContainers( : undefined, } ); - return data.map((c) => parseViewModel(c)); + return data.map((c) => parseListViewModel(c)); } catch (error) { throw parseAxiosError(error as Error, 'Unable to retrieve containers'); } diff --git a/app/react/docker/containers/types/response.ts b/app/react/docker/containers/types/response.ts index be41ed517..67f9e53c9 100644 --- a/app/react/docker/containers/types/response.ts +++ b/app/react/docker/containers/types/response.ts @@ -1,75 +1,15 @@ +import { + EndpointSettings, + MountPoint, + Port, +} from 'docker-types/generated/1.41'; + import { PortainerMetadata } from '@/react/docker/types'; -interface EndpointIPAMConfig { - IPv4Address?: string; - IPv6Address?: string; - LinkLocalIPs?: string[]; -} - -interface EndpointSettings { - IPAMConfig?: EndpointIPAMConfig; - Links: string[]; - Aliases: string[]; - NetworkID: string; - EndpointID: string; - Gateway: string; - IPAddress: string; - IPPrefixLen: number; - IPv6Gateway: string; - GlobalIPv6Address: string; - GlobalIPv6PrefixLen: number; - MacAddress: string; - DriverOpts: { [key: string]: string }; -} - export interface SummaryNetworkSettings { Networks: { [key: string]: EndpointSettings | undefined }; } -interface PortResponse { - IP?: string; - PrivatePort: number; - PublicPort?: number; - Type: string; -} - -enum MountPropagation { - // PropagationRPrivate RPRIVATE - RPrivate = 'rprivate', - // PropagationPrivate PRIVATE - Private = 'private', - // PropagationRShared RSHARED - RShared = 'rshared', - // PropagationShared SHARED - Shared = 'shared', - // PropagationRSlave RSLAVE - RSlave = 'rslave', - // PropagationSlave SLAVE - Slave = 'slave', -} - -enum MountType { - // TypeBind is the type for mounting host dir - Bind = 'bind', - // TypeVolume is the type for remote storage volumes - Volume = 'volume', - // TypeTmpfs is the type for mounting tmpfs - Tmpfs = 'tmpfs', - // TypeNamedPipe is the type for mounting Windows named pipes - NamedPipe = 'npipe', -} - -interface MountPoint { - Type?: MountType; - Name?: string; - Source: string; - Destination: string; - Driver?: string; - Mode: string; - RW: boolean; - Propagation: MountPropagation; -} - export interface Health { Status: 'healthy' | 'unhealthy' | 'starting'; FailingStreak: number; @@ -83,7 +23,7 @@ export interface DockerContainerResponse { ImageID: string; Command: string; Created: number; - Ports: PortResponse[]; + Ports: Port[]; SizeRw?: number; SizeRootFs?: number; Labels: { [key: string]: string }; diff --git a/app/react/docker/containers/utils.ts b/app/react/docker/containers/utils.ts index 6bb0ee242..6550c50b0 100644 --- a/app/react/docker/containers/utils.ts +++ b/app/react/docker/containers/utils.ts @@ -2,13 +2,13 @@ import _ from 'lodash'; import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel'; import { EnvironmentId } from '@/react/portainer/environments/types'; -import { useInfo } from '@/docker/services/system.service'; +import { useInfo } from '@/react/docker/proxy/queries/useInfo'; import { useEnvironment } from '@/react/portainer/environments/queries'; import { DockerContainer, ContainerStatus } from './types'; import { DockerContainerResponse } from './types/response'; -export function parseViewModel( +export function parseListViewModel( response: DockerContainerResponse ): DockerContainer { const resourceControl = diff --git a/app/react/docker/proxy/queries/useInfo.ts b/app/react/docker/proxy/queries/useInfo.ts new file mode 100644 index 000000000..aac9076fc --- /dev/null +++ b/app/react/docker/proxy/queries/useInfo.ts @@ -0,0 +1,43 @@ +import { useQuery } from 'react-query'; +import { SystemInfo } 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 async function getInfo(environmentId: EnvironmentId) { + try { + const { data } = await axios.get( + buildUrl(environmentId, 'info') + ); + return data; + } catch (err) { + throw parseAxiosError(err as Error, 'Unable to retrieve version'); + } +} + +export function useInfo( + environmentId: EnvironmentId, + select?: (info: SystemInfo) => TSelect +) { + return useQuery( + ['environment', environmentId, 'docker', 'info'], + () => getInfo(environmentId), + { + select, + } + ); +} + +export function useIsStandAlone(environmentId: EnvironmentId) { + const query = useInfo(environmentId, (info) => !info.Swarm?.NodeID); + + return !!query.data; +} + +export function useIsSwarm(environmentId: EnvironmentId) { + const query = useInfo(environmentId, (info) => !!info.Swarm?.NodeID); + + return !!query.data; +} diff --git a/app/react/docker/proxy/queries/useServicePlugins.ts b/app/react/docker/proxy/queries/useServicePlugins.ts new file mode 100644 index 000000000..0117c0e83 --- /dev/null +++ b/app/react/docker/proxy/queries/useServicePlugins.ts @@ -0,0 +1,114 @@ +import { useQuery } from 'react-query'; +import { + Plugin, + PluginInterfaceType, + PluginsInfo, +} from 'docker-types/generated/1.41'; + +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { EnvironmentId } from '@/react/portainer/environments/types'; + +import { queryKeys } from '../../queries/utils/root'; + +import { buildUrl } from './build-url'; +import { useInfo } from './useInfo'; + +export async function getPlugins(environmentId: EnvironmentId) { + try { + const { data } = await axios.get>( + buildUrl(environmentId, 'plugins') + ); + return data; + } catch (e) { + throw parseAxiosError(e as Error, 'Unable to retrieve plugins'); + } +} + +function usePlugins( + environmentId: EnvironmentId, + { enabled }: { enabled?: boolean } = {} +) { + return useQuery( + queryKeys.plugins(environmentId), + () => getPlugins(environmentId), + { enabled } + ); +} + +export function useServicePlugins( + environmentId: EnvironmentId, + systemOnly: boolean, + pluginType: keyof PluginsInfo, + pluginVersion: string +) { + const systemPluginsQuery = useInfo(environmentId, (info) => info.Plugins); + const pluginsQuery = usePlugins(environmentId, { enabled: !systemOnly }); + + return { + data: aggregateData(), + isLoading: systemPluginsQuery.isLoading || pluginsQuery.isLoading, + }; + + function aggregateData() { + if (!systemPluginsQuery.data) { + return null; + } + + const systemPlugins = systemPluginsQuery.data[pluginType] || []; + + if (systemOnly) { + return systemPlugins; + } + + const plugins = + pluginsQuery.data + ?.filter( + (plugin) => + plugin.Enabled && + // docker has an error in their types, so we need to cast to unknown first + // see https://docs.docker.com/engine/api/v1.41/#tag/Plugin/operation/PluginList + plugin.Config.Interface.Types.includes( + pluginVersion as unknown as PluginInterfaceType + ) + ) + .map((plugin) => plugin.Name) || []; + + return [...systemPlugins, ...plugins]; + } +} + +export function useLoggingPlugins( + environmentId: EnvironmentId, + systemOnly: boolean +) { + return useServicePlugins( + environmentId, + systemOnly, + 'Log', + 'docker.logdriver/1.0' + ); +} + +export function useVolumePlugins( + environmentId: EnvironmentId, + systemOnly: boolean +) { + return useServicePlugins( + environmentId, + systemOnly, + 'Volume', + 'docker.volumedriver/1.0' + ); +} + +export function useNetworkPlugins( + environmentId: EnvironmentId, + systemOnly: boolean +) { + return useServicePlugins( + environmentId, + systemOnly, + 'Network', + 'docker.networkdriver/1.0' + ); +} diff --git a/app/react/docker/proxy/queries/useVersion.ts b/app/react/docker/proxy/queries/useVersion.ts new file mode 100644 index 000000000..83d98ad44 --- /dev/null +++ b/app/react/docker/proxy/queries/useVersion.ts @@ -0,0 +1,34 @@ +import { useQuery } from 'react-query'; + +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( + buildUrl(environmentId, 'version') + ); + return data; + } catch (err) { + throw parseAxiosError(err as Error, 'Unable to retrieve version'); + } +} + +export function useVersion( + environmentId: EnvironmentId, + select?: (info: VersionResponse) => TSelect +) { + return useQuery( + ['environment', environmentId, 'docker', 'version'], + () => getVersion(environmentId), + { + select, + } + ); +} diff --git a/app/react/docker/queries/utils/root.ts b/app/react/docker/queries/utils/root.ts new file mode 100644 index 000000000..89b80b8c7 --- /dev/null +++ b/app/react/docker/queries/utils/root.ts @@ -0,0 +1,19 @@ +import { EnvironmentId } from '@/react/portainer/environments/types'; + +export const queryKeys = { + root: (environmentId: EnvironmentId) => ['docker', environmentId] as const, + snapshot: (environmentId: EnvironmentId) => + [...queryKeys.root(environmentId), 'snapshot'] as const, + snapshotQuery: (environmentId: EnvironmentId) => + [...queryKeys.snapshot(environmentId)] as const, + plugins: (environmentId: EnvironmentId) => + [...queryKeys.root(environmentId), 'plugins'] as const, +}; + +export function buildDockerUrl(environmentId: EnvironmentId) { + return `/docker/${environmentId}`; +} + +export function buildDockerSnapshotUrl(environmentId: EnvironmentId) { + return `${buildDockerUrl(environmentId)}/snapshot`; +} diff --git a/app/react/docker/types.ts b/app/react/docker/types.ts index 93b384896..4ffd662a3 100644 --- a/app/react/docker/types.ts +++ b/app/react/docker/types.ts @@ -8,3 +8,7 @@ export interface PortainerMetadata { ResourceControl?: ResourceControlResponse; Agent?: AgentMetadata; } + +export type PortainerResponse = T & { + Portainer?: PortainerMetadata; +}; diff --git a/app/react/sidebar/DockerSidebar.tsx b/app/react/sidebar/DockerSidebar.tsx index daf166ec6..1ec43458c 100644 --- a/app/react/sidebar/DockerSidebar.tsx +++ b/app/react/sidebar/DockerSidebar.tsx @@ -16,7 +16,8 @@ import { type EnvironmentId, } from '@/react/portainer/environments/types'; import { Authorized, useUser, isEnvironmentAdmin } from '@/react/hooks/useUser'; -import { useInfo, useVersion } from '@/docker/services/system.service'; +import { useInfo } from '@/react/docker/proxy/queries/useInfo'; +import { useVersion } from '@/react/docker/proxy/queries/useVersion'; import { SidebarItem } from './SidebarItem'; import { DashboardLink } from './items/DashboardLink'; diff --git a/app/setup-tests/setup-handlers/docker.ts b/app/setup-tests/setup-handlers/docker.ts index 288a746ab..3c2c0d28c 100644 --- a/app/setup-tests/setup-handlers/docker.ts +++ b/app/setup-tests/setup-handlers/docker.ts @@ -1,14 +1,20 @@ import { DefaultBodyType, PathParams, rest } from 'msw'; +import { SystemInfo } from 'docker-types/generated/1.41'; -import { - InfoResponse, - VersionResponse, -} from '@/docker/services/system.service'; +import { VersionResponse } from '@/react/docker/proxy/queries/useVersion'; export const dockerHandlers = [ - rest.get( + rest.get( '/api/endpoints/:endpointId/docker/info', - (req, res, ctx) => res(ctx.json({})) + (req, res, ctx) => + res( + ctx.json({ + Plugins: { Authorization: [], Log: [], Network: [], Volume: [] }, + MemTotal: 0, + NCPU: 0, + Runtimes: { runc: { path: 'runc' } }, + }) + ) ), rest.get( '/api/endpoints/:endpointId/docker/version', diff --git a/package.json b/package.json index 0cea844b7..d149259bd 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "codemirror": "^6.0.1", "core-js": "^3.19.3", "date-fns": "^2.29.3", + "docker-types": "^1.42.2", "fast-json-patch": "^3.1.1", "file-saver": "^2.0.5", "filesize": "~3.3.0", @@ -115,7 +116,6 @@ "react-select": "^5.2.1", "sanitize-html": "^2.8.1", "spinkit": "^2.0.1", - "splitargs": "github:deviantony/splitargs#semver:~0.2.0", "strip-ansi": "^6.0.0", "tippy.js": "^6.3.7", "toastr": "^2.1.4", diff --git a/yarn.lock b/yarn.lock index 8090682fe..cf3224478 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20,7 +20,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@apidevtools/json-schema-ref-parser@^9.0.6": +"@apidevtools/json-schema-ref-parser@9.0.9", "@apidevtools/json-schema-ref-parser@^9.0.6": version "9.0.9" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" integrity sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w== @@ -6808,6 +6808,13 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +busboy@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -6880,7 +6887,7 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.2.0: +camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -8091,6 +8098,14 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +docker-types@^1.42.2: + version "1.42.2" + resolved "https://registry.yarnpkg.com/docker-types/-/docker-types-1.42.2.tgz#40a3626abf99030abe306966d51b3fdae9c77408" + integrity sha512-Il8PAGTZpgRu8vMg+MnRTAD/FdEsTN2LYEFLHhhmiAWdGYkJHxDHWYSeBIIQMR6pJ/biHaF9qsTnYsJHX3OPTw== + dependencies: + openapi-typescript "5.4.1" + openapi-typescript-codegen "^0.24.0" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -9386,7 +9401,7 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@11.1.1, fs-extra@^11.1.0: +fs-extra@11.1.1, fs-extra@^11.1.0, fs-extra@^11.1.1: version "11.1.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== @@ -9656,6 +9671,11 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" +globalyzer@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" + integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== + globby@^11.0.1: version "11.0.4" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" @@ -9702,6 +9722,11 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -11323,6 +11348,13 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-ref-parser@^9.0.9: + version "9.0.9" + resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#66ea538e7450b12af342fa3d5b8458bc1e1e013f" + integrity sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q== + dependencies: + "@apidevtools/json-schema-ref-parser" "9.0.9" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -11907,6 +11939,11 @@ mime@^2.0.3: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -12562,6 +12599,29 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openapi-typescript-codegen@^0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/openapi-typescript-codegen/-/openapi-typescript-codegen-0.24.0.tgz#b3e6ade5bae75cd47868e5e3e4dc3bcf899cadab" + integrity sha512-rSt8t1XbMWhv6Db7GUI24NNli7FU5kzHLxcE8BpzgGWRdWyWt9IB2YoLyPahxNrVA7yOaVgnXPkrcTDRMQtJYg== + dependencies: + camelcase "^6.3.0" + commander "^10.0.0" + fs-extra "^11.1.1" + handlebars "^4.7.7" + json-schema-ref-parser "^9.0.9" + +openapi-typescript@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/openapi-typescript/-/openapi-typescript-5.4.1.tgz#38b4b45244acc1361f3c444537833a9e9cb03bf6" + integrity sha512-AGB2QiZPz4rE7zIwV3dRHtoUC/CWHhUjuzGXvtmMQN2AFV8xCTLKcZUHLcdPQmt/83i22nRE7+TxXOXkK+gf4Q== + dependencies: + js-yaml "^4.1.0" + mime "^3.0.0" + prettier "^2.6.2" + tiny-glob "^0.2.9" + undici "^5.4.0" + yargs-parser "^21.0.1" + opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" @@ -13354,7 +13414,7 @@ prettier-plugin-tailwindcss@^0.5.3: resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.3.tgz#ed4b31ee75bbce1db4ac020a859267d5b65ad8df" integrity sha512-M5K80V21yM+CTm/FEFYRv9/9LyInYbCSXpIoPAKMm8zy89IOwdiA2e4JVbcO7tvRtAQWz32zdj7/WKcsmFyAVg== -prettier@^2.8.0: +prettier@^2.6.2, prettier@^2.8.0: version "2.8.8" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -14835,10 +14895,6 @@ spinkit@^2.0.1: resolved "https://registry.yarnpkg.com/spinkit/-/spinkit-2.0.1.tgz#aefcd0acfdf15a90aa8e1f069d7e618515891f74" integrity sha512-oYBGY0GV1H1dX+ZdKnB6JVsYC1w/Xl20H111eb+WSS8nUYmlHgGb4y5buFSkzzceEeYYh5kMhXoAmoTpiQauiA== -"splitargs@github:deviantony/splitargs#semver:~0.2.0": - version "0.0.7" - resolved "https://codeload.github.com/deviantony/splitargs/tar.gz/2a87a1dfb1f9698b94e28e3106ad34057841dbd1" - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -14907,6 +14963,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + strict-event-emitter@^0.2.4, strict-event-emitter@^0.2.6: version "0.2.8" resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz#b4e768927c67273c14c13d20e19d5e6c934b47ca" @@ -15408,6 +15469,14 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tiny-glob@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" + integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== + dependencies: + globalyzer "0.1.0" + globrex "^0.1.2" + tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -15703,6 +15772,13 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= +undici@^5.4.0: + version "5.22.1" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.22.1.tgz#877d512effef2ac8be65e695f3586922e1a57d7b" + integrity sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw== + dependencies: + busboy "^1.6.0" + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -16525,7 +16601,7 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.9: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.0, yargs-parser@^21.1.1: +yargs-parser@^21.0.0, yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==