refactor(custom-templates): migrate list view to react [EE-2256] (#11611)

pull/11567/head^2
Chaim Lev-Ari 2024-05-30 12:04:28 +03:00 committed by GitHub
parent 5c6c66f010
commit 94c91035a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 200 additions and 617 deletions

View File

@ -1,17 +1,6 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { StackFromCustomTemplateFormWidget } from '@/react/docker/templates/StackFromCustomTemplateFormWidget';
export const templatesModule = angular
.module('portainer.docker.react.components.templates', [])
.component(
'stackFromCustomTemplateFormWidget',
r2a(withUIRouter(withCurrentUser(StackFromCustomTemplateFormWidget)), [
'template',
'unselect',
])
).name;
export const templatesModule = angular.module(
'portainer.docker.react.components.templates',
[]
).name;

View File

@ -166,7 +166,7 @@ angular
url: '/custom',
views: {
'content@': {
component: 'edgeCustomTemplatesView',
component: 'customTemplatesView',
},
},
data: {

View File

@ -1,13 +1,6 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { ListView } from '@/react/edge/templates/custom-templates/ListView';
export const templatesModule = angular
.module('portainer.edge.react.views.templates', [])
.component(
'edgeCustomTemplatesView',
r2a(withCurrentUser(withUIRouter(ListView)), [])
).name;
export const templatesModule = angular.module(
'portainer.edge.react.views.templates',
[]
).name;

View File

@ -1,8 +1,6 @@
import angular from 'angular';
import { kubeCustomTemplatesView } from './kube-custom-templates-view';
export default angular.module('portainer.kubernetes.custom-templates', []).config(config).component('kubeCustomTemplatesView', kubeCustomTemplatesView).name;
export default angular.module('portainer.kubernetes.custom-templates', []).config(config).name;
function config($stateRegistryProvider) {
const templates = {
@ -17,7 +15,7 @@ function config($stateRegistryProvider) {
views: {
'content@': {
component: 'kubeCustomTemplatesView',
component: 'customTemplatesView',
},
},
data: {

View File

@ -1,6 +0,0 @@
import controller from './kube-custom-templates-view.controller.js';
export const kubeCustomTemplatesView = {
templateUrl: './kube-custom-templates-view.html',
controller,
};

View File

@ -1,83 +0,0 @@
import _ from 'lodash-es';
import { confirmDelete } from '@@/modals/confirm';
export default class KubeCustomTemplatesViewController {
/* @ngInject */
constructor($async, $state, Authentication, CustomTemplateService, FormValidator, Notifications) {
Object.assign(this, { $async, $state, Authentication, CustomTemplateService, FormValidator, Notifications });
this.state = {
selectedTemplate: null,
formValidationError: '',
actionInProgress: false,
};
this.currentUser = {
isAdmin: false,
id: null,
};
this.isEditAllowed = this.isEditAllowed.bind(this);
this.getTemplates = this.getTemplates.bind(this);
this.validateForm = this.validateForm.bind(this);
this.confirmDelete = this.confirmDelete.bind(this);
this.selectTemplate = this.selectTemplate.bind(this);
}
selectTemplate(templateId) {
this.$state.go('kubernetes.deploy', { templateId });
}
isEditAllowed(template) {
// todo - check if current user is admin/endpointadmin/owner
return this.currentUser.isAdmin || this.currentUser.id === template.CreatedByUserId;
}
getTemplates() {
return this.$async(async () => {
try {
const templates = await this.CustomTemplateService.customTemplates(3);
this.templates = templates.filter((t) => !t.EdgeTemplate);
} catch (err) {
this.Notifications.error('Failed loading templates', err, 'Unable to load custom templates');
}
});
}
validateForm(accessControlData, isAdmin) {
this.state.formValidationError = '';
const error = this.FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
this.state.formValidationError = error;
return false;
}
return true;
}
confirmDelete(templateId) {
return this.$async(async () => {
const confirmed = await confirmDelete('Are you sure that you want to delete this template?');
if (!confirmed) {
return;
}
try {
var template = _.find(this.templates, { Id: templateId });
await this.CustomTemplateService.remove(templateId);
this.Notifications.success('Template successfully deleted', template && template.Title);
this.templates = this.templates.filter((template) => template.Id !== templateId);
} catch (err) {
this.Notifications.error('Failure', err, 'Failed to delete template');
}
});
}
$onInit() {
this.getTemplates();
this.currentUser.isAdmin = this.Authentication.isAdmin();
const user = this.Authentication.getUserDetails();
this.currentUser.id = user.ID;
}
}

View File

@ -1,9 +0,0 @@
<page-header title="'Custom Templates'" breadcrumbs="['Custom Templates']" reload="true"></page-header>
<custom-templates-list
templates="$ctrl.templates"
on-select="($ctrl.selectTemplate)"
on-delete="($ctrl.confirmDelete)"
selected-id="$ctrl.state.selectedTemplate.Id"
storage-key="'kube-custom-templates'"
></custom-templates-list>

View File

@ -4,16 +4,6 @@ import { r2a } from '@/react-tools/react2angular';
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
import { CustomTemplatesVariablesField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
import { withControlledInput } from '@/react-tools/withControlledInput';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { withUIRouter } from '@/react-tools/withUIRouter';
import {
CommonFields,
validation as commonFieldsValidation,
} from '@/react/portainer/custom-templates/components/CommonFields';
import { PlatformField } from '@/react/portainer/custom-templates/components/PlatformSelector';
import { TemplateTypeSelector } from '@/react/portainer/custom-templates/components/TemplateTypeSelector';
import { withFormValidation } from '@/react-tools/withFormValidation';
import { CustomTemplatesList } from '@/react/portainer/templates/custom-templates/ListView/CustomTemplatesList';
import { VariablesFieldAngular } from './variables-field';
@ -37,33 +27,6 @@ export const ngModule = angular
'errors',
'isVariablesNamesFromParent',
])
)
.component(
'customTemplatesList',
r2a(withUIRouter(withCurrentUser(CustomTemplatesList)), [
'onDelete',
'onSelect',
'templates',
'selectedId',
'templateLinkParams',
'storageKey',
])
)
.component(
'customTemplatesPlatformSelector',
r2a(PlatformField, ['onChange', 'value'])
)
.component(
'customTemplatesTypeSelector',
r2a(TemplateTypeSelector, ['onChange', 'value'])
);
withFormValidation(
ngModule,
withControlledInput(CommonFields, { values: 'onChange' }),
'customTemplatesCommonFields',
[],
commonFieldsValidation
);
export const customTemplatesModule = ngModule.name;

View File

@ -6,6 +6,7 @@ import { withUIRouter } from '@/react-tools/withUIRouter';
import { CreateView } from '@/react/portainer/templates/custom-templates/CreateView';
import { EditView } from '@/react/portainer/templates/custom-templates/EditView';
import { AppTemplatesView } from '@/react/portainer/templates/app-templates/AppTemplatesView';
import { ListView } from '@/react/portainer/templates/custom-templates/ListView/ListView';
export const templatesModule = angular
.module('portainer.app.react.views.templates', [])
@ -13,6 +14,10 @@ export const templatesModule = angular
'appTemplatesView',
r2a(withCurrentUser(withUIRouter(AppTemplatesView)), [])
)
.component(
'customTemplatesView',
r2a(withCurrentUser(withUIRouter(ListView)), [])
)
.component(
'createCustomTemplatesView',
r2a(withCurrentUser(withUIRouter(CreateView)), [])

View File

@ -1,15 +0,0 @@
<page-header title="'Custom Templates'" breadcrumbs="['Custom Templates']" reload="true"> </page-header>
<stack-from-custom-template-form-widget
ng-if="$ctrl.state.selectedTemplate"
template="$ctrl.state.selectedTemplate"
unselect="$ctrl.unselectTemplate"
></stack-from-custom-template-form-widget>
<custom-templates-list
templates="$ctrl.templates"
on-select="($ctrl.selectTemplate)"
on-delete="($ctrl.confirmDelete)"
selected-id="$ctrl.state.selectedTemplate.Id"
storage-key="'docker-custom-templates'"
></custom-templates-list>

View File

@ -1,305 +0,0 @@
import _ from 'lodash-es';
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { isTemplateVariablesEnabled, renderTemplate } from '@/react/portainer/custom-templates/components/utils';
import { confirmDelete } from '@@/modals/confirm';
import { getVariablesFieldDefaultValues } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/react/portainer/custom-templates/components/CommonFields';
class CustomTemplatesViewController {
/* @ngInject */
constructor(
$anchorScroll,
$async,
$rootScope,
$state,
Authentication,
CustomTemplateService,
FormValidator,
NetworkService,
Notifications,
ResourceControlService,
StackService,
StateManager
) {
this.$anchorScroll = $anchorScroll;
this.$async = $async;
this.$rootScope = $rootScope;
this.$state = $state;
this.Authentication = Authentication;
this.CustomTemplateService = CustomTemplateService;
this.FormValidator = FormValidator;
this.NetworkService = NetworkService;
this.Notifications = Notifications;
this.ResourceControlService = ResourceControlService;
this.StateManager = StateManager;
this.StackService = StackService;
this.isTemplateVariablesEnabled = isTemplateVariablesEnabled;
this.DOCKER_STANDALONE = 'DOCKER_STANDALONE';
this.DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
this.state = {
selectedTemplate: null,
showAdvancedOptions: false,
formValidationError: '',
actionInProgress: false,
deployable: false,
templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX,
templateContent: '',
templateLoadFailed: false,
};
this.currentUser = {
isAdmin: false,
id: null,
};
this.formValues = {
network: '',
name: '',
fileContent: '',
AccessControlData: new AccessControlFormData(),
variables: [],
};
this.getTemplates = this.getTemplates.bind(this);
this.getTemplatesAsync = this.getTemplatesAsync.bind(this);
this.removeTemplates = this.removeTemplates.bind(this);
this.removeTemplatesAsync = this.removeTemplatesAsync.bind(this);
this.validateForm = this.validateForm.bind(this);
this.createStack = this.createStack.bind(this);
this.createStackAsync = this.createStackAsync.bind(this);
this.selectTemplate = this.selectTemplate.bind(this);
this.selectTemplateAsync = this.selectTemplateAsync.bind(this);
this.unselectTemplate = this.unselectTemplate.bind(this);
this.unselectTemplateAsync = this.unselectTemplateAsync.bind(this);
this.getNetworks = this.getNetworks.bind(this);
this.getNetworksAsync = this.getNetworksAsync.bind(this);
this.confirmDelete = this.confirmDelete.bind(this);
this.confirmDeleteAsync = this.confirmDeleteAsync.bind(this);
this.editorUpdate = this.editorUpdate.bind(this);
this.isEditAllowed = this.isEditAllowed.bind(this);
this.onChangeFormValues = this.onChangeFormValues.bind(this);
this.onChangeTemplateVariables = this.onChangeTemplateVariables.bind(this);
}
isEditAllowed(template) {
return this.currentUser.isAdmin || this.currentUser.id === template.CreatedByUserId;
}
getTemplates() {
return this.$async(this.getTemplatesAsync);
}
async getTemplatesAsync() {
try {
const templates = await this.CustomTemplateService.customTemplates([1, 2]);
this.templates = templates.filter((t) => !t.EdgeTemplate);
} catch (err) {
this.Notifications.error('Failed loading templates', err, 'Unable to load custom templates');
}
}
removeTemplates(templates) {
return this.$async(this.removeTemplatesAsync, templates);
}
async removeTemplatesAsync(templates) {
for (let template of templates) {
try {
await this.CustomTemplateService.remove(template.id);
this.Notifications.success('Success', 'Removed template successfully');
_.remove(this.templates, template);
} catch (err) {
this.Notifications.error('Failed removing template', err, 'Unable to remove custom template');
}
}
}
onChangeTemplateVariables(variables) {
this.onChangeFormValues({ variables });
this.renderTemplate();
}
renderTemplate() {
if (!this.isTemplateVariablesEnabled) {
return;
}
const fileContent = renderTemplate(this.state.templateContent, this.formValues.variables, this.state.selectedTemplate.Variables);
this.onChangeFormValues({ fileContent });
}
onChangeFormValues(values) {
this.formValues = {
...this.formValues,
...values,
};
}
validateForm(accessControlData, isAdmin) {
this.state.formValidationError = '';
const error = this.FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
this.state.formValidationError = error;
return false;
}
return true;
}
createStack() {
return this.$async(this.createStackAsync);
}
async createStackAsync() {
const userId = this.currentUser.id;
const accessControlData = this.formValues.AccessControlData;
if (!this.validateForm(accessControlData, this.currentUser.isAdmin)) {
return;
}
const stackName = this.formValues.name;
const endpointId = this.endpoint.Id;
this.state.actionInProgress = true;
try {
const file = this.formValues.fileContent;
const createAction = this.state.selectedTemplate.Type === 1 ? this.StackService.createSwarmStackFromFileContent : this.StackService.createComposeStackFromFileContent;
const { ResourceControl: resourceControl } = await createAction(stackName, file, [], endpointId);
await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
this.Notifications.success('Success', 'Stack successfully deployed');
this.$state.go('docker.stacks');
} catch (err) {
this.Notifications.error('Deployment error', err, 'Failed to deploy stack');
} finally {
this.state.actionInProgress = false;
}
}
unselectTemplate() {
// wrapping unselect with async to make a digest cycle run between unselect to select
return this.$async(this.unselectTemplateAsync);
}
async unselectTemplateAsync() {
this.state.selectedTemplate = null;
this.formValues = {
network: '',
name: '',
fileContent: '',
AccessControlData: new AccessControlFormData(),
variables: [],
};
}
selectTemplate(templateId) {
return this.$async(this.selectTemplateAsync, templateId);
}
async selectTemplateAsync(templateId) {
if (this.state.selectedTemplate) {
await this.unselectTemplate(this.state.selectedTemplate);
}
const template = _.find(this.templates, { Id: templateId });
const isGit = template.GitConfig !== null;
this.state.isEditorReadOnly = isGit;
try {
this.state.templateContent = this.formValues.fileContent = await this.CustomTemplateService.customTemplateFile(template.Id, template.GitConfig !== null);
} catch (err) {
this.state.templateLoadFailed = true;
this.Notifications.error('Failure', err, 'Unable to retrieve custom template data');
}
this.formValues.network = _.find(this.availableNetworks, function (o) {
return o.Name === 'bridge';
});
this.formValues.name = template.Title ? template.Title : '';
this.state.selectedTemplate = template;
this.$anchorScroll('view-top');
const applicationState = this.StateManager.getState();
this.state.deployable = this.isDeployable(applicationState.endpoint, template.Type);
if (template.Variables && template.Variables.length > 0) {
const variables = getVariablesFieldDefaultValues(template.Variables);
this.onChangeTemplateVariables(variables);
}
window.scrollTo({ top: 0, behavior: 'smooth' });
}
getNetworks(provider, apiVersion) {
return this.$async(this.getNetworksAsync, provider, apiVersion);
}
async getNetworksAsync(provider, apiVersion) {
try {
const networks = await this.NetworkService.networks(
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
false,
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
);
this.availableNetworks = networks;
} catch (err) {
this.Notifications.error('Failure', err, 'Failed to load networks.');
}
}
confirmDelete(templateId) {
return this.$async(this.confirmDeleteAsync, templateId);
}
async confirmDeleteAsync(templateId) {
const confirmed = await confirmDelete('Are you sure that you want to delete this template?');
if (!confirmed) {
return;
}
try {
var template = _.find(this.templates, { Id: templateId });
await this.CustomTemplateService.remove(templateId);
this.Notifications.success('Template successfully deleted', template && template.Title);
this.templates = this.templates.filter((template) => template.Id !== templateId);
} catch (err) {
this.Notifications.error('Failure', err, 'Failed to delete template');
}
}
editorUpdate(value) {
this.formValues.fileContent = value;
}
isDeployable(endpoint, templateType) {
let deployable = false;
switch (templateType) {
case 1:
deployable = endpoint.mode.provider === this.DOCKER_SWARM_MODE;
break;
case 2:
deployable = endpoint.mode.provider === this.DOCKER_STANDALONE;
break;
}
return deployable;
}
$onInit() {
const applicationState = this.StateManager.getState();
const {
endpoint: { mode: endpointMode },
apiVersion,
} = applicationState;
this.getTemplates();
this.getNetworks(endpointMode.provider, apiVersion);
this.currentUser.isAdmin = this.Authentication.isAdmin();
const user = this.Authentication.getUserDetails();
this.currentUser.id = user.ID;
}
}
export default CustomTemplatesViewController;

View File

@ -1,9 +0,0 @@
import CustomTemplatesViewController from './customTemplatesViewController.js';
angular.module('portainer.app').component('customTemplatesView', {
templateUrl: './customTemplatesView.html',
controller: CustomTemplatesViewController,
bindings: {
endpoint: '<',
},
});

View File

@ -25,17 +25,18 @@ export function FormActions({
<FormSection title="Actions">
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
className="!ml-0"
loadingText={loadingText}
isLoading={isLoading}
disabled={!isValid}
data-cy={dataCy}
>
{submitLabel}
</LoadingButton>
{children}
<div className="flex item-center gap-3">
<LoadingButton
className="!ml-0"
loadingText={loadingText}
isLoading={isLoading}
disabled={!isValid}
data-cy={dataCy}
>
{submitLabel}
</LoadingButton>
{children}
</div>
</div>
</div>
</FormSection>

View File

@ -1,47 +0,0 @@
import { notifySuccess } from '@/portainer/services/notifications';
import { useCustomTemplates } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplates';
import { useDeleteTemplateMutation } from '@/react/portainer/templates/custom-templates/queries/useDeleteTemplateMutation';
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
import { CustomTemplatesList } from '@/react/portainer/templates/custom-templates/ListView/CustomTemplatesList';
import { PageHeader } from '@@/PageHeader';
import { confirmDelete } from '@@/modals/confirm';
export function ListView() {
const templatesQuery = useCustomTemplates({
params: {
edge: true,
},
});
const deleteMutation = useDeleteTemplateMutation();
return (
<>
<PageHeader title="Custom Templates" breadcrumbs="Custom Templates" />
<CustomTemplatesList
templates={templatesQuery.data}
onDelete={handleDelete}
templateLinkParams={(template) => ({
to: 'edge.stacks.new',
params: { templateId: template.Id, templateType: 'custom' },
})}
storageKey="edge-custom-templates"
/>
</>
);
async function handleDelete(templateId: CustomTemplate['Id']) {
if (
!(await confirmDelete('Are you sure you want to delete this template?'))
) {
return;
}
deleteMutation.mutate(templateId, {
onSuccess: () => {
notifySuccess('Success', 'Template deleted');
},
});
}
}

View File

@ -1 +0,0 @@
export { ListView } from './ListView';

View File

@ -1,35 +0,0 @@
import { transformGitAuthenticationViewModel } from '@/react/portainer/gitops/AuthFieldset/utils';
import { GitFormModel } from '@/react/portainer/gitops/types';
export function toGitRequest(
gitConfig: GitFormModel,
credentialId: number | undefined
): GitFormModel {
return {
...gitConfig,
...getGitAuthValues(gitConfig, credentialId),
};
}
function getGitAuthValues(
gitConfig: GitFormModel | undefined,
credentialId: number | undefined
) {
if (!credentialId) {
return gitConfig;
}
const authModel = transformGitAuthenticationViewModel({
...gitConfig,
RepositoryGitCredentialID: credentialId,
});
return authModel
? {
RepositoryAuthentication: true,
RepositoryGitCredentialID: authModel.GitCredentialID,
RepositoryPassword: authModel.Password,
RepositoryUsername: authModel.Username,
}
: {};
}

View File

@ -15,20 +15,20 @@ import { CustomTemplatesListItem } from './CustomTemplatesListItem';
export function CustomTemplatesList({
templates,
onSelect,
onDelete,
selectedId,
templateLinkParams,
storageKey,
}: {
templates?: CustomTemplate[];
onSelect?: (template: CustomTemplate['Id']) => void;
onDelete: (template: CustomTemplate['Id']) => void;
onDelete: (templateId: CustomTemplate['Id']) => void;
selectedId?: CustomTemplate['Id'];
templateLinkParams?: (template: CustomTemplate) => {
to: string;
params: object;
};
templateLinkParams?: (template: CustomTemplate) =>
| {
to: string;
params: object;
}
| undefined;
storageKey: string;
}) {
const [page, setPage] = useState(0);
@ -68,7 +68,6 @@ export function CustomTemplatesList({
<CustomTemplatesListItem
key={template.Id}
template={template}
onSelect={onSelect}
isSelected={template.Id === selectedId}
onDelete={onDelete}
linkParams={templateLinkParams?.(template)}

View File

@ -0,0 +1,58 @@
import { notifySuccess } from '@/portainer/services/notifications';
import { useParamState } from '@/react/hooks/useParamState';
import { PageHeader } from '@@/PageHeader';
import { confirmDelete } from '@@/modals/confirm';
import { useCustomTemplates } from '../queries/useCustomTemplates';
import { useDeleteTemplateMutation } from '../queries/useDeleteTemplateMutation';
import { CustomTemplate } from '../types';
import { StackFromCustomTemplateFormWidget } from './StackFromCustomTemplateFormWidget';
import { CustomTemplatesList } from './CustomTemplatesList';
import { useViewParams } from './useViewParams';
export function ListView() {
const { params, getTemplateLinkParams, storageKey, viewType } =
useViewParams();
const templatesQuery = useCustomTemplates({
params,
});
const deleteMutation = useDeleteTemplateMutation();
const [selectedTemplateId] = useParamState<number>('template', (param) =>
param ? parseInt(param, 10) : 0
);
return (
<>
<PageHeader title="Custom Templates" breadcrumbs="Custom Templates" />
{viewType === 'docker' && !!selectedTemplateId && (
<StackFromCustomTemplateFormWidget templateId={selectedTemplateId} />
)}
<CustomTemplatesList
templates={templatesQuery.data}
onDelete={handleDelete}
templateLinkParams={getTemplateLinkParams}
storageKey={storageKey}
selectedId={selectedTemplateId}
/>
</>
);
async function handleDelete(templateId: CustomTemplate['Id']) {
if (
!(await confirmDelete('Are you sure you want to delete this template?'))
) {
return;
}
deleteMutation.mutate(templateId, {
onSuccess: () => {
notifySuccess('Success', 'Template deleted');
},
});
}
}

View File

@ -23,26 +23,24 @@ import {
import { StackType } from '@/react/common/stacks/types';
import { toGitFormModel } from '@/react/portainer/gitops/types';
import { AdvancedSettings } from '@/react/portainer/templates/app-templates/DeployFormWidget/AdvancedSettings';
import { useSwarmId } from '@/react/docker/proxy/queries/useSwarm';
import { Button } from '@@/buttons';
import { FormActions } from '@@/form-components/FormActions';
import { FormSection } from '@@/form-components/FormSection';
import { WebEditorForm } from '@@/WebEditorForm';
import { useSwarmId } from '../../proxy/queries/useSwarm';
import { Link } from '@@/Link';
import { FormValues } from './types';
import { useValidation } from './useValidation';
export function DeployForm({
template,
unselect,
templateFile,
isDeployable,
}: {
template: CustomTemplate;
templateFile: string;
unselect: () => void;
isDeployable: boolean;
}) {
const router = useRouter();
@ -157,7 +155,12 @@ export function DeployForm({
>
<Button
type="reset"
onClick={() => unselect()}
as={Link}
props={{
to: '.',
'data-cy': 'cancel-stack-creation',
params: { template: null },
}}
color="default"
data-cy="cancel-stack-creation"
>

View File

@ -1,6 +1,7 @@
import { DeployWidget } from '@/react/portainer/templates/components/DeployWidget';
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
import { useCustomTemplateFile } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplateFile';
import { useCustomTemplate } from '@/react/portainer/templates/custom-templates/queries/useCustomTemplate';
import { TextTip } from '@@/Tip/TextTip';
@ -9,19 +10,21 @@ import { DeployForm } from './DeployForm';
import { TemplateLoadError } from './TemplateLoadError';
export function StackFromCustomTemplateFormWidget({
template,
unselect,
templateId,
}: {
template: CustomTemplate;
unselect: () => void;
templateId: CustomTemplate['Id'];
}) {
const isDeployable = useIsDeployable(template.Type);
const fileQuery = useCustomTemplateFile(template.Id);
const templateQuery = useCustomTemplate(templateId);
if (fileQuery.isLoading) {
const isDeployable = useIsDeployable(templateQuery.data?.Type);
const fileQuery = useCustomTemplateFile(templateId);
if (fileQuery.isLoading || !templateQuery.data) {
return null;
}
const template = templateQuery.data;
return (
<DeployWidget
logo={template.Logo}
@ -44,10 +47,10 @@ export function StackFromCustomTemplateFormWidget({
</div>
</div>
)}
{fileQuery.isSuccess && isDeployable && (
<DeployForm
template={template}
unselect={unselect}
templateFile={fileQuery.data}
isDeployable={isDeployable}
/>

View File

@ -1,9 +1,8 @@
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { StackType } from '@/react/common/stacks/types';
import { useIsSwarm } from '@/react/docker/proxy/queries/useInfo';
import { useIsSwarm } from '../../proxy/queries/useInfo';
export function useIsDeployable(type: StackType) {
export function useIsDeployable(type: StackType | undefined) {
const environmentId = useEnvironmentId();
const isSwarm = useIsSwarm(environmentId);

View File

@ -0,0 +1,73 @@
import { StackType } from '@/react/common/stacks/types';
import { useAuthorizations } from '@/react/hooks/useUser';
import { CustomTemplatesListParams } from '../queries/useCustomTemplates';
import { CustomTemplate } from '../types';
import { TemplateViewType, useViewType } from '../useViewType';
export function useViewParams(): {
viewType: TemplateViewType;
params: CustomTemplatesListParams;
getTemplateLinkParams?: (template: CustomTemplate) => {
to: string;
params: object;
};
storageKey: string;
} {
const viewType = useViewType();
const isAllowedDeploymentKubeQuery = useAuthorizations(
'K8sApplicationsAdvancedDeploymentRW'
);
const isAllowedDeploymentDockerQuery = useAuthorizations([
'DockerContainerCreate',
'PortainerStackCreate',
]);
switch (viewType) {
case 'kube':
return {
viewType,
params: { edge: false, type: [StackType.Kubernetes] },
getTemplateLinkParams: isAllowedDeploymentKubeQuery.authorized
? (template: CustomTemplate) => ({
to: 'kubernetes.deploy',
params: { templateId: template.Id, templateType: 'custom' },
})
: undefined,
storageKey: 'kube-custom-templates',
};
case 'edge':
return {
viewType,
params: { edge: true },
getTemplateLinkParams: (template: CustomTemplate) => ({
to: 'edge.stacks.new',
params: { templateId: template.Id, templateType: 'custom' },
}),
storageKey: 'edge-custom-templates',
};
case 'docker':
return {
viewType,
params: {
edge: false,
type: [StackType.DockerCompose, StackType.DockerSwarm],
},
getTemplateLinkParams: isAllowedDeploymentDockerQuery.authorized
? (template: CustomTemplate) => ({
to: '.',
params: { template: template.Id },
})
: undefined,
storageKey: 'docker-custom-templates',
};
default:
return {
viewType,
params: {},
getTemplateLinkParams: undefined,
storageKey: 'custom-templates',
};
}
}

View File

@ -20,6 +20,8 @@ type Params = {
edge?: boolean;
};
export { type Params as CustomTemplatesListParams };
export function useCustomTemplates<T = Array<CustomTemplate>>({
select,
params,
@ -38,6 +40,9 @@ async function getCustomTemplates({ type, edge }: Params = {}) {
type,
edge,
},
paramsSerializer: {
indexes: null,
},
});
return data;
} catch (e) {

View File

@ -4,15 +4,19 @@ export type TemplateViewType = 'kube' | 'docker' | 'edge';
export function useViewType(): TemplateViewType {
const {
state: { name },
state: { name = '' },
} = useCurrentStateAndParams();
if (name?.includes('kubernetes')) {
if (name.includes('kubernetes')) {
return 'kube';
}
if (name?.includes('docker')) {
if (name.includes('docker')) {
return 'docker';
}
return 'edge';
if (name.includes('edge')) {
return 'edge';
}
throw new Error(`Unknown view type: ${name}`);
}