mirror of
https://github.com/portainer/portainer.git
synced 2025-07-25 08:19:40 +02:00
refactor(docker/containers): migrate commands tab to react [EE-5208] (#10085)
This commit is contained in:
parent
46e73ee524
commit
f7366d9788
42 changed files with 1783 additions and 951 deletions
|
@ -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>
|
||||
);
|
||||
}
|
14
app/react/docker/containers/CreateView/CommandsTab/index.ts
Normal file
14
app/react/docker/containers/CreateView/CommandsTab/index.ts
Normal file
|
@ -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';
|
||||
}
|
11
app/react/docker/containers/CreateView/CommandsTab/types.ts
Normal file
11
app/react/docker/containers/CreateView/CommandsTab/types.ts
Normal file
|
@ -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(''),
|
||||
});
|
||||
}
|
10
app/react/docker/containers/CreateView/types.ts
Normal file
10
app/react/docker/containers/CreateView/types.ts
Normal file
|
@ -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 { isAgentEnvironment } from '@/react/portainer/environments/utils';
|
||||
|
||||
|
|
116
app/react/docker/containers/queries/container.ts
Normal file
116
app/react/docker/containers/queries/container.ts
Normal file
|
@ -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 { DockerContainerResponse } from '../types/response';
|
||||
import { parseViewModel } from '../utils';
|
||||
import { parseListViewModel } from '../utils';
|
||||
|
||||
import { Filters } from './types';
|
||||
import { queryKeys } from './query-keys';
|
||||
|
@ -58,7 +58,7 @@ async function getContainers(
|
|||
: undefined,
|
||||
}
|
||||
);
|
||||
return data.map((c) => parseViewModel(c));
|
||||
return data.map((c) => parseListViewModel(c));
|
||||
} catch (error) {
|
||||
throw parseAxiosError(error as Error, 'Unable to retrieve containers');
|
||||
}
|
||||
|
|
|
@ -1,75 +1,15 @@
|
|||
import {
|
||||
EndpointSettings,
|
||||
MountPoint,
|
||||
Port,
|
||||
} from 'docker-types/generated/1.41';
|
||||
|
||||
import { PortainerMetadata } from '@/react/docker/types';
|
||||
|
||||
interface EndpointIPAMConfig {
|
||||
IPv4Address?: string;
|
||||
IPv6Address?: string;
|
||||
LinkLocalIPs?: string[];
|
||||
}
|
||||
|
||||
interface EndpointSettings {
|
||||
IPAMConfig?: EndpointIPAMConfig;
|
||||
Links: string[];
|
||||
Aliases: string[];
|
||||
NetworkID: string;
|
||||
EndpointID: string;
|
||||
Gateway: string;
|
||||
IPAddress: string;
|
||||
IPPrefixLen: number;
|
||||
IPv6Gateway: string;
|
||||
GlobalIPv6Address: string;
|
||||
GlobalIPv6PrefixLen: number;
|
||||
MacAddress: string;
|
||||
DriverOpts: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface SummaryNetworkSettings {
|
||||
Networks: { [key: string]: EndpointSettings | undefined };
|
||||
}
|
||||
|
||||
interface PortResponse {
|
||||
IP?: string;
|
||||
PrivatePort: number;
|
||||
PublicPort?: number;
|
||||
Type: string;
|
||||
}
|
||||
|
||||
enum MountPropagation {
|
||||
// PropagationRPrivate RPRIVATE
|
||||
RPrivate = 'rprivate',
|
||||
// PropagationPrivate PRIVATE
|
||||
Private = 'private',
|
||||
// PropagationRShared RSHARED
|
||||
RShared = 'rshared',
|
||||
// PropagationShared SHARED
|
||||
Shared = 'shared',
|
||||
// PropagationRSlave RSLAVE
|
||||
RSlave = 'rslave',
|
||||
// PropagationSlave SLAVE
|
||||
Slave = 'slave',
|
||||
}
|
||||
|
||||
enum MountType {
|
||||
// TypeBind is the type for mounting host dir
|
||||
Bind = 'bind',
|
||||
// TypeVolume is the type for remote storage volumes
|
||||
Volume = 'volume',
|
||||
// TypeTmpfs is the type for mounting tmpfs
|
||||
Tmpfs = 'tmpfs',
|
||||
// TypeNamedPipe is the type for mounting Windows named pipes
|
||||
NamedPipe = 'npipe',
|
||||
}
|
||||
|
||||
interface MountPoint {
|
||||
Type?: MountType;
|
||||
Name?: string;
|
||||
Source: string;
|
||||
Destination: string;
|
||||
Driver?: string;
|
||||
Mode: string;
|
||||
RW: boolean;
|
||||
Propagation: MountPropagation;
|
||||
}
|
||||
|
||||
export interface Health {
|
||||
Status: 'healthy' | 'unhealthy' | 'starting';
|
||||
FailingStreak: number;
|
||||
|
@ -83,7 +23,7 @@ export interface DockerContainerResponse {
|
|||
ImageID: string;
|
||||
Command: string;
|
||||
Created: number;
|
||||
Ports: PortResponse[];
|
||||
Ports: Port[];
|
||||
SizeRw?: number;
|
||||
SizeRootFs?: number;
|
||||
Labels: { [key: string]: string };
|
||||
|
|
|
@ -2,13 +2,13 @@ import _ from 'lodash';
|
|||
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { useInfo } from '@/docker/services/system.service';
|
||||
import { useInfo } from '@/react/docker/proxy/queries/useInfo';
|
||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||
|
||||
import { DockerContainer, ContainerStatus } from './types';
|
||||
import { DockerContainerResponse } from './types/response';
|
||||
|
||||
export function parseViewModel(
|
||||
export function parseListViewModel(
|
||||
response: DockerContainerResponse
|
||||
): DockerContainer {
|
||||
const resourceControl =
|
||||
|
|
43
app/react/docker/proxy/queries/useInfo.ts
Normal file
43
app/react/docker/proxy/queries/useInfo.ts
Normal file
|
@ -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;
|
||||
}
|
114
app/react/docker/proxy/queries/useServicePlugins.ts
Normal file
114
app/react/docker/proxy/queries/useServicePlugins.ts
Normal file
|
@ -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'
|
||||
);
|
||||
}
|
34
app/react/docker/proxy/queries/useVersion.ts
Normal file
34
app/react/docker/proxy/queries/useVersion.ts
Normal file
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
19
app/react/docker/queries/utils/root.ts
Normal file
19
app/react/docker/queries/utils/root.ts
Normal file
|
@ -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;
|
||||
Agent?: AgentMetadata;
|
||||
}
|
||||
|
||||
export type PortainerResponse<T> = T & {
|
||||
Portainer?: PortainerMetadata;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue