mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 21:39:40 +02:00
fix(app templates): load app template for deployment [BE-11382] (#141)
This commit is contained in:
parent
20e3d3a15b
commit
c0c7144539
23 changed files with 453 additions and 60 deletions
|
@ -15,6 +15,7 @@ const BoxSelectorReact = react2angular(BoxSelector, [
|
||||||
'radioName',
|
'radioName',
|
||||||
'slim',
|
'slim',
|
||||||
'hiddenSpacingCount',
|
'hiddenSpacingCount',
|
||||||
|
'error',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const boxSelectorModule = angular
|
export const boxSelectorModule = angular
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
|
||||||
import styles from './BoxSelector.module.css';
|
import styles from './BoxSelector.module.css';
|
||||||
import { BoxSelectorItem } from './BoxSelectorItem';
|
import { BoxSelectorItem } from './BoxSelectorItem';
|
||||||
import { BoxSelectorOption, Value } from './types';
|
import { BoxSelectorOption, Value } from './types';
|
||||||
|
@ -21,6 +23,7 @@ export type Props<T extends Value> = Union<T> & {
|
||||||
options: ReadonlyArray<BoxSelectorOption<T>> | Array<BoxSelectorOption<T>>;
|
options: ReadonlyArray<BoxSelectorOption<T>> | Array<BoxSelectorOption<T>>;
|
||||||
slim?: boolean;
|
slim?: boolean;
|
||||||
hiddenSpacingCount?: number;
|
hiddenSpacingCount?: number;
|
||||||
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function BoxSelector<T extends Value>({
|
export function BoxSelector<T extends Value>({
|
||||||
|
@ -28,6 +31,7 @@ export function BoxSelector<T extends Value>({
|
||||||
options,
|
options,
|
||||||
slim = false,
|
slim = false,
|
||||||
hiddenSpacingCount,
|
hiddenSpacingCount,
|
||||||
|
error,
|
||||||
...props
|
...props
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
return (
|
return (
|
||||||
|
@ -54,6 +58,7 @@ export function BoxSelector<T extends Value>({
|
||||||
<div key={index} className="flex-1" />
|
<div key={index} className="flex-1" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{error && <FormError>{error}</FormError>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { CellContext } from '@tanstack/react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
import { Badge } from '@@/Badge';
|
||||||
|
|
||||||
import { EdgeGroupListItemResponse } from '../../queries/useEdgeGroups';
|
import { EdgeGroupListItemResponse } from '../../queries/useEdgeGroups';
|
||||||
|
|
||||||
|
@ -32,7 +33,9 @@ function NameCell({
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
{(item.HasEdgeJob || item.HasEdgeStack) && (
|
{(item.HasEdgeJob || item.HasEdgeStack) && (
|
||||||
<span className="label label-info image-tag space-left">in use</span>
|
<Badge type="info" className="ml-1">
|
||||||
|
in use
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
186
app/react/edge/edge-stacks/CreateView/CreateForm.test.tsx
Normal file
186
app/react/edge/edge-stacks/CreateView/CreateForm.test.tsx
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
import { DefaultBodyType, HttpResponse } from 'msw';
|
||||||
|
import { render, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import { UserViewModel } from '@/portainer/models/user';
|
||||||
|
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||||
|
import { withTestRouter } from '@/react/test-utils/withRouter';
|
||||||
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
||||||
|
import { http, server } from '@/setup-tests/server';
|
||||||
|
import selectEvent from '@/react/test-utils/react-select';
|
||||||
|
|
||||||
|
import { CreateForm } from './CreateForm';
|
||||||
|
|
||||||
|
// browser address
|
||||||
|
// /edge/stacks/new?templateId=54&templateType=app
|
||||||
|
vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
|
||||||
|
...(await importOriginal()),
|
||||||
|
useCurrentStateAndParams: vi.fn(() => ({
|
||||||
|
params: { templateId: 54, templateType: 'app' },
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@uiw/react-codemirror', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => <div />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// app templates request
|
||||||
|
// GET /api/templates
|
||||||
|
const templatesResponseBody = {
|
||||||
|
version: '3',
|
||||||
|
templates: [
|
||||||
|
{
|
||||||
|
id: 54,
|
||||||
|
type: 3,
|
||||||
|
title: 'TOSIBOX Lock for Container',
|
||||||
|
description:
|
||||||
|
'Lock for Container brings secure connectivity inside your industrial IoT devices',
|
||||||
|
administrator_only: false,
|
||||||
|
image: '',
|
||||||
|
repository: {
|
||||||
|
url: 'https://github.com/portainer/templates',
|
||||||
|
stackfile: 'stacks/tosibox/docker-compose.yml',
|
||||||
|
},
|
||||||
|
stackFile: '',
|
||||||
|
logo: 'https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/tosibox.png',
|
||||||
|
env: [
|
||||||
|
{
|
||||||
|
name: 'LICENSE_KEY',
|
||||||
|
label: 'License key',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
platform: 'linux',
|
||||||
|
categories: ['edge'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// app template content request
|
||||||
|
// GET /api/templates/54/file
|
||||||
|
const templateContentResponseBody = {
|
||||||
|
FileContent:
|
||||||
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
'version: "3.7"\nservices:\n tosibox-lock-for-container:\n container_name: tosibox-lock-for-container\n image: tosibox/lock-for-container:latest\n hostname: tb-lfc\n restart: unless-stopped\n cap_add:\n - NET_ADMIN\n - SYS_TIME\n - SYS_PTRACE\n ports:\n - 80\n networks:\n - tbnet\n volumes:\n - tosibox-lfc:/etc/tosibox/docker_volume\n environment:\n - LICENSE_KEY=${LICENSE_KEY}\nvolumes:\n tosibox-lfc:\n name: tosibox-lfc\nnetworks:\n tbnet:\n name: tbnet\n ipam:\n config:\n - subnet: 10.10.206.0/24\n',
|
||||||
|
};
|
||||||
|
|
||||||
|
// edge groups
|
||||||
|
const edgeGroups = [
|
||||||
|
{
|
||||||
|
Id: 1,
|
||||||
|
Name: 'docker',
|
||||||
|
Dynamic: false,
|
||||||
|
TagIds: [],
|
||||||
|
Endpoints: [12],
|
||||||
|
PartialMatch: false,
|
||||||
|
HasEdgeStack: false,
|
||||||
|
HasEdgeJob: false,
|
||||||
|
EndpointTypes: [4],
|
||||||
|
TrustedEndpoints: [12],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: 2,
|
||||||
|
Name: 'kubernetes',
|
||||||
|
Dynamic: false,
|
||||||
|
TagIds: [],
|
||||||
|
Endpoints: [11],
|
||||||
|
PartialMatch: false,
|
||||||
|
HasEdgeStack: false,
|
||||||
|
HasEdgeJob: false,
|
||||||
|
EndpointTypes: [7],
|
||||||
|
TrustedEndpoints: [11],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// expected form values
|
||||||
|
const expectedPayload = {
|
||||||
|
deploymentType: 0,
|
||||||
|
edgeGroups: [1],
|
||||||
|
name: 'my-stack',
|
||||||
|
envVars: [{ name: 'LICENSE_KEY', value: 'license-123' }],
|
||||||
|
prePullImage: false,
|
||||||
|
registries: [],
|
||||||
|
retryDeploy: false,
|
||||||
|
staggerConfig: {
|
||||||
|
StaggerOption: 1,
|
||||||
|
StaggerParallelOption: 1,
|
||||||
|
DeviceNumber: 1,
|
||||||
|
DeviceNumberStartFrom: 0,
|
||||||
|
DeviceNumberIncrementBy: 2,
|
||||||
|
Timeout: '',
|
||||||
|
UpdateDelay: '',
|
||||||
|
UpdateFailureAction: 1,
|
||||||
|
},
|
||||||
|
useManifestNamespaces: false,
|
||||||
|
stackFileContent:
|
||||||
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
|
'version: "3.7"\nservices:\n tosibox-lock-for-container:\n container_name: tosibox-lock-for-container\n image: tosibox/lock-for-container:latest\n hostname: tb-lfc\n restart: unless-stopped\n cap_add:\n - NET_ADMIN\n - SYS_TIME\n - SYS_PTRACE\n ports:\n - 80\n networks:\n - tbnet\n volumes:\n - tosibox-lfc:/etc/tosibox/docker_volume\n environment:\n - LICENSE_KEY=${LICENSE_KEY}\nvolumes:\n tosibox-lfc:\n name: tosibox-lfc\nnetworks:\n tbnet:\n name: tbnet\n ipam:\n config:\n - subnet: 10.10.206.0/24\n',
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderCreateForm() {
|
||||||
|
server.use(
|
||||||
|
http.get('/api/templates', () => HttpResponse.json(templatesResponseBody))
|
||||||
|
);
|
||||||
|
server.use(
|
||||||
|
http.post('/api/templates/54/file', () =>
|
||||||
|
HttpResponse.json(templateContentResponseBody)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
server.use(http.get('/api/edge_stacks', () => HttpResponse.json([])));
|
||||||
|
server.use(http.get('/api/edge_groups', () => HttpResponse.json(edgeGroups)));
|
||||||
|
server.use(http.get('/api/registries', () => HttpResponse.json([])));
|
||||||
|
server.use(http.get('/api/custom_templates', () => HttpResponse.json([])));
|
||||||
|
|
||||||
|
const user = new UserViewModel({ Username: 'user' });
|
||||||
|
const Wrapped = withTestQueryProvider(
|
||||||
|
withUserProvider(withTestRouter(CreateForm), user)
|
||||||
|
);
|
||||||
|
return render(<Wrapped />);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('The web editor should be visible for app templates', async () => {
|
||||||
|
const { getByRole, getByLabelText } = renderCreateForm();
|
||||||
|
|
||||||
|
// Wait for the form to be rendered
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByRole('form')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// the web editor should be visible
|
||||||
|
expect(getByLabelText('Web editor')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('The form should submit the correct request body', async () => {
|
||||||
|
let requestBody: DefaultBodyType;
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
http.post('/api/edge_stacks/create/string', async ({ request }) => {
|
||||||
|
requestBody = await request.json();
|
||||||
|
return HttpResponse.json({});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByRole, getByLabelText } = renderCreateForm();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByRole('form')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// fill in the name and select the docker edge group
|
||||||
|
const user = userEvent.setup();
|
||||||
|
await user.type(getByRole('textbox', { name: 'Name *' }), 'my-stack');
|
||||||
|
await user.type(
|
||||||
|
getByRole('textbox', { name: 'License key *' }),
|
||||||
|
'license-123'
|
||||||
|
);
|
||||||
|
const selectElement = getByLabelText('Edge groups');
|
||||||
|
await selectEvent.select(selectElement, 'docker');
|
||||||
|
|
||||||
|
// submit the form
|
||||||
|
await user.click(getByRole('button', { name: /Deploy the stack/i }));
|
||||||
|
|
||||||
|
// verify the request body
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(requestBody).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
|
@ -18,12 +18,15 @@ import { relativePathValidation } from '@/react/portainer/gitops/RelativePathFie
|
||||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
||||||
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
|
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
|
||||||
import { DeployMethod, GitFormModel } from '@/react/portainer/gitops/types';
|
import { DeployMethod, GitFormModel } from '@/react/portainer/gitops/types';
|
||||||
|
import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { envVarValidation } from '@@/form-components/EnvironmentVariablesFieldset';
|
import { envVarValidation } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
import { file } from '@@/form-components/yup-file-validation';
|
import { file } from '@@/form-components/yup-file-validation';
|
||||||
|
|
||||||
import { DeploymentType } from '../types';
|
import { DeploymentType } from '../types';
|
||||||
import { staggerConfigValidation } from '../components/StaggerFieldset';
|
import { staggerConfigValidation } from '../components/StaggerFieldset';
|
||||||
|
import { createHasEnvironmentTypeFunction } from '../ItemView/EditEdgeStackForm/useEdgeGroupHasType';
|
||||||
|
import { useEdgeGroups } from '../../edge-groups/queries/useEdgeGroups';
|
||||||
|
|
||||||
import { FormValues, Method } from './types';
|
import { FormValues, Method } from './types';
|
||||||
import { templateFieldsetValidation } from './TemplateFieldset/validation';
|
import { templateFieldsetValidation } from './TemplateFieldset/validation';
|
||||||
|
@ -39,6 +42,8 @@ export function useValidation({
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const gitCredentialsQuery = useGitCredentials(user.Id);
|
const gitCredentialsQuery = useGitCredentials(user.Id);
|
||||||
const nameValidation = useNameValidation();
|
const nameValidation = useNameValidation();
|
||||||
|
const edgeGroupsQuery = useEdgeGroups();
|
||||||
|
const edgeGroups = edgeGroupsQuery.data;
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -53,7 +58,47 @@ export function useValidation({
|
||||||
.min(1, 'At least one Edge group is required'),
|
.min(1, 'At least one Edge group is required'),
|
||||||
deploymentType: mixed<DeploymentType>()
|
deploymentType: mixed<DeploymentType>()
|
||||||
.oneOf([DeploymentType.Compose, DeploymentType.Kubernetes])
|
.oneOf([DeploymentType.Compose, DeploymentType.Kubernetes])
|
||||||
.required(),
|
.required()
|
||||||
|
.test(
|
||||||
|
'kubernetes-deployment-type-validation',
|
||||||
|
'Kubernetes deployment type is not compatible with the selected edge group(s), which contain Docker environments',
|
||||||
|
(value) => {
|
||||||
|
if (value !== DeploymentType.Kubernetes) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasType = createHasEnvironmentTypeFunction(
|
||||||
|
values.groupIds,
|
||||||
|
edgeGroups
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasDockerEndpoint = hasType(
|
||||||
|
EnvironmentType.EdgeAgentOnDocker
|
||||||
|
);
|
||||||
|
|
||||||
|
return !hasDockerEndpoint;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.test(
|
||||||
|
'compose-deployment-type-validation',
|
||||||
|
'Compose deployment type is not compatible with the selected edge group(s), which contain Kubernetes environments',
|
||||||
|
(value) => {
|
||||||
|
if (value !== DeploymentType.Compose) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasType = createHasEnvironmentTypeFunction(
|
||||||
|
values.groupIds,
|
||||||
|
edgeGroups
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasKubeEndpoint = hasType(
|
||||||
|
EnvironmentType.EdgeAgentOnKubernetes
|
||||||
|
);
|
||||||
|
|
||||||
|
return !hasKubeEndpoint;
|
||||||
|
}
|
||||||
|
),
|
||||||
envVars: envVarValidation(),
|
envVars: envVarValidation(),
|
||||||
privateRegistryId: number().default(0),
|
privateRegistryId: number().default(0),
|
||||||
prePullImage: boolean().default(false),
|
prePullImage: boolean().default(false),
|
||||||
|
@ -92,6 +137,12 @@ export function useValidation({
|
||||||
useManifestNamespaces: boolean().default(false),
|
useManifestNamespaces: boolean().default(false),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
[appTemplate?.Env, customTemplate, gitCredentialsQuery.data, nameValidation]
|
[
|
||||||
|
appTemplate?.Env,
|
||||||
|
customTemplate,
|
||||||
|
edgeGroups,
|
||||||
|
gitCredentialsQuery.data,
|
||||||
|
nameValidation,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,10 @@ export function CreateView() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Create Edge stack"
|
title="Create Edge Stack"
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ label: 'Edge Stacks', link: 'edge.stacks' },
|
{ label: 'Edge Stacks', link: 'edge.stacks' },
|
||||||
'Create Edge stack',
|
'Create Edge Stack',
|
||||||
]}
|
]}
|
||||||
reload
|
reload
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -16,9 +16,10 @@ import {
|
||||||
import { FileUploadForm } from '@@/form-components/FileUpload';
|
import { FileUploadForm } from '@@/form-components/FileUpload';
|
||||||
|
|
||||||
import { TemplateFieldset } from './TemplateFieldset/TemplateFieldset';
|
import { TemplateFieldset } from './TemplateFieldset/TemplateFieldset';
|
||||||
import { useRenderTemplate } from './useRenderTemplate';
|
import { useRenderCustomTemplate } from './useRenderCustomTemplate';
|
||||||
import { DockerFormValues } from './types';
|
import { DockerFormValues } from './types';
|
||||||
import { DockerContentField } from './DockerContentField';
|
import { DockerContentField } from './DockerContentField';
|
||||||
|
import { useRenderAppTemplate } from './useRenderAppTemplate';
|
||||||
|
|
||||||
const buildMethods = [editor, upload, git, edgeStackTemplate] as const;
|
const buildMethods = [editor, upload, git, edgeStackTemplate] as const;
|
||||||
|
|
||||||
|
@ -38,7 +39,14 @@ export function DockerComposeForm({
|
||||||
const { errors, values, setValues } = useFormikContext<DockerFormValues>();
|
const { errors, values, setValues } = useFormikContext<DockerFormValues>();
|
||||||
const { method } = values;
|
const { method } = values;
|
||||||
|
|
||||||
const template = useRenderTemplate(values.templateValues, setValues);
|
const { customTemplate, isInitialLoading: isCustomTemplateLoading } =
|
||||||
|
useRenderCustomTemplate(values.templateValues, setValues);
|
||||||
|
const { appTemplate, isInitialLoading: isAppTemplateLoading } =
|
||||||
|
useRenderAppTemplate(values.templateValues, setValues);
|
||||||
|
|
||||||
|
const isTemplate =
|
||||||
|
method === edgeStackTemplate.value && (customTemplate || appTemplate);
|
||||||
|
const isTemplateLoading = isCustomTemplateLoading || isAppTemplateLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -73,15 +81,17 @@ export function DockerComposeForm({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
errors={errors?.templateValues}
|
errors={errors?.templateValues}
|
||||||
|
isLoadingValues={isTemplateLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(method === editor.value ||
|
{(method === editor.value || isTemplate) && !isTemplateLoading && (
|
||||||
(method === edgeStackTemplate.value && template)) && (
|
|
||||||
<DockerContentField
|
<DockerContentField
|
||||||
value={values.fileContent}
|
value={values.fileContent}
|
||||||
onChange={(value) => handleChange({ fileContent: value })}
|
onChange={(value) => handleChange({ fileContent: value })}
|
||||||
readonly={method === edgeStackTemplate.value && !!template?.GitConfig}
|
readonly={
|
||||||
|
method === edgeStackTemplate.value && !!customTemplate?.GitConfig
|
||||||
|
}
|
||||||
error={errors?.fileContent}
|
error={errors?.fileContent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { Form, useFormikContext } from 'formik';
|
||||||
import { applySetStateAction } from '@/react-tools/apply-set-state-action';
|
import { applySetStateAction } from '@/react-tools/apply-set-state-action';
|
||||||
import { EnvironmentType } from '@/react/portainer/environments/types';
|
import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
|
||||||
import { EnvironmentVariablesPanel } from '@@/form-components/EnvironmentVariablesFieldset';
|
import { EnvironmentVariablesPanel } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
import { FormActions } from '@@/form-components/FormActions';
|
import { FormActions } from '@@/form-components/FormActions';
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@ import { EdgeGroupsSelector } from '../components/EdgeGroupsSelector';
|
||||||
import { EdgeStackDeploymentTypeSelector } from '../components/EdgeStackDeploymentTypeSelector';
|
import { EdgeStackDeploymentTypeSelector } from '../components/EdgeStackDeploymentTypeSelector';
|
||||||
import { StaggerFieldset } from '../components/StaggerFieldset';
|
import { StaggerFieldset } from '../components/StaggerFieldset';
|
||||||
import { PrivateRegistryFieldsetWrapper } from '../ItemView/EditEdgeStackForm/PrivateRegistryFieldsetWrapper';
|
import { PrivateRegistryFieldsetWrapper } from '../ItemView/EditEdgeStackForm/PrivateRegistryFieldsetWrapper';
|
||||||
import { useValidateEnvironmentTypes } from '../ItemView/EditEdgeStackForm/useEdgeGroupHasType';
|
import { useEdgeGroupHasType } from '../ItemView/EditEdgeStackForm/useEdgeGroupHasType';
|
||||||
import { DeploymentType } from '../types';
|
import { DeploymentType } from '../types';
|
||||||
|
|
||||||
import { DockerComposeForm } from './DockerComposeForm';
|
import { DockerComposeForm } from './DockerComposeForm';
|
||||||
|
@ -38,13 +37,20 @@ export function InnerForm({
|
||||||
}) {
|
}) {
|
||||||
const { values, setFieldValue, errors, setValues, setFieldError, isValid } =
|
const { values, setFieldValue, errors, setValues, setFieldError, isValid } =
|
||||||
useFormikContext<FormValues>();
|
useFormikContext<FormValues>();
|
||||||
const { hasType } = useValidateEnvironmentTypes(values.groupIds);
|
const { hasType } = useEdgeGroupHasType(values.groupIds);
|
||||||
|
|
||||||
const hasKubeEndpoint = hasType(EnvironmentType.EdgeAgentOnKubernetes);
|
const hasKubeEndpoint = hasType(EnvironmentType.EdgeAgentOnKubernetes);
|
||||||
const hasDockerEndpoint = hasType(EnvironmentType.EdgeAgentOnDocker);
|
const hasDockerEndpoint = hasType(EnvironmentType.EdgeAgentOnDocker);
|
||||||
|
const hasMultipleTypes = hasKubeEndpoint && hasDockerEndpoint;
|
||||||
|
const multipleTypesError = hasMultipleTypes
|
||||||
|
? `There are no available deployment types when there is more than one
|
||||||
|
type of environment in your edge group selection (e.g. Kubernetes and
|
||||||
|
Docker environments). Please select edge groups that have environments
|
||||||
|
of the same type.`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form className="form-horizontal">
|
<Form className="form-horizontal" role="form">
|
||||||
<NameField
|
<NameField
|
||||||
onChange={(value) => setFieldValue('name', value)}
|
onChange={(value) => setFieldValue('name', value)}
|
||||||
value={values.name}
|
value={values.name}
|
||||||
|
@ -54,23 +60,15 @@ export function InnerForm({
|
||||||
<EdgeGroupsSelector
|
<EdgeGroupsSelector
|
||||||
value={values.groupIds}
|
value={values.groupIds}
|
||||||
onChange={(value) => setFieldValue('groupIds', value)}
|
onChange={(value) => setFieldValue('groupIds', value)}
|
||||||
error={errors.groupIds}
|
error={errors.groupIds || multipleTypesError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasKubeEndpoint && hasDockerEndpoint && (
|
|
||||||
<TextTip>
|
|
||||||
There are no available deployment types when there is more than one
|
|
||||||
type of environment in your edge group selection (e.g. Kubernetes and
|
|
||||||
Docker environments). Please select edge groups that have environments
|
|
||||||
of the same type.
|
|
||||||
</TextTip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<EdgeStackDeploymentTypeSelector
|
<EdgeStackDeploymentTypeSelector
|
||||||
value={values.deploymentType}
|
value={values.deploymentType}
|
||||||
hasDockerEndpoint={hasDockerEndpoint}
|
hasDockerEndpoint={hasDockerEndpoint}
|
||||||
hasKubeEndpoint={hasKubeEndpoint}
|
hasKubeEndpoint={hasKubeEndpoint}
|
||||||
onChange={(value) => setFieldValue('deploymentType', value)}
|
onChange={(value) => setFieldValue('deploymentType', value)}
|
||||||
|
error={errors.deploymentType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{values.deploymentType === DeploymentType.Compose && (
|
{values.deploymentType === DeploymentType.Compose && (
|
||||||
|
|
|
@ -16,10 +16,12 @@ export function TemplateFieldset({
|
||||||
values,
|
values,
|
||||||
setValues,
|
setValues,
|
||||||
errors,
|
errors,
|
||||||
|
isLoadingValues,
|
||||||
}: {
|
}: {
|
||||||
errors?: FormikErrors<Values>;
|
errors?: FormikErrors<Values>;
|
||||||
values: Values;
|
values: Values;
|
||||||
setValues: (values: SetStateAction<Values>) => void;
|
setValues: (values: SetStateAction<Values>) => void;
|
||||||
|
isLoadingValues?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -27,8 +29,9 @@ export function TemplateFieldset({
|
||||||
error={errors?.templateId}
|
error={errors?.templateId}
|
||||||
value={values}
|
value={values}
|
||||||
onChange={handleChangeTemplate}
|
onChange={handleChangeTemplate}
|
||||||
|
isLoadingValues={isLoadingValues}
|
||||||
/>
|
/>
|
||||||
{values.templateId && (
|
{values.templateId && !isLoadingValues && (
|
||||||
<>
|
<>
|
||||||
{values.type === 'custom' && (
|
{values.type === 'custom' && (
|
||||||
<CustomTemplateFieldset
|
<CustomTemplateFieldset
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { CustomTemplate } from '@/react/portainer/templates/custom-templates/typ
|
||||||
|
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
import { Select as ReactSelect } from '@@/form-components/ReactSelect';
|
import { Select as ReactSelect } from '@@/form-components/ReactSelect';
|
||||||
|
import { InlineLoader } from '@@/InlineLoader';
|
||||||
|
|
||||||
import { SelectedTemplateValue } from './types';
|
import { SelectedTemplateValue } from './types';
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ export function TemplateSelector({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
error,
|
error,
|
||||||
|
isLoadingValues,
|
||||||
}: {
|
}: {
|
||||||
value: SelectedTemplateValue;
|
value: SelectedTemplateValue;
|
||||||
onChange: (
|
onChange: (
|
||||||
|
@ -23,6 +25,7 @@ export function TemplateSelector({
|
||||||
type: 'app' | 'custom' | undefined
|
type: 'app' | 'custom' | undefined
|
||||||
) => void;
|
) => void;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
isLoadingValues?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { options, getTemplate, selectedValue } = useOptions(value);
|
const { options, getTemplate, selectedValue } = useOptions(value);
|
||||||
|
|
||||||
|
@ -48,6 +51,9 @@ export function TemplateSelector({
|
||||||
}}
|
}}
|
||||||
data-cy="edge-stacks-create-template-selector"
|
data-cy="edge-stacks-create-template-selector"
|
||||||
/>
|
/>
|
||||||
|
{isLoadingValues && (
|
||||||
|
<InlineLoader>Loading template values...</InlineLoader>
|
||||||
|
)}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,12 +102,18 @@ export function useCreate({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBasePayload(values: FormValues): BasePayload {
|
function getBasePayload(values: FormValues): BasePayload {
|
||||||
|
const templateEnvVarsAsPairs = Object.entries(
|
||||||
|
values.templateValues.envVars
|
||||||
|
).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
return {
|
return {
|
||||||
userId: user.Id,
|
userId: user.Id,
|
||||||
deploymentType: values.deploymentType,
|
deploymentType: values.deploymentType,
|
||||||
edgeGroups: values.groupIds,
|
edgeGroups: values.groupIds,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
envVars: values.envVars,
|
envVars: [...values.envVars, ...templateEnvVarsAsPairs],
|
||||||
registries: values.privateRegistryId ? [values.privateRegistryId] : [],
|
registries: values.privateRegistryId ? [values.privateRegistryId] : [],
|
||||||
prePullImage: values.prePullImage,
|
prePullImage: values.prePullImage,
|
||||||
retryDeploy: values.retryDeploy,
|
retryDeploy: values.retryDeploy,
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { SetStateAction, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
||||||
|
import { useAppTemplate } from '@/react/portainer/templates/app-templates/queries/useAppTemplates';
|
||||||
|
import { useAppTemplateFile } from '@/react/portainer/templates/app-templates/queries/useAppTemplateFile';
|
||||||
|
import { TemplateViewModel } from '@/react/portainer/templates/app-templates/view-model';
|
||||||
|
|
||||||
|
import { DeploymentType } from '../types';
|
||||||
|
import { getDefaultStaggerConfig } from '../components/StaggerFieldset.types';
|
||||||
|
|
||||||
|
import { DockerFormValues, FormValues } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useRenderAppTemplate fetches the app template (file and data) and returns it
|
||||||
|
* as a TemplateViewModel.
|
||||||
|
*
|
||||||
|
* It also renders the template file and updates the form values.
|
||||||
|
*/
|
||||||
|
export function useRenderAppTemplate(
|
||||||
|
templateValues: DockerFormValues['templateValues'],
|
||||||
|
setValues: (values: SetStateAction<DockerFormValues>) => void
|
||||||
|
) {
|
||||||
|
const templateQuery = useAppTemplate(templateValues.templateId, {
|
||||||
|
enabled: templateValues.type === 'app',
|
||||||
|
});
|
||||||
|
|
||||||
|
const template = templateQuery.data;
|
||||||
|
|
||||||
|
const templateFileQuery = useAppTemplateFile(templateValues.templateId, {
|
||||||
|
enabled: templateValues.type === 'app',
|
||||||
|
});
|
||||||
|
const [renderedFile, setRenderedFile] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (templateFileQuery.data) {
|
||||||
|
const newFile = renderTemplate(
|
||||||
|
templateFileQuery.data,
|
||||||
|
templateValues.variables,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newFile !== renderedFile) {
|
||||||
|
setRenderedFile(newFile);
|
||||||
|
setValues((values) => ({
|
||||||
|
...values,
|
||||||
|
fileContent: newFile,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
renderedFile,
|
||||||
|
setValues,
|
||||||
|
template,
|
||||||
|
templateFileQuery.data,
|
||||||
|
templateValues.variables,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [currentTemplateId, setCurrentTemplateId] = useState<
|
||||||
|
number | undefined
|
||||||
|
>(templateValues.templateId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (template?.Id !== currentTemplateId) {
|
||||||
|
setCurrentTemplateId(template?.Id);
|
||||||
|
setValues((values) => ({
|
||||||
|
...values,
|
||||||
|
...getValuesFromAppTemplate(template),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [currentTemplateId, setValues, template]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appTemplate: template,
|
||||||
|
isInitialLoading:
|
||||||
|
templateQuery.isInitialLoading || templateFileQuery.isInitialLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValuesFromAppTemplate(
|
||||||
|
template: TemplateViewModel | undefined
|
||||||
|
): Partial<FormValues> {
|
||||||
|
if (!template) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deploymentType: DeploymentType.Compose,
|
||||||
|
...(template
|
||||||
|
? {
|
||||||
|
prePullImage: false,
|
||||||
|
retryDeploy: false,
|
||||||
|
staggerConfig: getDefaultStaggerConfig(),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import { getDefaultStaggerConfig } from '../components/StaggerFieldset.types';
|
||||||
|
|
||||||
import { DockerFormValues, FormValues } from './types';
|
import { DockerFormValues, FormValues } from './types';
|
||||||
|
|
||||||
export function useRenderTemplate(
|
export function useRenderCustomTemplate(
|
||||||
templateValues: DockerFormValues['templateValues'],
|
templateValues: DockerFormValues['templateValues'],
|
||||||
setValues: (values: SetStateAction<DockerFormValues>) => void
|
setValues: (values: SetStateAction<DockerFormValues>) => void
|
||||||
) {
|
) {
|
||||||
|
@ -69,7 +69,11 @@ export function useRenderTemplate(
|
||||||
}
|
}
|
||||||
}, [currentTemplateId, setValues, template]);
|
}, [currentTemplateId, setValues, template]);
|
||||||
|
|
||||||
return template;
|
return {
|
||||||
|
customTemplate: template,
|
||||||
|
isInitialLoading:
|
||||||
|
templateQuery.isInitialLoading || templateFileQuery.isInitialLoading,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValuesFromTemplate(
|
function getValuesFromTemplate(
|
|
@ -42,7 +42,7 @@ import { FormError } from '@@/form-components/FormError';
|
||||||
import { EnvironmentVariablesPanel } from '@@/form-components/EnvironmentVariablesFieldset';
|
import { EnvironmentVariablesPanel } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
import { EnvVar } from '@@/form-components/EnvironmentVariablesFieldset/types';
|
import { EnvVar } from '@@/form-components/EnvironmentVariablesFieldset/types';
|
||||||
|
|
||||||
import { useValidateEnvironmentTypes } from '../useEdgeGroupHasType';
|
import { useEdgeGroupHasType } from '../useEdgeGroupHasType';
|
||||||
import { PrivateRegistryFieldset } from '../../../components/PrivateRegistryFieldset';
|
import { PrivateRegistryFieldset } from '../../../components/PrivateRegistryFieldset';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -172,7 +172,7 @@ function InnerForm({
|
||||||
const { values, setFieldValue, isValid, handleSubmit, errors, dirty } =
|
const { values, setFieldValue, isValid, handleSubmit, errors, dirty } =
|
||||||
useFormikContext<FormValues>();
|
useFormikContext<FormValues>();
|
||||||
|
|
||||||
const { hasType } = useValidateEnvironmentTypes(values.groupIds);
|
const { hasType } = useEdgeGroupHasType(values.groupIds);
|
||||||
|
|
||||||
const hasKubeEndpoint = hasType(EnvironmentType.EdgeAgentOnKubernetes);
|
const hasKubeEndpoint = hasType(EnvironmentType.EdgeAgentOnKubernetes);
|
||||||
const hasDockerEndpoint = hasType(EnvironmentType.EdgeAgentOnDocker);
|
const hasDockerEndpoint = hasType(EnvironmentType.EdgeAgentOnDocker);
|
||||||
|
|
|
@ -46,7 +46,7 @@ import { getDefaultStaggerConfig } from '../../components/StaggerFieldset.types'
|
||||||
|
|
||||||
import { PrivateRegistryFieldsetWrapper } from './PrivateRegistryFieldsetWrapper';
|
import { PrivateRegistryFieldsetWrapper } from './PrivateRegistryFieldsetWrapper';
|
||||||
import { FormValues } from './types';
|
import { FormValues } from './types';
|
||||||
import { useValidateEnvironmentTypes } from './useEdgeGroupHasType';
|
import { useEdgeGroupHasType } from './useEdgeGroupHasType';
|
||||||
import { useStaggerUpdateStatus } from './useStaggerUpdateStatus';
|
import { useStaggerUpdateStatus } from './useStaggerUpdateStatus';
|
||||||
import { useUpdateEdgeStackMutation } from './useUpdateEdgeStackMutation';
|
import { useUpdateEdgeStackMutation } from './useUpdateEdgeStackMutation';
|
||||||
import { ComposeForm } from './ComposeForm';
|
import { ComposeForm } from './ComposeForm';
|
||||||
|
@ -194,7 +194,7 @@ function InnerForm({
|
||||||
usePreventExit(initialValues.content, values.content, !isSaved);
|
usePreventExit(initialValues.content, values.content, !isSaved);
|
||||||
|
|
||||||
const { getCachedContent, setContentCache } = useCachedContent();
|
const { getCachedContent, setContentCache } = useCachedContent();
|
||||||
const { hasType } = useValidateEnvironmentTypes(values.edgeGroups);
|
const { hasType } = useEdgeGroupHasType(values.edgeGroups);
|
||||||
const staggerUpdateStatus = useStaggerUpdateStatus(edgeStack.Id);
|
const staggerUpdateStatus = useStaggerUpdateStatus(edgeStack.Id);
|
||||||
const [selectedVersion, setSelectedVersion] = useState(versionOptions?.[0]);
|
const [selectedVersion, setSelectedVersion] = useState(versionOptions?.[0]);
|
||||||
const selectedParallelOption =
|
const selectedParallelOption =
|
||||||
|
|
|
@ -5,22 +5,40 @@ import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||||
import { EnvironmentType } from '@/react/portainer/environments/types';
|
import { EnvironmentType } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
export function useValidateEnvironmentTypes(groupIds: Array<EdgeGroup['Id']>) {
|
export function useEdgeGroupHasType(groupIds: Array<EdgeGroup['Id']>) {
|
||||||
const edgeGroupsQuery = useEdgeGroups();
|
const edgeGroupsQuery = useEdgeGroups();
|
||||||
|
|
||||||
const edgeGroups = edgeGroupsQuery.data || [];
|
const edgeGroups = edgeGroupsQuery.data;
|
||||||
|
|
||||||
const modelEdgeGroups = _.compact(
|
const hasTypeFunction = createHasEnvironmentTypeFunction(
|
||||||
groupIds.map((id) => edgeGroups.find((e) => e.Id === id))
|
groupIds,
|
||||||
|
edgeGroups
|
||||||
);
|
);
|
||||||
const endpointTypes = modelEdgeGroups.flatMap((group) => group.EndpointTypes);
|
|
||||||
|
|
||||||
const hasType = useCallback(
|
const hasType = useCallback(
|
||||||
(type: EnvironmentType) => endpointTypes.includes(type),
|
(type: EnvironmentType) => hasTypeFunction(type),
|
||||||
[endpointTypes]
|
[hasTypeFunction]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasType,
|
hasType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if any of the edge groups have the given type
|
||||||
|
*/
|
||||||
|
export function createHasEnvironmentTypeFunction(
|
||||||
|
groupIds: EdgeGroup['Id'][],
|
||||||
|
edgeGroups?: EdgeGroup[]
|
||||||
|
) {
|
||||||
|
const modelEdgeGroups = _.compact(
|
||||||
|
groupIds.map((id) => edgeGroups?.find((e) => e.Id === id))
|
||||||
|
);
|
||||||
|
const endpointTypes = modelEdgeGroups.flatMap((group) => group.EndpointTypes);
|
||||||
|
|
||||||
|
function hasType(type: EnvironmentType) {
|
||||||
|
return endpointTypes.includes(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasType;
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ interface Props {
|
||||||
hasDockerEndpoint: boolean;
|
hasDockerEndpoint: boolean;
|
||||||
hasKubeEndpoint: boolean;
|
hasKubeEndpoint: boolean;
|
||||||
allowKubeToSelectCompose?: boolean;
|
allowKubeToSelectCompose?: boolean;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EdgeStackDeploymentTypeSelector({
|
export function EdgeStackDeploymentTypeSelector({
|
||||||
|
@ -21,6 +22,7 @@ export function EdgeStackDeploymentTypeSelector({
|
||||||
hasDockerEndpoint,
|
hasDockerEndpoint,
|
||||||
hasKubeEndpoint,
|
hasKubeEndpoint,
|
||||||
allowKubeToSelectCompose,
|
allowKubeToSelectCompose,
|
||||||
|
error,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const deploymentOptions: BoxSelectorOption<DeploymentType>[] = [
|
const deploymentOptions: BoxSelectorOption<DeploymentType>[] = [
|
||||||
{
|
{
|
||||||
|
@ -52,6 +54,7 @@ export function EdgeStackDeploymentTypeSelector({
|
||||||
value={value}
|
value={value}
|
||||||
options={deploymentOptions}
|
options={deploymentOptions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
error={error}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -26,11 +26,11 @@ export function HelmRepositoryDatatableActions({ selectedItems }: Props) {
|
||||||
'repository',
|
'repository',
|
||||||
'repositories'
|
'repositories'
|
||||||
)}?`}
|
)}?`}
|
||||||
data-cy="credentials-deleteButton"
|
data-cy="helmRepository-deleteButton"
|
||||||
/>
|
/>
|
||||||
<AddButton
|
<AddButton
|
||||||
to="portainer.account.createHelmRepository"
|
to="portainer.account.createHelmRepository"
|
||||||
data-cy="credentials-addButton"
|
data-cy="helmRepository-addButton"
|
||||||
>
|
>
|
||||||
Add Helm repository
|
Add Helm repository
|
||||||
</AddButton>
|
</AddButton>
|
||||||
|
|
|
@ -6,9 +6,12 @@ import { AppTemplate } from '../types';
|
||||||
|
|
||||||
import { buildUrl } from './build-url';
|
import { buildUrl } from './build-url';
|
||||||
|
|
||||||
export function useFetchTemplateFile(id?: AppTemplate['id']) {
|
export function useAppTemplateFile(
|
||||||
|
id?: AppTemplate['id'],
|
||||||
|
{ enabled }: { enabled?: boolean } = {}
|
||||||
|
) {
|
||||||
return useQuery(['templates', id, 'file'], () => fetchFilePreview(id!), {
|
return useQuery(['templates', id, 'file'], () => fetchFilePreview(id!), {
|
||||||
enabled: !!id,
|
enabled: !!id && enabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,11 @@ import { TemplateViewModel } from '../view-model';
|
||||||
|
|
||||||
import { buildUrl } from './build-url';
|
import { buildUrl } from './build-url';
|
||||||
|
|
||||||
|
export type AppTemplatesResponse = {
|
||||||
|
version: string;
|
||||||
|
templates: Array<AppTemplate>;
|
||||||
|
};
|
||||||
|
|
||||||
export function useAppTemplates<T = Array<TemplateViewModel>>({
|
export function useAppTemplates<T = Array<TemplateViewModel>>({
|
||||||
environmentId,
|
environmentId,
|
||||||
select,
|
select,
|
||||||
|
@ -43,15 +48,10 @@ export function useAppTemplate(
|
||||||
id: AppTemplate['id'] | undefined,
|
id: AppTemplate['id'] | undefined,
|
||||||
{ enabled }: { enabled?: boolean } = {}
|
{ enabled }: { enabled?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
const templateListQuery = useAppTemplates({ enabled: !!id && enabled });
|
return useAppTemplates({
|
||||||
|
enabled: !!id && enabled,
|
||||||
const template = templateListQuery.data?.find((t) => t.Id === id);
|
select: (templates) => templates.find((t) => t.Id === id),
|
||||||
|
});
|
||||||
return {
|
|
||||||
data: template,
|
|
||||||
isLoading: templateListQuery.isInitialLoading,
|
|
||||||
error: templateListQuery.error,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTemplatesWithRegistry(
|
async function getTemplatesWithRegistry(
|
||||||
|
@ -75,10 +75,7 @@ async function getTemplatesWithRegistry(
|
||||||
|
|
||||||
export async function getAppTemplates() {
|
export async function getAppTemplates() {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<{
|
const { data } = await axios.get<AppTemplatesResponse>(buildUrl());
|
||||||
version: string;
|
|
||||||
templates: Array<AppTemplate>;
|
|
||||||
}>(buildUrl());
|
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw parseAxiosError(err);
|
throw parseAxiosError(err);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
import { StackType } from '@/react/common/stacks/types';
|
import { StackType } from '@/react/common/stacks/types';
|
||||||
|
|
||||||
import { Platform } from '../../types';
|
import { Platform } from '../../types';
|
||||||
import { useFetchTemplateFile } from '../../app-templates/queries/useFetchTemplateFile';
|
import { useAppTemplateFile } from '../../app-templates/queries/useAppTemplateFile';
|
||||||
import { getDefaultEdgeTemplateSettings } from '../types';
|
import { getDefaultEdgeTemplateSettings } from '../types';
|
||||||
|
|
||||||
import { FormValues, Method } from './types';
|
import { FormValues, Method } from './types';
|
||||||
|
@ -31,7 +31,7 @@ export function useInitialValues({
|
||||||
params: { fileContent = '' },
|
params: { fileContent = '' },
|
||||||
} = useCurrentStateAndParams();
|
} = useCurrentStateAndParams();
|
||||||
|
|
||||||
const fileContentQuery = useFetchTemplateFile(appTemplateId);
|
const fileContentQuery = useAppTemplateFile(appTemplateId);
|
||||||
if (fileContentQuery.isInitialLoading) {
|
if (fileContentQuery.isInitialLoading) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
3
app/setup-tests/setup-rtl.ts
Normal file
3
app/setup-tests/setup-rtl.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { configure } from '@testing-library/react';
|
||||||
|
|
||||||
|
configure({ testIdAttribute: 'data-cy' });
|
|
@ -7,7 +7,7 @@ export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: ['./app/setup-tests/setup-msw.ts', './app/setup-tests/stub-modules.ts', './app/setup-tests/setup.ts'],
|
setupFiles: ['./app/setup-tests/setup-msw.ts', './app/setup-tests/stub-modules.ts', './app/setup-tests/setup.ts', './app/setup-tests/setup-rtl.ts'],
|
||||||
coverage: {
|
coverage: {
|
||||||
reporter: ['text', 'html'],
|
reporter: ['text', 'html'],
|
||||||
exclude: ['node_modules/', 'app/setup-tests/global-setup.js'],
|
exclude: ['node_modules/', 'app/setup-tests/global-setup.js'],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue