mirror of https://github.com/portainer/portainer
refactor(docker/containers): migrate commands tab to react [EE-5208] (#10085)
parent
46e73ee524
commit
f7366d9788
|
@ -1,5 +1,4 @@
|
||||||
import _ from 'lodash-es';
|
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;
|
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';
|
'use strict';
|
||||||
var helper = {};
|
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) {
|
helper.configFromContainer = function (container) {
|
||||||
var config = container.Config;
|
var config = container.Config;
|
||||||
// HostConfig
|
// HostConfig
|
||||||
|
|
|
@ -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(' ');
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<ComponentProps<typeof CommandsTab>, CommandsTabValues>(
|
||||||
|
ngModule,
|
||||||
|
withUIRouter(withReactQuery(CommandsTab)),
|
||||||
|
'dockerCreateContainerCommandsTab',
|
||||||
|
['apiVersion'],
|
||||||
|
commandsTabValidation
|
||||||
|
);
|
|
@ -22,8 +22,10 @@ import { AgentHostBrowser } from '@/react/docker/host/BrowseView/AgentHostBrowse
|
||||||
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
|
import { AgentVolumeBrowser } from '@/react/docker/volumes/BrowseView/AgentVolumeBrowser';
|
||||||
import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable';
|
import { ProcessesDatatable } from '@/react/docker/containers/StatsView/ProcessesDatatable';
|
||||||
|
|
||||||
|
import { containersModule } from './containers';
|
||||||
|
|
||||||
const ngModule = angular
|
const ngModule = angular
|
||||||
.module('portainer.docker.react.components', [])
|
.module('portainer.docker.react.components', [containersModule])
|
||||||
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
|
.component('dockerfileDetails', r2a(DockerfileDetails, ['image']))
|
||||||
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
|
.component('dockerHealthStatus', r2a(HealthStatus, ['health']))
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
|
|
||||||
export const containersModule = angular
|
export const containersModule = angular
|
||||||
.module('portainer.docker.containers', [])
|
.module('portainer.docker.react.views.containers', [])
|
||||||
.component(
|
.component(
|
||||||
'containersView',
|
'containersView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), ['endpoint'])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(ListView))), ['endpoint'])
|
||||||
|
|
|
@ -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<VersionResponse>(
|
|
||||||
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<InfoResponse>(
|
|
||||||
buildUrl(environmentId, 'info')
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
throw parseAxiosError(err as Error, 'Unable to retrieve version');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useInfo<TSelect = InfoResponse>(
|
|
||||||
environmentId: EnvironmentId,
|
|
||||||
select?: (info: InfoResponse) => TSelect
|
|
||||||
) {
|
|
||||||
return useQuery(
|
|
||||||
['environment', environmentId, 'docker', 'info'],
|
|
||||||
() => getInfo(environmentId),
|
|
||||||
{
|
|
||||||
select,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export function useVersion<TSelect = VersionResponse>(
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||||
|
import { commandStringToArray } from '@/docker/helpers/containers';
|
||||||
|
|
||||||
angular.module('portainer.docker').controller('ContainerConsoleController', [
|
angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||||
'$scope',
|
'$scope',
|
||||||
|
@ -101,7 +102,7 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
Tty: true,
|
Tty: true,
|
||||||
User: $scope.formValues.user,
|
User: $scope.formValues.user,
|
||||||
Cmd: ContainerHelper.commandStringToArray(command),
|
Cmd: commandStringToArray(command),
|
||||||
};
|
};
|
||||||
|
|
||||||
ContainerService.createExec(execConfig)
|
ContainerService.createExec(execConfig)
|
||||||
|
|
|
@ -7,9 +7,10 @@ import { confirmDestructive } from '@@/modals/confirm';
|
||||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
import { buildConfirmButton } from '@@/modals/utils';
|
import { buildConfirmButton } from '@@/modals/utils';
|
||||||
|
|
||||||
import { ContainerCapabilities, ContainerCapability } from '../../../models/containerCapabilities';
|
import { commandsTabUtils } from '@/react/docker/containers/CreateView/CommandsTab';
|
||||||
import { AccessControlFormData } from '../../../../portainer/components/accessControlForm/porAccessControlFormModel';
|
import { ContainerCapabilities, ContainerCapability } from '@/docker/models/containerCapabilities';
|
||||||
import { ContainerDetailsViewModel } from '../../../models/container';
|
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||||
|
import { ContainerDetailsViewModel } from '@/docker/models/container';
|
||||||
|
|
||||||
import './createcontainer.css';
|
import './createcontainer.css';
|
||||||
|
|
||||||
|
@ -20,11 +21,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
'$state',
|
'$state',
|
||||||
'$timeout',
|
'$timeout',
|
||||||
'$transition$',
|
'$transition$',
|
||||||
'$filter',
|
|
||||||
'$analytics',
|
'$analytics',
|
||||||
'Container',
|
'Container',
|
||||||
'ContainerHelper',
|
'ContainerHelper',
|
||||||
'Image',
|
|
||||||
'ImageHelper',
|
'ImageHelper',
|
||||||
'Volume',
|
'Volume',
|
||||||
'NetworkService',
|
'NetworkService',
|
||||||
|
@ -37,7 +36,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
'RegistryService',
|
'RegistryService',
|
||||||
'SystemService',
|
'SystemService',
|
||||||
'SettingsService',
|
'SettingsService',
|
||||||
'PluginService',
|
|
||||||
'HttpRequestHelper',
|
'HttpRequestHelper',
|
||||||
'endpoint',
|
'endpoint',
|
||||||
function (
|
function (
|
||||||
|
@ -47,11 +45,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$state,
|
$state,
|
||||||
$timeout,
|
$timeout,
|
||||||
$transition$,
|
$transition$,
|
||||||
$filter,
|
|
||||||
$analytics,
|
$analytics,
|
||||||
Container,
|
Container,
|
||||||
ContainerHelper,
|
ContainerHelper,
|
||||||
Image,
|
|
||||||
ImageHelper,
|
ImageHelper,
|
||||||
Volume,
|
Volume,
|
||||||
NetworkService,
|
NetworkService,
|
||||||
|
@ -64,7 +60,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
RegistryService,
|
RegistryService,
|
||||||
SystemService,
|
SystemService,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
PluginService,
|
|
||||||
HttpRequestHelper,
|
HttpRequestHelper,
|
||||||
endpoint
|
endpoint
|
||||||
) {
|
) {
|
||||||
|
@ -80,7 +75,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
selectedGPUs: ['all'],
|
selectedGPUs: ['all'],
|
||||||
capabilities: ['compute', 'utility'],
|
capabilities: ['compute', 'utility'],
|
||||||
},
|
},
|
||||||
Console: 'none',
|
|
||||||
Volumes: [],
|
Volumes: [],
|
||||||
NetworkContainer: null,
|
NetworkContainer: null,
|
||||||
Labels: [],
|
Labels: [],
|
||||||
|
@ -95,15 +89,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
MemoryLimit: 0,
|
MemoryLimit: 0,
|
||||||
MemoryReservation: 0,
|
MemoryReservation: 0,
|
||||||
ShmSize: 64,
|
ShmSize: 64,
|
||||||
CmdMode: 'default',
|
|
||||||
EntrypointMode: 'default',
|
|
||||||
Env: [],
|
Env: [],
|
||||||
NodeName: null,
|
NodeName: null,
|
||||||
capabilities: [],
|
capabilities: [],
|
||||||
Sysctls: [],
|
Sysctls: [],
|
||||||
LogDriverName: '',
|
|
||||||
LogDriverOpts: [],
|
|
||||||
RegistryModel: new PorImageRegistryModel(),
|
RegistryModel: new PorImageRegistryModel(),
|
||||||
|
commands: commandsTabUtils.getDefaultViewModel(),
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.extraNetworks = {};
|
$scope.extraNetworks = {};
|
||||||
|
@ -114,6 +105,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
mode: '',
|
mode: '',
|
||||||
pullImageValidity: true,
|
pullImageValidity: true,
|
||||||
settingUnlimitedResources: false,
|
settingUnlimitedResources: false,
|
||||||
|
containerIsLoaded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.onAlwaysPullChange = onAlwaysPullChange;
|
$scope.onAlwaysPullChange = onAlwaysPullChange;
|
||||||
|
@ -121,6 +113,13 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$scope.handleAutoRemoveChange = handleAutoRemoveChange;
|
$scope.handleAutoRemoveChange = handleAutoRemoveChange;
|
||||||
$scope.handlePrivilegedChange = handlePrivilegedChange;
|
$scope.handlePrivilegedChange = handlePrivilegedChange;
|
||||||
$scope.handleInitChange = handleInitChange;
|
$scope.handleInitChange = handleInitChange;
|
||||||
|
$scope.handleCommandsChange = handleCommandsChange;
|
||||||
|
|
||||||
|
function handleCommandsChange(commands) {
|
||||||
|
return $scope.$evalAsync(() => {
|
||||||
|
$scope.formValues.commands = commands;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onAlwaysPullChange(checked) {
|
function onAlwaysPullChange(checked) {
|
||||||
return $scope.$evalAsync(() => {
|
return $scope.$evalAsync(() => {
|
||||||
|
@ -179,10 +178,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$scope.config = {
|
$scope.config = {
|
||||||
Image: '',
|
Image: '',
|
||||||
Env: [],
|
Env: [],
|
||||||
Cmd: '',
|
Cmd: null,
|
||||||
MacAddress: '',
|
MacAddress: '',
|
||||||
ExposedPorts: {},
|
ExposedPorts: {},
|
||||||
Entrypoint: '',
|
Entrypoint: null,
|
||||||
|
WorkingDir: '',
|
||||||
|
User: '',
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
RestartPolicy: {
|
RestartPolicy: {
|
||||||
Name: 'no',
|
Name: 'no',
|
||||||
|
@ -201,6 +202,10 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
CapAdd: [],
|
CapAdd: [],
|
||||||
CapDrop: [],
|
CapDrop: [],
|
||||||
Sysctls: {},
|
Sysctls: {},
|
||||||
|
LogConfig: {
|
||||||
|
Type: '',
|
||||||
|
Config: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
NetworkingConfig: {
|
NetworkingConfig: {
|
||||||
EndpointsConfig: {},
|
EndpointsConfig: {},
|
||||||
|
@ -262,14 +267,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$scope.formValues.Sysctls.splice(index, 1);
|
$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;
|
$scope.fromContainerMultipleNetworks = false;
|
||||||
|
|
||||||
function prepareImageConfig(config) {
|
function prepareImageConfig(config) {
|
||||||
|
@ -284,36 +281,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
config.HostConfig.PortBindings = bindings;
|
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) {
|
function prepareEnvironmentVariables(config) {
|
||||||
config.Env = envVarsUtils.convertToArrayOfStrings($scope.formValues.Env);
|
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) {
|
function prepareCapabilities(config) {
|
||||||
var allowed = $scope.formValues.capabilities.filter(function (item) {
|
var allowed = $scope.formValues.capabilities.filter(function (item) {
|
||||||
return item.allowed === true;
|
return item.allowed === true;
|
||||||
|
@ -511,40 +461,22 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
|
|
||||||
function prepareConfiguration() {
|
function prepareConfiguration() {
|
||||||
var config = angular.copy($scope.config);
|
var config = angular.copy($scope.config);
|
||||||
prepareCmd(config);
|
config = commandsTabUtils.toRequest(config, $scope.formValues.commands);
|
||||||
prepareEntrypoint(config);
|
|
||||||
prepareNetworkConfig(config);
|
prepareNetworkConfig(config);
|
||||||
prepareImageConfig(config);
|
prepareImageConfig(config);
|
||||||
preparePortBindings(config);
|
preparePortBindings(config);
|
||||||
prepareConsole(config);
|
|
||||||
prepareEnvironmentVariables(config);
|
prepareEnvironmentVariables(config);
|
||||||
prepareVolumes(config);
|
prepareVolumes(config);
|
||||||
prepareLabels(config);
|
prepareLabels(config);
|
||||||
prepareDevices(config);
|
prepareDevices(config);
|
||||||
prepareResources(config);
|
prepareResources(config);
|
||||||
prepareLogDriver(config);
|
|
||||||
prepareCapabilities(config);
|
prepareCapabilities(config);
|
||||||
prepareSysctls(config);
|
prepareSysctls(config);
|
||||||
prepareGPUOptions(config);
|
prepareGPUOptions(config);
|
||||||
return 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() {
|
function loadFromContainerPortBindings() {
|
||||||
const bindings = ContainerHelper.sortAndCombinePorts($scope.config.HostConfig.PortBindings);
|
const bindings = ContainerHelper.sortAndCombinePorts($scope.config.HostConfig.PortBindings);
|
||||||
$scope.config.HostConfig.PortBindings = bindings;
|
$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() {
|
function loadFromContainerDevices() {
|
||||||
var path = [];
|
var path = [];
|
||||||
for (var dev in $scope.config.HostConfig.Devices) {
|
for (var dev in $scope.config.HostConfig.Devices) {
|
||||||
|
@ -765,15 +685,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
$scope.fromContainer = fromContainer;
|
$scope.fromContainer = fromContainer;
|
||||||
$scope.state.mode = 'duplicate';
|
$scope.state.mode = 'duplicate';
|
||||||
$scope.config = ContainerHelper.configFromContainer(fromContainer.Model);
|
$scope.config = ContainerHelper.configFromContainer(fromContainer.Model);
|
||||||
loadFromContainerCmd(d);
|
|
||||||
loadFromContainerEntrypoint(d);
|
$scope.formValues.commands = commandsTabUtils.toViewModel(d);
|
||||||
loadFromContainerLogging(d);
|
|
||||||
loadFromContainerPortBindings(d);
|
loadFromContainerPortBindings(d);
|
||||||
loadFromContainerVolumes(d);
|
loadFromContainerVolumes(d);
|
||||||
loadFromContainerNetworkConfig(d);
|
loadFromContainerNetworkConfig(d);
|
||||||
loadFromContainerEnvironmentVariables(d);
|
loadFromContainerEnvironmentVariables(d);
|
||||||
loadFromContainerLabels(d);
|
loadFromContainerLabels(d);
|
||||||
loadFromContainerConsole(d);
|
|
||||||
loadFromContainerDevices(d);
|
loadFromContainerDevices(d);
|
||||||
loadFromContainerDeviceRequests(d);
|
loadFromContainerDeviceRequests(d);
|
||||||
loadFromContainerImageConfig(d);
|
loadFromContainerImageConfig(d);
|
||||||
|
@ -781,22 +700,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
loadFromContainerCapabilities(d);
|
loadFromContainerCapabilities(d);
|
||||||
loadFromContainerSysctls(d);
|
loadFromContainerSysctls(d);
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
$scope.state.containerIsLoaded = true;
|
||||||
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve container');
|
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() {
|
async function initView() {
|
||||||
var nodeName = $transition$.params().nodeName;
|
var nodeName = $transition$.params().nodeName;
|
||||||
$scope.formValues.NodeName = nodeName;
|
$scope.formValues.NodeName = nodeName;
|
||||||
|
@ -845,6 +756,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||||
if ($transition$.params().from) {
|
if ($transition$.params().from) {
|
||||||
loadFromContainerSpec();
|
loadFromContainerSpec();
|
||||||
} else {
|
} else {
|
||||||
|
$scope.state.containerIsLoaded = true;
|
||||||
$scope.fromContainer = {};
|
$scope.fromContainer = {};
|
||||||
$scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : [];
|
$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.allowBindMounts = $scope.isAdminOrEndpointAdmin || endpoint.SecuritySettings.allowBindMountsForRegularUsers;
|
||||||
$scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers;
|
$scope.allowPrivilegedMode = endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers;
|
||||||
|
|
||||||
PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) {
|
|
||||||
$scope.availableLoggingDrivers = loggingDrivers;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(accessControlData, isAdmin) {
|
function validateForm(accessControlData, isAdmin) {
|
||||||
|
|
|
@ -202,160 +202,16 @@
|
||||||
<li ng-if="areContainerCapabilitiesEnabled" class="interactive"><a data-target="#container-capabilities" data-toggle="tab">Capabilities</a></li>
|
<li ng-if="areContainerCapabilitiesEnabled" class="interactive"><a data-target="#container-capabilities" data-toggle="tab">Capabilities</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<!-- tab-content -->
|
<!-- tab-content -->
|
||||||
|
<div class="form-horizontal">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<!-- tab-command -->
|
<!-- tab-command -->
|
||||||
<div class="tab-pane active" id="command">
|
<div class="tab-pane active" id="command">
|
||||||
<form class="form-horizontal" style="margin-top: 15px">
|
<docker-create-container-commands-tab
|
||||||
<!-- command-input -->
|
ng-if="state.containerIsLoaded"
|
||||||
<div class="form-group">
|
values="formValues.commands"
|
||||||
<label for="container_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
|
api-version="applicationState.endpoint.apiVersion"
|
||||||
<div class="col-sm-9">
|
on-change="(handleCommandsChange)"
|
||||||
<div class="input-group">
|
></docker-create-container-commands-tab>
|
||||||
<div class="input-group-btn">
|
|
||||||
<label class="btn btn-light" ng-model="formValues.CmdMode" uib-btn-radio="'default'" style="margin-left: 0px"> Default</label>
|
|
||||||
<label class="btn btn-light" ng-model="formValues.CmdMode" uib-btn-radio="'override'">Override</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
ng-model="config.Cmd"
|
|
||||||
ng-disabled="formValues.CmdMode === 'default'"
|
|
||||||
id="container_command"
|
|
||||||
placeholder="e.g. '-logtostderr' '--housekeeping_interval=5s' or /usr/bin/nginx -t -c /mynginx.conf"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !command-input -->
|
|
||||||
<!-- entrypoint-input -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="container_entrypoint" class="col-sm-2 col-lg-1 control-label text-left">
|
|
||||||
Entrypoint
|
|
||||||
<portainer-tooltip
|
|
||||||
message="'When container entrypoint is entered as part of the Command field, set Entrypoint to Override mode and leave blank, else it will revert to default.'"
|
|
||||||
></portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-btn">
|
|
||||||
<label class="btn btn-light" ng-model="formValues.EntrypointMode" uib-btn-radio="'default'" style="margin-left: 0px"> Default</label>
|
|
||||||
<label class="btn btn-light" ng-model="formValues.EntrypointMode" uib-btn-radio="'override'">Override</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
ng-model="config.Entrypoint"
|
|
||||||
ng-disabled="formValues.EntrypointMode === 'default'"
|
|
||||||
id="container_entrypoint"
|
|
||||||
placeholder="e.g. /bin/sh -c"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !entrypoint-input -->
|
|
||||||
<!-- workdir-user-input -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="container_workingdir" class="col-sm-2 col-lg-1 control-label text-left">Working Dir</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input type="text" class="form-control" ng-model="config.WorkingDir" id="container_workingdir" placeholder="e.g. /myapp" />
|
|
||||||
</div>
|
|
||||||
<label for="container_user" class="col-sm-1 control-label text-left">User</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input type="text" class="form-control" ng-model="config.User" id="container_user" placeholder="e.g. nginx" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !workdir-user-input -->
|
|
||||||
<!-- console -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="container_console" class="col-sm-2 col-lg-1 control-label text-left">Console</label>
|
|
||||||
<div class="col-sm-10 col-lg-11">
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<label class="radio-inline">
|
|
||||||
<input type="radio" name="container_console" ng-model="formValues.Console" value="both" />
|
|
||||||
Interactive & TTY <span class="small text-muted">(-i -t)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<label class="radio-inline">
|
|
||||||
<input type="radio" name="container_console" ng-model="formValues.Console" value="interactive" />
|
|
||||||
Interactive <span class="small text-muted">(-i)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-offset-2 col-sm-10 col-lg-offset-1 col-lg-11">
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<label class="radio-inline">
|
|
||||||
<input type="radio" name="container_console" ng-model="formValues.Console" value="tty" />
|
|
||||||
TTY <span class="small text-muted">(-t)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<label class="radio-inline">
|
|
||||||
<input type="radio" name="container_console" ng-model="formValues.Console" value="none" />
|
|
||||||
None
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !console -->
|
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Logging </div>
|
|
||||||
<!-- logging-driver -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="log-driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<select class="form-control" ng-model="formValues.LogDriverName" id="log-driver">
|
|
||||||
<option selected value="">Default logging driver</option>
|
|
||||||
<option ng-repeat="driver in availableLoggingDrivers" ng-value="driver">{{ driver }}</option>
|
|
||||||
<option value="none">none</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-5">
|
|
||||||
<p class="small text-muted">
|
|
||||||
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 <a href="https://docs.docker.com/engine/admin/logging/overview/#supported-logging-drivers" target="_blank">in the Docker documentation</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !logging-driver -->
|
|
||||||
<!-- logging-opts -->
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12" style="margin-top: 5px">
|
|
||||||
<label class="control-label text-left">
|
|
||||||
Options
|
|
||||||
<portainer-tooltip
|
|
||||||
position="'top'"
|
|
||||||
message="'Add button is disabled unless a driver other than none or default is selected. Options are specific to the selected driver, refer to the driver documentation.'"
|
|
||||||
></portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<span
|
|
||||||
class="label label-default interactive"
|
|
||||||
style="margin-left: 10px"
|
|
||||||
ng-click="!formValues.LogDriverName || formValues.LogDriverName === 'none' || addLogDriverOpt(formValues.LogDriverName)"
|
|
||||||
>
|
|
||||||
<pr-icon icon="'plus'" mode="'alt'"></pr-icon> add logging driver option
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- logging-opts-input-list -->
|
|
||||||
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
|
||||||
<div ng-repeat="opt in formValues.LogDriverOpts" style="margin-top: 2px">
|
|
||||||
<div class="input-group col-sm-5 input-group-sm">
|
|
||||||
<span class="input-group-addon">option</span>
|
|
||||||
<input type="text" class="form-control" ng-model="opt.name" placeholder="e.g. FOO" />
|
|
||||||
</div>
|
|
||||||
<div class="input-group col-sm-5 input-group-sm">
|
|
||||||
<span class="input-group-addon">value</span>
|
|
||||||
<input type="text" class="form-control" ng-model="opt.value" placeholder="e.g. bar" />
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-light" type="button" ng-click="removeLogDriverOpt($index)">
|
|
||||||
<pr-icon icon="'trash-2'" class-name="'icon-secondary icon-md'"></pr-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- logging-opts-input-list -->
|
|
||||||
</div>
|
|
||||||
<!-- !logging-opts -->
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- !tab-command -->
|
<!-- !tab-command -->
|
||||||
<!-- tab-volume -->
|
<!-- tab-volume -->
|
||||||
|
@ -643,7 +499,9 @@
|
||||||
<div ng-if="showDeviceMapping" class="form-group">
|
<div ng-if="showDeviceMapping" class="form-group">
|
||||||
<div class="col-sm-12" style="margin-top: 5px">
|
<div class="col-sm-12" style="margin-top: 5px">
|
||||||
<label class="control-label text-left">Devices</label>
|
<label class="control-label text-left">Devices</label>
|
||||||
<span class="label label-default interactive" style="margin-left: 10px" ng-click="addDevice()"> <pr-icon icon="'plus'" mode="'alt'"></pr-icon> add device </span>
|
<span class="label label-default interactive" style="margin-left: 10px" ng-click="addDevice()">
|
||||||
|
<pr-icon icon="'plus'" mode="'alt'"></pr-icon> add device
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- devices-input-list -->
|
<!-- devices-input-list -->
|
||||||
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
||||||
|
@ -703,7 +561,6 @@
|
||||||
<!-- #region GPU -->
|
<!-- #region GPU -->
|
||||||
<div ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
<div ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||||
<div class="col-sm-12 form-section-title"> GPU </div>
|
<div class="col-sm-12 form-section-title"> GPU </div>
|
||||||
|
|
||||||
<gpu
|
<gpu
|
||||||
ng-if="applicationState.endpoint.apiVersion >= 1.4"
|
ng-if="applicationState.endpoint.apiVersion >= 1.4"
|
||||||
values="formValues.GPU"
|
values="formValues.GPU"
|
||||||
|
@ -715,7 +572,6 @@
|
||||||
>
|
>
|
||||||
</gpu>
|
</gpu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- #endregion GPU -->
|
<!-- #endregion GPU -->
|
||||||
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
|
<div ng-class="{ 'edit-resources': state.mode == 'duplicate' }">
|
||||||
<div class="col-sm-12 form-section-title"> Resources </div>
|
<div class="col-sm-12 form-section-title"> Resources </div>
|
||||||
|
@ -809,7 +665,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !cpu-limit-input -->
|
<!-- !cpu-limit-input -->
|
||||||
|
|
||||||
<!-- update-limit-btn -->
|
<!-- update-limit-btn -->
|
||||||
<div class="form-group" ng-if="state.mode == 'duplicate'">
|
<div class="form-group" ng-if="state.mode == 'duplicate'">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
@ -842,6 +697,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !tab-container-capabilities -->
|
<!-- !tab-container-capabilities -->
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { commandStringToArray } from '@/docker/helpers/containers';
|
||||||
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
|
import { DockerHubViewModel } from 'Portainer/models/dockerhub';
|
||||||
import { TemplateViewModel } from '../../models/template';
|
import { TemplateViewModel } from '../../models/template';
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, Cont
|
||||||
configuration.name = containerName;
|
configuration.name = containerName;
|
||||||
configuration.Hostname = template.Hostname;
|
configuration.Hostname = template.Hostname;
|
||||||
configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
|
configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
|
||||||
configuration.Cmd = ContainerHelper.commandStringToArray(template.Command);
|
configuration.Cmd = commandStringToArray(template.Command);
|
||||||
var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
|
var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
|
||||||
configuration.HostConfig.PortBindings = portConfiguration.bindings;
|
configuration.HostConfig.PortBindings = portConfiguration.bindings;
|
||||||
configuration.ExposedPorts = portConfiguration.exposedPorts;
|
configuration.ExposedPorts = portConfiguration.exposedPorts;
|
||||||
|
|
|
@ -65,7 +65,7 @@ export function parseAxiosError(
|
||||||
let resultMsg = msg;
|
let resultMsg = msg;
|
||||||
|
|
||||||
if (isAxiosError(err)) {
|
if (isAxiosError(err)) {
|
||||||
const { error, details } = parseError(err as AxiosError);
|
const { error, details } = parseError(err);
|
||||||
resultErr = error;
|
resultErr = error;
|
||||||
if (msg && details) {
|
if (msg && details) {
|
||||||
resultMsg = `${msg}: ${details}`;
|
resultMsg = `${msg}: ${details}`;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { isEdgeEnvironment, isDockerAPIEnvironment } from '@/react/portainer/env
|
||||||
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
||||||
import { confirmDisassociate } from '@/react/portainer/environments/ItemView/ConfirmDisassociateModel';
|
import { confirmDisassociate } from '@/react/portainer/environments/ItemView/ConfirmDisassociateModel';
|
||||||
import { buildConfirmButton } from '@@/modals/utils';
|
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);
|
angular.module('portainer.app').controller('EndpointController', EndpointController);
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ function sizeClassLabel(size?: Size) {
|
||||||
case 'medium':
|
case 'medium':
|
||||||
return 'col-sm-4 col-lg-3';
|
return 'col-sm-4 col-lg-3';
|
||||||
case 'xsmall':
|
case 'xsmall':
|
||||||
return 'col-sm-2';
|
return 'col-sm-1';
|
||||||
case 'vertical':
|
case 'vertical':
|
||||||
return '';
|
return '';
|
||||||
default:
|
default:
|
||||||
|
@ -81,7 +81,7 @@ function sizeClassChildren(size?: Size) {
|
||||||
case 'medium':
|
case 'medium':
|
||||||
return 'col-sm-8 col-lg-9';
|
return 'col-sm-8 col-lg-9';
|
||||||
case 'xsmall':
|
case 'xsmall':
|
||||||
return 'col-sm-10';
|
return 'col-sm-11';
|
||||||
case 'vertical':
|
case 'vertical':
|
||||||
return '';
|
return '';
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -13,7 +13,9 @@ export function Input({
|
||||||
className,
|
className,
|
||||||
mRef: ref,
|
mRef: ref,
|
||||||
...props
|
...props
|
||||||
}: InputHTMLAttributes<HTMLInputElement> & { mRef?: Ref<HTMLInputElement> }) {
|
}: InputHTMLAttributes<HTMLInputElement> & {
|
||||||
|
mRef?: Ref<HTMLInputElement>;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { useInputGroupContext } from './InputGroup';
|
import { useInputGroupContext } from './InputGroup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should wrap all buttons inside a InputGroup
|
||||||
|
*
|
||||||
|
* example:
|
||||||
|
* ```
|
||||||
|
* <InputGroup>
|
||||||
|
* <InputGroup.ButtonWrapper>
|
||||||
|
* <Button>...</Button>
|
||||||
|
* <Button>...</Button>
|
||||||
|
* </InputGroup.ButtonWrapper>
|
||||||
|
* </InputGroup>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
export function InputGroupButtonWrapper({
|
export function InputGroupButtonWrapper({
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<unknown>) {
|
}: PropsWithChildren<unknown>) {
|
||||||
useInputGroupContext();
|
useInputGroupContext();
|
||||||
|
|
||||||
return (
|
return <span className="input-group-btn">{children}</span>;
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
'input-group-btn [&>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}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,19 @@ import { InputGroupButtonWrapper } from './InputGroupButtonWrapper';
|
||||||
|
|
||||||
interface InputGroupSubComponents {
|
interface InputGroupSubComponents {
|
||||||
Addon: typeof InputGroupAddon;
|
Addon: typeof InputGroupAddon;
|
||||||
|
/**
|
||||||
|
* Should wrap all buttons inside a InputGroup
|
||||||
|
*
|
||||||
|
* example:
|
||||||
|
* ```
|
||||||
|
* <InputGroup>
|
||||||
|
* <InputGroup.ButtonWrapper>
|
||||||
|
* <Button>...</Button>
|
||||||
|
* <Button>...</Button>
|
||||||
|
* </InputGroup.ButtonWrapper>
|
||||||
|
* </InputGroup>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
ButtonWrapper: typeof InputGroupButtonWrapper;
|
ButtonWrapper: typeof InputGroupButtonWrapper;
|
||||||
Input: typeof Input;
|
Input: typeof Input;
|
||||||
className: string | undefined;
|
className: string | undefined;
|
||||||
|
|
|
@ -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<Values>;
|
||||||
|
}) {
|
||||||
|
const [controlledValues, setControlledValues] = useState(values);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-3">
|
||||||
|
<FormControl
|
||||||
|
label="Command"
|
||||||
|
inputId="command-input"
|
||||||
|
size="xsmall"
|
||||||
|
errors={errors?.cmd}
|
||||||
|
>
|
||||||
|
<OverridableInput
|
||||||
|
value={controlledValues.cmd}
|
||||||
|
onChange={(cmd) => handleChange({ cmd })}
|
||||||
|
id="command-input"
|
||||||
|
placeholder="e.g. '-logtostderr' '--housekeeping_interval=5s' or /usr/bin/nginx -t -c /mynginx.conf"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
label="Entrypoint"
|
||||||
|
inputId="entrypoint-input"
|
||||||
|
size="xsmall"
|
||||||
|
tooltip="When container entrypoint is entered as part of the Command field, set Entrypoint to Override mode and leave blank, else it will revert to default."
|
||||||
|
errors={errors?.entrypoint}
|
||||||
|
>
|
||||||
|
<OverridableInput
|
||||||
|
value={controlledValues.entrypoint}
|
||||||
|
onChange={(entrypoint) => handleChange({ entrypoint })}
|
||||||
|
id="entrypoint-input"
|
||||||
|
placeholder="e.g. /bin/sh -c"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="flex justify-between gap-4">
|
||||||
|
<FormControl
|
||||||
|
label="Working Dir"
|
||||||
|
inputId="working-dir-input"
|
||||||
|
className="w-1/2"
|
||||||
|
errors={errors?.workingDir}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={controlledValues.workingDir}
|
||||||
|
onChange={(e) => handleChange({ workingDir: e.target.value })}
|
||||||
|
placeholder="e.g. /myapp"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="User"
|
||||||
|
inputId="user-input"
|
||||||
|
className="w-1/2"
|
||||||
|
errors={errors?.user}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={controlledValues.user}
|
||||||
|
onChange={(e) => handleChange({ user: e.target.value })}
|
||||||
|
placeholder="e.g. nginx"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConsoleSettings
|
||||||
|
value={controlledValues.console}
|
||||||
|
onChange={(console) => handleChange({ console })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoggerConfig
|
||||||
|
apiVersion={apiVersion}
|
||||||
|
value={controlledValues.logConfig}
|
||||||
|
onChange={(logConfig) =>
|
||||||
|
handleChange({
|
||||||
|
logConfig,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
errors={errors?.logConfig}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleChange(newValues: Partial<Values>) {
|
||||||
|
onChange({ ...values, ...newValues });
|
||||||
|
setControlledValues((values) => ({ ...values, ...newValues }));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ContainerConfig, 'OpenStdin' | 'Tty'>;
|
||||||
|
|
||||||
|
export function ConsoleSettings({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: ConsoleSetting;
|
||||||
|
onChange(value: ConsoleSetting): void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FormControl label="Console" size="xsmall">
|
||||||
|
<Item
|
||||||
|
value="both"
|
||||||
|
onChange={handleChange}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
Interactive & TTY <span className="small text-muted">(-i -t)</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
selected={value}
|
||||||
|
/>
|
||||||
|
<Item
|
||||||
|
value="interactive"
|
||||||
|
onChange={handleChange}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
Interactive <span className="small text-muted">(-i)</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
selected={value}
|
||||||
|
/>
|
||||||
|
<Item
|
||||||
|
value="tty"
|
||||||
|
onChange={handleChange}
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
TTY <span className="small text-muted">(-t)</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
selected={value}
|
||||||
|
/>
|
||||||
|
<Item
|
||||||
|
value="none"
|
||||||
|
onChange={handleChange}
|
||||||
|
label={<>None</>}
|
||||||
|
selected={value}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleChange(value: ConsoleSetting) {
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Item({
|
||||||
|
value,
|
||||||
|
selected,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
value: ConsoleSetting;
|
||||||
|
selected: ConsoleSetting;
|
||||||
|
onChange(value: ConsoleSetting): void;
|
||||||
|
label: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="radio-inline !m-0 w-1/2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="container_console"
|
||||||
|
value={value}
|
||||||
|
checked={value === selected}
|
||||||
|
onChange={() => onChange(value)}
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validation() {
|
||||||
|
return mixed<ConsoleSetting>()
|
||||||
|
.oneOf([...consoleSettingTypes])
|
||||||
|
.default('none');
|
||||||
|
}
|
|
@ -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<LogConfig>;
|
||||||
|
}) {
|
||||||
|
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 (
|
||||||
|
<FormSection title="Logging">
|
||||||
|
<FormControl label="Driver">
|
||||||
|
<PortainerSelect
|
||||||
|
value={value.type}
|
||||||
|
onChange={(type) => onChange({ ...value, type: type || '' })}
|
||||||
|
options={pluginOptions}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextTip color="blue">
|
||||||
|
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
|
||||||
|
<a
|
||||||
|
href="https://docs.docker.com/engine/admin/logging/overview/#supported-logging-drivers"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
in the Docker documentation
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</TextTip>
|
||||||
|
|
||||||
|
<InputList
|
||||||
|
tooltip={
|
||||||
|
isDisabled
|
||||||
|
? 'Add button is disabled unless a driver other than none or default is selected. Options are specific to the selected driver, refer to the driver documentation.'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
label="Options"
|
||||||
|
onChange={(options) => handleChange({ options })}
|
||||||
|
value={value.options}
|
||||||
|
item={Item}
|
||||||
|
itemBuilder={() => ({ option: '', value: '' })}
|
||||||
|
disabled={isDisabled}
|
||||||
|
errors={errors?.options}
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleChange(partial: Partial<LogConfig>) {
|
||||||
|
onChange({ ...value, ...partial });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Item({
|
||||||
|
item: { option, value },
|
||||||
|
onChange,
|
||||||
|
error,
|
||||||
|
}: ItemProps<{ option: string; value: string }>) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex w-full gap-4">
|
||||||
|
<InputGroup className="w-1/2">
|
||||||
|
<InputGroup.Addon>option</InputGroup.Addon>
|
||||||
|
<InputGroup.Input
|
||||||
|
value={option}
|
||||||
|
onChange={(e) => handleChange({ option: e.target.value })}
|
||||||
|
placeholder="e.g. FOO"
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup className="w-1/2">
|
||||||
|
<InputGroup.Addon>value</InputGroup.Addon>
|
||||||
|
<InputGroup.Input
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => handleChange({ value: e.target.value })}
|
||||||
|
placeholder="e.g bar"
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
{error && <FormError>{_.first(Object.values(error))}</FormError>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleChange(partial: Partial<{ option: string; value: string }>) {
|
||||||
|
onChange({ option, value, ...partial });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validation(): SchemaOf<LogConfig> {
|
||||||
|
return object({
|
||||||
|
options: array().of(
|
||||||
|
object({
|
||||||
|
option: string().required('Option is required'),
|
||||||
|
value: string().required('Value is required'),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
type: string().default('none'),
|
||||||
|
});
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.ButtonWrapper>
|
||||||
|
<Button
|
||||||
|
color="light"
|
||||||
|
size="medium"
|
||||||
|
className={clsx('!ml-0', { active: !override })}
|
||||||
|
onClick={() => onChange(null)}
|
||||||
|
>
|
||||||
|
Default
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="light"
|
||||||
|
size="medium"
|
||||||
|
className={clsx({ active: override })}
|
||||||
|
onClick={() => onChange('')}
|
||||||
|
>
|
||||||
|
Override
|
||||||
|
</Button>
|
||||||
|
</InputGroup.ButtonWrapper>
|
||||||
|
<InputGroup.Input
|
||||||
|
disabled={!override}
|
||||||
|
value={value || ''}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
id={id}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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,
|
||||||
|
};
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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';
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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<Values> {
|
||||||
|
return object({
|
||||||
|
cmd: string().nullable().default(''),
|
||||||
|
entrypoint: string().nullable().default(''),
|
||||||
|
logConfig: logConfigValidation(),
|
||||||
|
console: consoleValidation(),
|
||||||
|
user: string().default(''),
|
||||||
|
workingDir: string().default(''),
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import {
|
||||||
|
ContainerConfig,
|
||||||
|
HostConfig,
|
||||||
|
NetworkingConfig,
|
||||||
|
} from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
|
export interface CreateContainerRequest extends ContainerConfig {
|
||||||
|
HostConfig: HostConfig;
|
||||||
|
NetworkingConfig: NetworkingConfig;
|
||||||
|
}
|
|
@ -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 { Environment } from '@/react/portainer/environments/types';
|
||||||
import { isAgentEnvironment } from '@/react/portainer/environments/utils';
|
import { isAgentEnvironment } from '@/react/portainer/environments/utils';
|
||||||
|
|
||||||
|
|
|
@ -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<string>;
|
||||||
|
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<string> | 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<MountPoint>;
|
||||||
|
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<ContainerJSON>;
|
||||||
|
|
||||||
|
async function getContainer(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
containerId: ContainerId
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<ContainerResponse>(
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import { withGlobalError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
import { urlBuilder } from '../containers.service';
|
import { urlBuilder } from '../containers.service';
|
||||||
import { DockerContainerResponse } from '../types/response';
|
import { DockerContainerResponse } from '../types/response';
|
||||||
import { parseViewModel } from '../utils';
|
import { parseListViewModel } from '../utils';
|
||||||
|
|
||||||
import { Filters } from './types';
|
import { Filters } from './types';
|
||||||
import { queryKeys } from './query-keys';
|
import { queryKeys } from './query-keys';
|
||||||
|
@ -58,7 +58,7 @@ async function getContainers(
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data.map((c) => parseViewModel(c));
|
return data.map((c) => parseListViewModel(c));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw parseAxiosError(error as Error, 'Unable to retrieve containers');
|
throw parseAxiosError(error as Error, 'Unable to retrieve containers');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +1,15 @@
|
||||||
|
import {
|
||||||
|
EndpointSettings,
|
||||||
|
MountPoint,
|
||||||
|
Port,
|
||||||
|
} from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
import { PortainerMetadata } from '@/react/docker/types';
|
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 {
|
export interface SummaryNetworkSettings {
|
||||||
Networks: { [key: string]: EndpointSettings | undefined };
|
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 {
|
export interface Health {
|
||||||
Status: 'healthy' | 'unhealthy' | 'starting';
|
Status: 'healthy' | 'unhealthy' | 'starting';
|
||||||
FailingStreak: number;
|
FailingStreak: number;
|
||||||
|
@ -83,7 +23,7 @@ export interface DockerContainerResponse {
|
||||||
ImageID: string;
|
ImageID: string;
|
||||||
Command: string;
|
Command: string;
|
||||||
Created: number;
|
Created: number;
|
||||||
Ports: PortResponse[];
|
Ports: Port[];
|
||||||
SizeRw?: number;
|
SizeRw?: number;
|
||||||
SizeRootFs?: number;
|
SizeRootFs?: number;
|
||||||
Labels: { [key: string]: string };
|
Labels: { [key: string]: string };
|
||||||
|
|
|
@ -2,13 +2,13 @@ import _ from 'lodash';
|
||||||
|
|
||||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
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 { useEnvironment } from '@/react/portainer/environments/queries';
|
||||||
|
|
||||||
import { DockerContainer, ContainerStatus } from './types';
|
import { DockerContainer, ContainerStatus } from './types';
|
||||||
import { DockerContainerResponse } from './types/response';
|
import { DockerContainerResponse } from './types/response';
|
||||||
|
|
||||||
export function parseViewModel(
|
export function parseListViewModel(
|
||||||
response: DockerContainerResponse
|
response: DockerContainerResponse
|
||||||
): DockerContainer {
|
): DockerContainer {
|
||||||
const resourceControl =
|
const resourceControl =
|
||||||
|
|
|
@ -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<SystemInfo>(
|
||||||
|
buildUrl(environmentId, 'info')
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err as Error, 'Unable to retrieve version');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useInfo<TSelect = SystemInfo>(
|
||||||
|
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;
|
||||||
|
}
|
|
@ -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<Array<Plugin>>(
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
}
|
|
@ -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<VersionResponse>(
|
||||||
|
buildUrl(environmentId, 'version')
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err as Error, 'Unable to retrieve version');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVersion<TSelect = VersionResponse>(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
select?: (info: VersionResponse) => TSelect
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
['environment', environmentId, 'docker', 'version'],
|
||||||
|
() => getVersion(environmentId),
|
||||||
|
{
|
||||||
|
select,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -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`;
|
||||||
|
}
|
|
@ -8,3 +8,7 @@ export interface PortainerMetadata {
|
||||||
ResourceControl?: ResourceControlResponse;
|
ResourceControl?: ResourceControlResponse;
|
||||||
Agent?: AgentMetadata;
|
Agent?: AgentMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PortainerResponse<T> = T & {
|
||||||
|
Portainer?: PortainerMetadata;
|
||||||
|
};
|
||||||
|
|
|
@ -16,7 +16,8 @@ import {
|
||||||
type EnvironmentId,
|
type EnvironmentId,
|
||||||
} from '@/react/portainer/environments/types';
|
} from '@/react/portainer/environments/types';
|
||||||
import { Authorized, useUser, isEnvironmentAdmin } from '@/react/hooks/useUser';
|
import { Authorized, useUser, isEnvironmentAdmin } from '@/react/hooks/useUser';
|
||||||
import { useInfo, 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 { SidebarItem } from './SidebarItem';
|
||||||
import { DashboardLink } from './items/DashboardLink';
|
import { DashboardLink } from './items/DashboardLink';
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import { DefaultBodyType, PathParams, rest } from 'msw';
|
import { DefaultBodyType, PathParams, rest } from 'msw';
|
||||||
|
import { SystemInfo } from 'docker-types/generated/1.41';
|
||||||
|
|
||||||
import {
|
import { VersionResponse } from '@/react/docker/proxy/queries/useVersion';
|
||||||
InfoResponse,
|
|
||||||
VersionResponse,
|
|
||||||
} from '@/docker/services/system.service';
|
|
||||||
|
|
||||||
export const dockerHandlers = [
|
export const dockerHandlers = [
|
||||||
rest.get<DefaultBodyType, PathParams, InfoResponse>(
|
rest.get<DefaultBodyType, PathParams, SystemInfo>(
|
||||||
'/api/endpoints/:endpointId/docker/info',
|
'/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<DefaultBodyType, PathParams, VersionResponse>(
|
rest.get<DefaultBodyType, PathParams, VersionResponse>(
|
||||||
'/api/endpoints/:endpointId/docker/version',
|
'/api/endpoints/:endpointId/docker/version',
|
||||||
|
|
|
@ -85,6 +85,7 @@
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"core-js": "^3.19.3",
|
"core-js": "^3.19.3",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"docker-types": "^1.42.2",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "~3.3.0",
|
"filesize": "~3.3.0",
|
||||||
|
@ -115,7 +116,6 @@
|
||||||
"react-select": "^5.2.1",
|
"react-select": "^5.2.1",
|
||||||
"sanitize-html": "^2.8.1",
|
"sanitize-html": "^2.8.1",
|
||||||
"spinkit": "^2.0.1",
|
"spinkit": "^2.0.1",
|
||||||
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
|
|
||||||
"strip-ansi": "^6.0.0",
|
"strip-ansi": "^6.0.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"toastr": "^2.1.4",
|
"toastr": "^2.1.4",
|
||||||
|
|
94
yarn.lock
94
yarn.lock
|
@ -20,7 +20,7 @@
|
||||||
"@jridgewell/gen-mapping" "^0.3.0"
|
"@jridgewell/gen-mapping" "^0.3.0"
|
||||||
"@jridgewell/trace-mapping" "^0.3.9"
|
"@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"
|
version "9.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b"
|
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==
|
integrity sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==
|
||||||
|
@ -6808,6 +6808,13 @@ buffer@^6.0.3:
|
||||||
base64-js "^1.3.1"
|
base64-js "^1.3.1"
|
||||||
ieee754 "^1.2.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:
|
bytes@3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
|
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"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||||
|
|
||||||
camelcase@^6.2.0:
|
camelcase@^6.2.0, camelcase@^6.3.0:
|
||||||
version "6.3.0"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||||
|
@ -8091,6 +8098,14 @@ dns-packet@^5.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@leichtgewicht/ip-codec" "^2.0.1"
|
"@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:
|
doctrine@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
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"
|
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
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"
|
version "11.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
|
||||||
integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
|
integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
|
||||||
|
@ -9656,6 +9671,11 @@ globalthis@^1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-properties "^1.1.3"
|
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:
|
globby@^11.0.1:
|
||||||
version "11.0.4"
|
version "11.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
|
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
|
||||||
|
@ -9702,6 +9722,11 @@ globby@^6.1.0:
|
||||||
pify "^2.0.0"
|
pify "^2.0.0"
|
||||||
pinkie-promise "^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:
|
gopd@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
|
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"
|
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==
|
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:
|
json-schema-traverse@^0.4.1:
|
||||||
version "0.4.1"
|
version "0.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
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"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
|
||||||
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
|
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:
|
mimic-fn@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
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-docker "^2.1.1"
|
||||||
is-wsl "^2.2.0"
|
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:
|
opener@^1.5.2:
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
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"
|
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.3.tgz#ed4b31ee75bbce1db4ac020a859267d5b65ad8df"
|
||||||
integrity sha512-M5K80V21yM+CTm/FEFYRv9/9LyInYbCSXpIoPAKMm8zy89IOwdiA2e4JVbcO7tvRtAQWz32zdj7/WKcsmFyAVg==
|
integrity sha512-M5K80V21yM+CTm/FEFYRv9/9LyInYbCSXpIoPAKMm8zy89IOwdiA2e4JVbcO7tvRtAQWz32zdj7/WKcsmFyAVg==
|
||||||
|
|
||||||
prettier@^2.8.0:
|
prettier@^2.6.2, prettier@^2.8.0:
|
||||||
version "2.8.8"
|
version "2.8.8"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
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"
|
resolved "https://registry.yarnpkg.com/spinkit/-/spinkit-2.0.1.tgz#aefcd0acfdf15a90aa8e1f069d7e618515891f74"
|
||||||
integrity sha512-oYBGY0GV1H1dX+ZdKnB6JVsYC1w/Xl20H111eb+WSS8nUYmlHgGb4y5buFSkzzceEeYYh5kMhXoAmoTpiQauiA==
|
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:
|
sprintf-js@~1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
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"
|
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
|
||||||
integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
|
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:
|
strict-event-emitter@^0.2.4, strict-event-emitter@^0.2.6:
|
||||||
version "0.2.8"
|
version "0.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz#b4e768927c67273c14c13d20e19d5e6c934b47ca"
|
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"
|
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
|
||||||
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
|
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:
|
tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
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"
|
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
|
||||||
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
|
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:
|
unfetch@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
|
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"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||||
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
|
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"
|
version "21.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||||
|
|
Loading…
Reference in New Issue