feat(podman): support add podman envs in the wizard [r8s-20] (#12056)
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 3.0 KiB |
@ -0,0 +1,302 @@
|
|||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
createColumnHelper,
|
||||||
|
createTable,
|
||||||
|
getCoreRowModel,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Datatable, defaultGlobalFilterFn, Props } from './Datatable';
|
||||||
|
import {
|
||||||
|
BasicTableSettings,
|
||||||
|
createPersistedStore,
|
||||||
|
refreshableSettings,
|
||||||
|
RefreshableTableSettings,
|
||||||
|
} from './types';
|
||||||
|
import { useTableState } from './useTableState';
|
||||||
|
|
||||||
|
// Mock data and dependencies
|
||||||
|
type MockData = { id: string; name: string; age: number };
|
||||||
|
const mockData = [
|
||||||
|
{ id: '1', name: 'John Doe', age: 30 },
|
||||||
|
{ id: '2', name: 'Jane Smith', age: 25 },
|
||||||
|
{ id: '3', name: 'Bob Johnson', age: 35 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockColumns = [
|
||||||
|
{ accessorKey: 'name', header: 'Name' },
|
||||||
|
{ accessorKey: 'age', header: 'Age' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// mock table settings / state
|
||||||
|
export interface TableSettings
|
||||||
|
extends BasicTableSettings,
|
||||||
|
RefreshableTableSettings {}
|
||||||
|
function createStore(storageKey: string) {
|
||||||
|
return createPersistedStore<TableSettings>(storageKey, 'name', (set) => ({
|
||||||
|
...refreshableSettings(set),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const storageKey = 'test-table';
|
||||||
|
const settingsStore = createStore(storageKey);
|
||||||
|
const mockSettingsManager = {
|
||||||
|
pageSize: 10,
|
||||||
|
search: '',
|
||||||
|
sortBy: undefined,
|
||||||
|
setSearch: vitest.fn(),
|
||||||
|
setSortBy: vitest.fn(),
|
||||||
|
setPageSize: vitest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function DatatableWithStore(props: Omit<Props<MockData>, 'settingsManager'>) {
|
||||||
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
|
return (
|
||||||
|
<Datatable {...props} settingsManager={tableState} data-cy="test-table" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Datatable', () => {
|
||||||
|
it('renders the table with correct data', () => {
|
||||||
|
render(
|
||||||
|
<DatatableWithStore
|
||||||
|
dataset={mockData}
|
||||||
|
columns={mockColumns}
|
||||||
|
data-cy="test-table"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Bob Johnson')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the table with a title', () => {
|
||||||
|
render(
|
||||||
|
<DatatableWithStore
|
||||||
|
dataset={mockData}
|
||||||
|
columns={mockColumns}
|
||||||
|
title="Test Table"
|
||||||
|
data-cy="test-table"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Table')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles row selection when not disabled', () => {
|
||||||
|
render(
|
||||||
|
<DatatableWithStore
|
||||||
|
dataset={mockData}
|
||||||
|
columns={mockColumns}
|
||||||
|
data-cy="test-table"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
|
fireEvent.click(checkboxes[1]); // Select the first row
|
||||||
|
|
||||||
|
// Check if the row is selected (you might need to adapt this based on your implementation)
|
||||||
|
expect(checkboxes[1]).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables row selection when disableSelect is true', () => {
|
||||||
|
render(
|
||||||
|
<DatatableWithStore
|
||||||
|
dataset={mockData}
|
||||||
|
columns={mockColumns}
|
||||||
|
disableSelect
|
||||||
|
data-cy="test-table"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkboxes = screen.queryAllByRole('checkbox');
|
||||||
|
expect(checkboxes.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles sorting', () => {
|
||||||
|
render(
|
||||||
|
<Datatable
|
||||||
|
dataset={mockData}
|
||||||
|
columns={mockColumns}
|
||||||
|
settingsManager={mockSettingsManager}
|
||||||
|
data-cy="test-table"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nameHeader = screen.getByText('Name');
|
||||||
|
fireEvent.click(nameHeader);
|
||||||
|
|
||||||
|
// Check if setSortBy was called with the correct arguments
|
||||||
|
expect(mockSettingsManager.setSortBy).toHaveBeenCalledWith('name', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders loading state', () => {
|
||||||
|
render(
|
||||||
|
<DatatableWithStore
|
||||||
|
dataset={mockData}
|
||||||
|
columns={mockColumns}
|
||||||
|
isLoading
|
||||||
|
data-cy="test-table"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders empty state', () => {
|
||||||
|
render(
|
||||||
|
<DatatableWithStore
|
||||||
|
dataset={[]}
|
||||||
|
columns={mockColumns}
|
||||||
|
emptyContentLabel="No data available"
|
||||||
|
data-cy="test-table"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('No data available')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test the defaultGlobalFilterFn used in searches
|
||||||
|
type Person = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
isEmployed: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
city?: string;
|
||||||
|
family?: { sister: string; uncles?: string[] };
|
||||||
|
};
|
||||||
|
const data: Person[] = [
|
||||||
|
{
|
||||||
|
// searching primitives should be supported
|
||||||
|
id: '1',
|
||||||
|
name: 'Alice',
|
||||||
|
age: 30,
|
||||||
|
isEmployed: true,
|
||||||
|
// supporting arrays of primitives should be supported
|
||||||
|
tags: ['music', 'likes-pixar'],
|
||||||
|
// supporting objects of primitives should be supported (values only).
|
||||||
|
// but shouldn't be support nested objects / arrays
|
||||||
|
family: { sister: 'sophie', uncles: ['john', 'david'] },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const columnHelper = createColumnHelper<Person>();
|
||||||
|
const columns = [
|
||||||
|
columnHelper.accessor('name', {
|
||||||
|
id: 'name',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('isEmployed', {
|
||||||
|
id: 'isEmployed',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('age', {
|
||||||
|
id: 'age',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('tags', {
|
||||||
|
id: 'tags',
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('family', {
|
||||||
|
id: 'family',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const mockTable = createTable({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
state: {},
|
||||||
|
onStateChange() {},
|
||||||
|
renderFallbackValue: undefined,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
});
|
||||||
|
const mockRow = mockTable.getRow('1');
|
||||||
|
|
||||||
|
describe('defaultGlobalFilterFn', () => {
|
||||||
|
it('should return true when filterValue is null', () => {
|
||||||
|
const result = defaultGlobalFilterFn(mockRow, 'Name', null);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when filterValue.search is empty', () => {
|
||||||
|
const result = defaultGlobalFilterFn(mockRow, 'Name', {
|
||||||
|
search: '',
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter string values correctly', () => {
|
||||||
|
expect(
|
||||||
|
defaultGlobalFilterFn(mockRow, 'name', {
|
||||||
|
search: 'hello',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
defaultGlobalFilterFn(mockRow, 'name', {
|
||||||
|
search: 'ALICE',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
defaultGlobalFilterFn(mockRow, 'name', {
|
||||||
|
search: 'Alice',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter number values correctly', () => {
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'age', { search: '123' })).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'age', { search: '30' })).toBe(true);
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'age', { search: '67' })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter boolean values correctly', () => {
|
||||||
|
expect(
|
||||||
|
defaultGlobalFilterFn(mockRow, 'isEmployed', { search: 'true' })
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
defaultGlobalFilterFn(mockRow, 'isEmployed', { search: 'false' })
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter object values correctly', () => {
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'sophie' })).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'family', { search: '30' })).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter array values correctly', () => {
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'tags', { search: 'music' })).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
defaultGlobalFilterFn(mockRow, 'tags', { search: 'Likes-Pixar' })
|
||||||
|
).toBe(true);
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'tags', { search: 'grape' })).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'tags', { search: 'likes' })).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex nested structures', () => {
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'sophie' })).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'mason' })).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not filter non-primitive values within objects and arrays', () => {
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'john' })).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(defaultGlobalFilterFn(mockRow, 'family', { search: 'david' })).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,147 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
import { DockerNetwork } from '@/react/docker/networks/types';
|
||||||
|
|
||||||
|
import { ContainerListViewModel } from '../../types';
|
||||||
|
import { ContainerDetailsJSON } from '../../queries/useContainer';
|
||||||
|
|
||||||
|
import { getDefaultViewModel, getNetworkMode } from './toViewModel';
|
||||||
|
|
||||||
|
describe('getDefaultViewModel', () => {
|
||||||
|
it('should return the correct default view model for Windows', () => {
|
||||||
|
const result = getDefaultViewModel(true);
|
||||||
|
expect(result).toEqual({
|
||||||
|
networkMode: 'nat',
|
||||||
|
hostname: '',
|
||||||
|
domain: '',
|
||||||
|
macAddress: '',
|
||||||
|
ipv4Address: '',
|
||||||
|
ipv6Address: '',
|
||||||
|
primaryDns: '',
|
||||||
|
secondaryDns: '',
|
||||||
|
hostsFileEntries: [],
|
||||||
|
container: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct default view model for Podman', () => {
|
||||||
|
const result = getDefaultViewModel(false, true);
|
||||||
|
expect(result).toEqual({
|
||||||
|
networkMode: 'podman',
|
||||||
|
hostname: '',
|
||||||
|
domain: '',
|
||||||
|
macAddress: '',
|
||||||
|
ipv4Address: '',
|
||||||
|
ipv6Address: '',
|
||||||
|
primaryDns: '',
|
||||||
|
secondaryDns: '',
|
||||||
|
hostsFileEntries: [],
|
||||||
|
container: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct default view model for Linux Docker', () => {
|
||||||
|
const result = getDefaultViewModel(false);
|
||||||
|
expect(result).toEqual({
|
||||||
|
networkMode: 'bridge',
|
||||||
|
hostname: '',
|
||||||
|
domain: '',
|
||||||
|
macAddress: '',
|
||||||
|
ipv4Address: '',
|
||||||
|
ipv6Address: '',
|
||||||
|
primaryDns: '',
|
||||||
|
secondaryDns: '',
|
||||||
|
hostsFileEntries: [],
|
||||||
|
container: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getNetworkMode', () => {
|
||||||
|
const mockNetworks: Array<DockerNetwork> = [
|
||||||
|
{
|
||||||
|
Name: 'bridge',
|
||||||
|
Id: 'bridge-id',
|
||||||
|
Driver: 'bridge',
|
||||||
|
Scope: 'local',
|
||||||
|
Attachable: false,
|
||||||
|
Internal: false,
|
||||||
|
IPAM: { Config: [], Driver: '', Options: {} },
|
||||||
|
Options: {},
|
||||||
|
Containers: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'host',
|
||||||
|
Id: 'host-id',
|
||||||
|
Driver: 'host',
|
||||||
|
Scope: 'local',
|
||||||
|
Attachable: false,
|
||||||
|
Internal: false,
|
||||||
|
IPAM: { Config: [], Driver: '', Options: {} },
|
||||||
|
Options: {},
|
||||||
|
Containers: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: 'custom',
|
||||||
|
Id: 'custom-id',
|
||||||
|
Driver: 'bridge',
|
||||||
|
Scope: 'local',
|
||||||
|
Attachable: true,
|
||||||
|
Internal: false,
|
||||||
|
IPAM: { Config: [], Driver: '', Options: {} },
|
||||||
|
Options: {},
|
||||||
|
Containers: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockRunningContainers: Array<ContainerListViewModel> = [
|
||||||
|
{
|
||||||
|
Id: 'container-1',
|
||||||
|
Names: ['container-1-name'],
|
||||||
|
} as ContainerListViewModel, // gaslight the type to avoid over-specifying
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should return the network mode from HostConfig', () => {
|
||||||
|
const config: ContainerDetailsJSON = {
|
||||||
|
HostConfig: { NetworkMode: 'host' },
|
||||||
|
};
|
||||||
|
expect(getNetworkMode(config, mockNetworks)).toEqual(['host']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the network mode from NetworkSettings if HostConfig is empty', () => {
|
||||||
|
const config: ContainerDetailsJSON = {
|
||||||
|
NetworkSettings: { Networks: { custom: {} } },
|
||||||
|
};
|
||||||
|
expect(getNetworkMode(config, mockNetworks)).toEqual(['custom']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return container mode when NetworkMode starts with "container:"', () => {
|
||||||
|
const config: ContainerDetailsJSON = {
|
||||||
|
HostConfig: { NetworkMode: 'container:container-1' },
|
||||||
|
};
|
||||||
|
expect(getNetworkMode(config, mockNetworks, mockRunningContainers)).toEqual(
|
||||||
|
['container', 'container-1-name']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "podman" for bridge network when isPodman is true', () => {
|
||||||
|
const config: ContainerDetailsJSON = {
|
||||||
|
HostConfig: { NetworkMode: 'bridge' },
|
||||||
|
};
|
||||||
|
expect(getNetworkMode(config, mockNetworks, [], true)).toEqual(['podman']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return "bridge" for default network mode on Docker', () => {
|
||||||
|
const config: ContainerDetailsJSON = {
|
||||||
|
HostConfig: { NetworkMode: 'default' },
|
||||||
|
};
|
||||||
|
expect(getNetworkMode(config, mockNetworks)).toEqual(['bridge']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the first available network if no matching network is found', () => {
|
||||||
|
const config: ContainerDetailsJSON = {
|
||||||
|
HostConfig: { NetworkMode: 'non-existent' },
|
||||||
|
};
|
||||||
|
expect(getNetworkMode(config, mockNetworks)).toEqual(['bridge']);
|
||||||
|
});
|
||||||
|
});
|
@ -1,58 +1,83 @@
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { List } from 'lucide-react';
|
import { List } from 'lucide-react';
|
||||||
import { useMemo } from 'react';
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
import { Datatable } from '@@/datatables';
|
import { Datatable } from '@@/datatables';
|
||||||
import { createPersistedStore } from '@@/datatables/types';
|
import { createPersistedStore } from '@@/datatables/types';
|
||||||
import { useTableState } from '@@/datatables/useTableState';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
|
import { useContainerTop } from '../queries/useContainerTop';
|
||||||
|
import { ContainerProcesses } from '../queries/types';
|
||||||
|
|
||||||
const tableKey = 'container-processes';
|
const tableKey = 'container-processes';
|
||||||
const store = createPersistedStore(tableKey);
|
const store = createPersistedStore(tableKey);
|
||||||
|
|
||||||
export function ProcessesDatatable({
|
type ProcessRow = {
|
||||||
dataset,
|
id: number;
|
||||||
headers,
|
};
|
||||||
}: {
|
|
||||||
dataset?: Array<Array<string | number>>;
|
|
||||||
headers?: Array<string>;
|
|
||||||
}) {
|
|
||||||
const tableState = useTableState(store, tableKey);
|
|
||||||
const rows = useMemo(() => {
|
|
||||||
if (!dataset || !headers) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataset.map((row, index) => ({
|
type ProcessesDatatableProps = {
|
||||||
id: index,
|
rows: Array<ProcessRow>;
|
||||||
...Object.fromEntries(
|
columns: Array<ColumnDef<ProcessRow>>;
|
||||||
headers.map((header, index) => [header, row[index]])
|
};
|
||||||
),
|
|
||||||
}));
|
export function ProcessesDatatable() {
|
||||||
}, [dataset, headers]);
|
const {
|
||||||
|
params: { id: containerId },
|
||||||
const columns = useMemo(
|
} = useCurrentStateAndParams();
|
||||||
() =>
|
const environmentId = useEnvironmentId();
|
||||||
headers
|
const topQuery = useContainerTop(
|
||||||
? headers.map(
|
environmentId,
|
||||||
(header) =>
|
containerId,
|
||||||
({ header, accessorKey: header }) satisfies ColumnDef<{
|
(containerProcesses: ContainerProcesses) =>
|
||||||
[k: string]: string;
|
parseContainerProcesses(containerProcesses)
|
||||||
}>
|
|
||||||
)
|
|
||||||
: [],
|
|
||||||
[headers]
|
|
||||||
);
|
);
|
||||||
|
const tableState = useTableState(store, tableKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Datatable
|
<Datatable
|
||||||
title="Processes"
|
title="Processes"
|
||||||
titleIcon={List}
|
titleIcon={List}
|
||||||
dataset={rows}
|
dataset={topQuery.data?.rows ?? []}
|
||||||
columns={columns}
|
columns={topQuery.data?.columns ?? []}
|
||||||
settingsManager={tableState}
|
settingsManager={tableState}
|
||||||
disableSelect
|
disableSelect
|
||||||
isLoading={!dataset}
|
isLoading={topQuery.isLoading}
|
||||||
data-cy="docker-container-stats-processes-datatable"
|
data-cy="docker-container-stats-processes-datatable"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// transform the data from the API into the format expected by the datatable
|
||||||
|
function parseContainerProcesses(
|
||||||
|
containerProcesses: ContainerProcesses
|
||||||
|
): ProcessesDatatableProps {
|
||||||
|
const { Processes: processes, Titles: titles } = containerProcesses;
|
||||||
|
const rows = processes?.map((row, index) => {
|
||||||
|
// docker has the row data as an array of many strings
|
||||||
|
// podman has the row data as an array with a single string separated by one or many spaces
|
||||||
|
const processArray = row.length === 1 ? row[0].split(/\s+/) : row;
|
||||||
|
return {
|
||||||
|
id: index,
|
||||||
|
...Object.fromEntries(
|
||||||
|
titles.map((header, index) => [header, processArray[index]])
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = titles
|
||||||
|
? titles.map(
|
||||||
|
(header) =>
|
||||||
|
({ header, accessorKey: header }) satisfies ColumnDef<{
|
||||||
|
[k: string]: string;
|
||||||
|
}>
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
columns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
import { fullURIIntoRepoAndTag } from './utils';
|
||||||
|
|
||||||
|
describe('fullURIIntoRepoAndTag', () => {
|
||||||
|
it('splits registry/image-repo:tag correctly', () => {
|
||||||
|
const result = fullURIIntoRepoAndTag('registry.example.com/my-image:v1.0');
|
||||||
|
expect(result).toEqual({
|
||||||
|
repo: 'registry.example.com/my-image',
|
||||||
|
tag: 'v1.0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('splits image-repo:tag correctly', () => {
|
||||||
|
const result = fullURIIntoRepoAndTag('nginx:latest');
|
||||||
|
expect(result).toEqual({ repo: 'nginx', tag: 'latest' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('splits registry:port/image-repo:tag correctly', () => {
|
||||||
|
const result = fullURIIntoRepoAndTag(
|
||||||
|
'registry.example.com:5000/my-image:v2.1'
|
||||||
|
);
|
||||||
|
expect(result).toEqual({
|
||||||
|
repo: 'registry.example.com:5000/my-image',
|
||||||
|
tag: 'v2.1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty string input', () => {
|
||||||
|
const result = fullURIIntoRepoAndTag('');
|
||||||
|
expect(result).toEqual({ repo: '', tag: 'latest' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles input with multiple colons', () => {
|
||||||
|
const result = fullURIIntoRepoAndTag('registry:5000/namespace/image:v1.0');
|
||||||
|
expect(result).toEqual({
|
||||||
|
repo: 'registry:5000/namespace/image',
|
||||||
|
tag: 'v1.0',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles input with @ symbol (digest)', () => {
|
||||||
|
const result = fullURIIntoRepoAndTag(
|
||||||
|
'myregistry.azurecr.io/image@sha256:123456'
|
||||||
|
);
|
||||||
|
expect(result).toEqual({
|
||||||
|
repo: 'myregistry.azurecr.io/image@sha256',
|
||||||
|
tag: '123456',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,28 +1,41 @@
|
|||||||
import { CellContext } from '@tanstack/react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { environmentTypeIcon } from '@/portainer/filters/filters';
|
|
||||||
import {
|
import {
|
||||||
Environment,
|
getEnvironmentTypeIcon,
|
||||||
EnvironmentType,
|
getPlatformTypeName,
|
||||||
} from '@/react/portainer/environments/types';
|
} from '@/react/portainer/environments/utils';
|
||||||
import { getPlatformTypeName } from '@/react/portainer/environments/utils';
|
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
|
import { EnvironmentListItem } from '../types';
|
||||||
|
import { EnvironmentType, ContainerEngine } from '../../types';
|
||||||
|
|
||||||
import { columnHelper } from './helper';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
export const type = columnHelper.accessor('Type', {
|
type TypeCellContext = {
|
||||||
header: 'Type',
|
type: EnvironmentType;
|
||||||
cell: Cell,
|
containerEngine?: ContainerEngine;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export const type = columnHelper.accessor(
|
||||||
|
(rowItem): TypeCellContext => ({
|
||||||
|
type: rowItem.Type,
|
||||||
|
containerEngine: rowItem.ContainerEngine,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
header: 'Type',
|
||||||
|
cell: Cell,
|
||||||
|
id: 'Type',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function Cell({ getValue }: CellContext<Environment, EnvironmentType>) {
|
function Cell({ getValue }: CellContext<EnvironmentListItem, TypeCellContext>) {
|
||||||
const type = getValue();
|
const { type, containerEngine } = getValue();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Icon icon={environmentTypeIcon(type)} />
|
<Icon icon={getEnvironmentTypeIcon(type, containerEngine)} />
|
||||||
{getPlatformTypeName(type)}
|
{getPlatformTypeName(type, containerEngine)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { ContainerEngine, EnvironmentId } from '../types';
|
||||||
|
|
||||||
|
import { useEnvironment } from './useEnvironment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useIsPodman returns true if the current environment is using podman as container engine.
|
||||||
|
* @returns isPodman boolean, can also be undefined if the environment hasn't loaded yet.
|
||||||
|
*/
|
||||||
|
export function useIsPodman(envId: EnvironmentId) {
|
||||||
|
const { data: isPodman } = useEnvironment(
|
||||||
|
envId,
|
||||||
|
(env) => env.ContainerEngine === ContainerEngine.Podman
|
||||||
|
);
|
||||||
|
return isPodman;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
export function getDockerEnvironmentType(isSwarm: boolean, isPodman?: boolean) {
|
||||||
|
if (isPodman) {
|
||||||
|
return 'Podman';
|
||||||
|
}
|
||||||
|
return isSwarm ? 'Swarm' : 'Standalone';
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
export { EnvironmentTypeSelectView } from './EndpointTypeView';
|
export { EnvironmentTypeSelectView } from './EnvironmentTypeSelectView';
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
ContainerEngine,
|
||||||
|
Environment,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { AgentForm } from '../../shared/AgentForm/AgentForm';
|
||||||
|
|
||||||
|
import { DeploymentScripts } from './DeploymentScripts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onCreate(environment: Environment): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentTab({ onCreate }: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeploymentScripts />
|
||||||
|
|
||||||
|
<div className="mt-5">
|
||||||
|
<AgentForm
|
||||||
|
onCreate={onCreate}
|
||||||
|
containerEngine={ContainerEngine.Podman}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails';
|
||||||
|
|
||||||
|
import { CopyButton } from '@@/buttons/CopyButton';
|
||||||
|
import { Code } from '@@/Code';
|
||||||
|
import { NavTabs } from '@@/NavTabs';
|
||||||
|
import { NavContainer } from '@@/NavTabs/NavContainer';
|
||||||
|
|
||||||
|
const deploymentPodman = [
|
||||||
|
{
|
||||||
|
id: 'all',
|
||||||
|
label: 'Linux (CentOS)',
|
||||||
|
command: linuxPodmanCommandRootful,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DeploymentScripts() {
|
||||||
|
const deployments = deploymentPodman;
|
||||||
|
const [deployType, setDeployType] = useState(deployments[0].id);
|
||||||
|
|
||||||
|
const agentDetailsQuery = useAgentDetails();
|
||||||
|
|
||||||
|
if (!agentDetailsQuery) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { agentVersion, agentSecret } = agentDetailsQuery;
|
||||||
|
|
||||||
|
const options = deployments.map((c) => {
|
||||||
|
const code = c.command(agentVersion, agentSecret);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: c.id,
|
||||||
|
label: c.label,
|
||||||
|
children: <DeployCode code={code} />,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavContainer>
|
||||||
|
<NavTabs
|
||||||
|
options={options}
|
||||||
|
onSelect={(id: string) => setDeployType(id)}
|
||||||
|
selectedId={deployType}
|
||||||
|
/>
|
||||||
|
</NavContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeployCodeProps {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeployCode({ code }: DeployCodeProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="code-script">
|
||||||
|
<Code>{code}</Code>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<CopyButton copyText={code} data-cy="copy-deployment-script">
|
||||||
|
Copy command
|
||||||
|
</CopyButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function linuxPodmanCommandRootful(agentVersion: string, agentSecret: string) {
|
||||||
|
const secret =
|
||||||
|
agentSecret === '' ? '' : `\\\n -e AGENT_SECRET=${agentSecret} `;
|
||||||
|
|
||||||
|
return `sudo systemctl enable --now podman.socket\n
|
||||||
|
sudo podman volume create portainer\n
|
||||||
|
sudo podman run -d \\
|
||||||
|
-p 9001:9001 ${secret}\\
|
||||||
|
--name portainer_agent \\
|
||||||
|
--restart=always \\
|
||||||
|
--privileged \\
|
||||||
|
-v /run/podman/podman.sock:/var/run/docker.sock \\
|
||||||
|
-v /var/lib/containers/storage/volumes:/var/lib/docker/volumes \\
|
||||||
|
-v /:/host \\
|
||||||
|
portainer/agent:${agentVersion}
|
||||||
|
`;
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { AgentTab } from './AgentTab';
|
@ -0,0 +1,68 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { CopyButton } from '@@/buttons/CopyButton';
|
||||||
|
import { Code } from '@@/Code';
|
||||||
|
import { NavTabs } from '@@/NavTabs';
|
||||||
|
import { NavContainer } from '@@/NavTabs/NavContainer';
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
|
const deployments = [
|
||||||
|
{
|
||||||
|
id: 'linux',
|
||||||
|
label: 'Linux (CentOS)',
|
||||||
|
command: `sudo systemctl enable --now podman.socket`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DeploymentScripts() {
|
||||||
|
const [deployType, setDeployType] = useState(deployments[0].id);
|
||||||
|
|
||||||
|
const options = deployments.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
label: c.label,
|
||||||
|
children: <DeployCode code={c.command} />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavContainer>
|
||||||
|
<NavTabs
|
||||||
|
options={options}
|
||||||
|
onSelect={(id: string) => setDeployType(id)}
|
||||||
|
selectedId={deployType}
|
||||||
|
/>
|
||||||
|
</NavContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeployCodeProps {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeployCode({ code }: DeployCodeProps) {
|
||||||
|
const bindMountCode = `-v "/run/podman/podman.sock:/var/run/docker.sock"`;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextTip color="blue" className="mb-1">
|
||||||
|
When using the socket, ensure that you have started the Portainer
|
||||||
|
container with the following Podman flag:
|
||||||
|
</TextTip>
|
||||||
|
<Code>{bindMountCode}</Code>
|
||||||
|
<div className="mt-2 mb-4">
|
||||||
|
<CopyButton copyText={bindMountCode} data-cy="copy-deployment-command">
|
||||||
|
Copy command
|
||||||
|
</CopyButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextTip color="blue" className="mb-1">
|
||||||
|
To use the socket, ensure that you have started the Podman rootful
|
||||||
|
socket:
|
||||||
|
</TextTip>
|
||||||
|
<Code>{code}</Code>
|
||||||
|
<div className="mt-2">
|
||||||
|
<CopyButton copyText={code} data-cy="copy-deployment-command">
|
||||||
|
Copy command
|
||||||
|
</CopyButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
import { Field, Form, Formik, useFormikContext } from 'formik';
|
||||||
|
import { useReducer } from 'react';
|
||||||
|
import { Plug2 } from 'lucide-react';
|
||||||
|
|
||||||
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
import { useCreateLocalDockerEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation';
|
||||||
|
import {
|
||||||
|
ContainerEngine,
|
||||||
|
Environment,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||||
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
|
import { Input } from '@@/form-components/Input';
|
||||||
|
import { SwitchField } from '@@/form-components/SwitchField';
|
||||||
|
|
||||||
|
import { NameField } from '../../shared/NameField';
|
||||||
|
import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
|
||||||
|
|
||||||
|
import { useValidation } from './SocketForm.validation';
|
||||||
|
import { FormValues } from './types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onCreate(environment: Environment): void;
|
||||||
|
containerEngine: ContainerEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SocketForm({ onCreate, containerEngine }: Props) {
|
||||||
|
const [formKey, clearForm] = useReducer((state) => state + 1, 0);
|
||||||
|
const initialValues: FormValues = {
|
||||||
|
name: '',
|
||||||
|
socketPath: '',
|
||||||
|
overridePath: false,
|
||||||
|
meta: { groupId: 1, tagIds: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutation = useCreateLocalDockerEnvironmentMutation();
|
||||||
|
const validation = useValidation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validationSchema={validation}
|
||||||
|
validateOnMount
|
||||||
|
key={formKey}
|
||||||
|
>
|
||||||
|
{({ isValid, dirty }) => (
|
||||||
|
<Form>
|
||||||
|
<NameField />
|
||||||
|
|
||||||
|
<OverrideSocketFieldset />
|
||||||
|
|
||||||
|
<MoreSettingsSection />
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<LoadingButton
|
||||||
|
className="wizard-connect-button vertical-center"
|
||||||
|
data-cy="docker-socket-connect-button"
|
||||||
|
loadingText="Connecting environment..."
|
||||||
|
isLoading={mutation.isLoading}
|
||||||
|
disabled={!dirty || !isValid}
|
||||||
|
icon={Plug2}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</LoadingButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSubmit(values: FormValues) {
|
||||||
|
mutation.mutate(
|
||||||
|
{
|
||||||
|
name: values.name,
|
||||||
|
socketPath: values.overridePath ? values.socketPath : '',
|
||||||
|
meta: values.meta,
|
||||||
|
containerEngine,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(environment) {
|
||||||
|
notifySuccess('Environment created', environment.Name);
|
||||||
|
clearForm();
|
||||||
|
onCreate(environment);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverrideSocketFieldset() {
|
||||||
|
const { values, setFieldValue, errors } = useFormikContext<FormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<SwitchField
|
||||||
|
checked={values.overridePath}
|
||||||
|
data-cy="create-docker-env-socket-override-switch"
|
||||||
|
onChange={(checked) => setFieldValue('overridePath', checked)}
|
||||||
|
label="Override default socket path"
|
||||||
|
labelClass="col-sm-3 col-lg-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{values.overridePath && (
|
||||||
|
<FormControl
|
||||||
|
label="Socket Path"
|
||||||
|
tooltip="Path to the Podman socket. Remember to bind-mount the socket, see the important notice above for more information."
|
||||||
|
errors={errors.socketPath}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
name="socketPath"
|
||||||
|
as={Input}
|
||||||
|
placeholder="e.g. /run/podman/podman.sock (on Linux)"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { boolean, object, SchemaOf, string } from 'yup';
|
||||||
|
|
||||||
|
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
|
||||||
|
import { useNameValidation } from '../../shared/NameField';
|
||||||
|
|
||||||
|
import { FormValues } from './types';
|
||||||
|
|
||||||
|
export function useValidation(): SchemaOf<FormValues> {
|
||||||
|
return object({
|
||||||
|
name: useNameValidation(),
|
||||||
|
meta: metadataValidation(),
|
||||||
|
overridePath: boolean().default(false),
|
||||||
|
socketPath: string()
|
||||||
|
.default('')
|
||||||
|
.when('overridePath', (overridePath, schema) =>
|
||||||
|
overridePath
|
||||||
|
? schema.required(
|
||||||
|
'Socket Path is required when override path is enabled'
|
||||||
|
)
|
||||||
|
: schema
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
ContainerEngine,
|
||||||
|
Environment,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
|
import { DeploymentScripts } from './DeploymentScripts';
|
||||||
|
import { SocketForm } from './SocketForm';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onCreate(environment: Environment): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SocketTab({ onCreate }: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextTip color="orange" className="mb-2" inline={false}>
|
||||||
|
To connect via socket, Portainer server must be running in a Podman
|
||||||
|
container.
|
||||||
|
</TextTip>
|
||||||
|
|
||||||
|
<DeploymentScripts />
|
||||||
|
|
||||||
|
<div className="mt-5">
|
||||||
|
<SocketForm
|
||||||
|
onCreate={onCreate}
|
||||||
|
containerEngine={ContainerEngine.Podman}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { SocketTab } from './SocketTab';
|
@ -0,0 +1,8 @@
|
|||||||
|
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
|
||||||
|
|
||||||
|
export interface FormValues {
|
||||||
|
name: string;
|
||||||
|
socketPath: string;
|
||||||
|
overridePath: boolean;
|
||||||
|
meta: EnvironmentMetadata;
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Zap, Plug2 } from 'lucide-react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ContainerEngine,
|
||||||
|
Environment,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
|
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
||||||
|
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c';
|
||||||
|
import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c';
|
||||||
|
|
||||||
|
import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector';
|
||||||
|
import { BadgeIcon } from '@@/BadgeIcon';
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
|
||||||
|
import { AnalyticsStateKey } from '../types';
|
||||||
|
import { EdgeAgentTab } from '../shared/EdgeAgentTab';
|
||||||
|
|
||||||
|
import { AgentTab } from './AgentTab';
|
||||||
|
import { SocketTab } from './SocketTab';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: BoxSelectorOption<
|
||||||
|
'agent' | 'api' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync'
|
||||||
|
>[] = _.compact([
|
||||||
|
{
|
||||||
|
id: 'agent',
|
||||||
|
icon: <BadgeIcon icon={Zap} size="3xl" />,
|
||||||
|
label: 'Agent',
|
||||||
|
description: '',
|
||||||
|
value: 'agent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'socket',
|
||||||
|
icon: <BadgeIcon icon={Plug2} size="3xl" />,
|
||||||
|
label: 'Socket',
|
||||||
|
description: '',
|
||||||
|
value: 'socket',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edgeAgentStandard',
|
||||||
|
icon: <BadgeIcon icon={EdgeAgentStandardIcon} size="3xl" />,
|
||||||
|
label: 'Edge Agent Standard',
|
||||||
|
description: '',
|
||||||
|
value: 'edgeAgentStandard',
|
||||||
|
},
|
||||||
|
isBE && {
|
||||||
|
id: 'edgeAgentAsync',
|
||||||
|
icon: <BadgeIcon icon={EdgeAgentAsyncIcon} size="3xl" />,
|
||||||
|
label: 'Edge Agent Async',
|
||||||
|
description: '',
|
||||||
|
value: 'edgeAgentAsync',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const containerEngine = ContainerEngine.Podman;
|
||||||
|
|
||||||
|
export function WizardPodman({ onCreate }: Props) {
|
||||||
|
const [creationType, setCreationType] = useState(options[0].value);
|
||||||
|
|
||||||
|
const tab = getTab(creationType);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-horizontal">
|
||||||
|
<BoxSelector
|
||||||
|
onChange={(v) => setCreationType(v)}
|
||||||
|
options={options}
|
||||||
|
value={creationType}
|
||||||
|
radioName="creation-type"
|
||||||
|
/>
|
||||||
|
<TextTip color="orange" className="mb-2" inline={false}>
|
||||||
|
Currently, Portainer only supports <b>Podman 5</b> running in rootful
|
||||||
|
(privileged) mode on <b>CentOS 9</b> Linux environments. Rootless mode
|
||||||
|
and other Linux distros may work, but aren't officially supported.
|
||||||
|
</TextTip>
|
||||||
|
{tab}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function getTab(
|
||||||
|
creationType:
|
||||||
|
| 'agent'
|
||||||
|
| 'api'
|
||||||
|
| 'socket'
|
||||||
|
| 'edgeAgentStandard'
|
||||||
|
| 'edgeAgentAsync'
|
||||||
|
) {
|
||||||
|
switch (creationType) {
|
||||||
|
case 'agent':
|
||||||
|
return (
|
||||||
|
<AgentTab
|
||||||
|
onCreate={(environment) => onCreate(environment, 'podmanAgent')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'socket':
|
||||||
|
return (
|
||||||
|
<SocketTab
|
||||||
|
onCreate={(environment) =>
|
||||||
|
onCreate(environment, 'podmanLocalEnvironment')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'edgeAgentStandard':
|
||||||
|
return (
|
||||||
|
<EdgeAgentTab
|
||||||
|
onCreate={(environment) =>
|
||||||
|
onCreate(environment, 'podmanEdgeAgentStandard')
|
||||||
|
}
|
||||||
|
commands={[commandsTabs.podmanLinux]}
|
||||||
|
containerEngine={containerEngine}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'edgeAgentAsync':
|
||||||
|
return (
|
||||||
|
<EdgeAgentTab
|
||||||
|
asyncMode
|
||||||
|
onCreate={(environment) =>
|
||||||
|
onCreate(environment, 'podmanEdgeAgentAsync')
|
||||||
|
}
|
||||||
|
commands={[commandsTabs.podmanLinux]}
|
||||||
|
containerEngine={containerEngine}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { WizardPodman } from './WizardPodman';
|