1
0
Fork 0
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:
Chaim Lev-Ari 2023-09-04 19:07:29 +01:00 committed by GitHub
parent 46e73ee524
commit f7366d9788
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1783 additions and 951 deletions

View file

@ -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 }));
}
}

View file

@ -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');
}

View file

@ -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&apos;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'),
});
}

View file

@ -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>
);
}

View 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,
};

View file

@ -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 };
}
}
}

View file

@ -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';
}

View 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;
}

View file

@ -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(''),
});
}

View file

@ -0,0 +1,10 @@
import {
ContainerConfig,
HostConfig,
NetworkingConfig,
} from 'docker-types/generated/1.41';
export interface CreateContainerRequest extends ContainerConfig {
HostConfig: HostConfig;
NetworkingConfig: NetworkingConfig;
}

View file

@ -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';

View 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,
};
}

View file

@ -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');
}

View file

@ -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 };

View file

@ -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 =

View 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;
}

View 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'
);
}

View 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,
}
);
}

View 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`;
}

View file

@ -8,3 +8,7 @@ export interface PortainerMetadata {
ResourceControl?: ResourceControlResponse;
Agent?: AgentMetadata;
}
export type PortainerResponse<T> = T & {
Portainer?: PortainerMetadata;
};