1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-09 07:45:22 +02:00

feature(helm): move helm charts inside advance deployments (create from manifest) [EE-5999] (#10395)

This commit is contained in:
Prabhat Khera 2023-10-09 11:20:44 +13:00 committed by GitHub
parent 9885694df6
commit b468070945
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 877 additions and 388 deletions

View file

@ -0,0 +1,57 @@
import { useMemo } from 'react';
import { useCurrentUser } from '@/react/hooks/useUser';
import helm from '@/assets/ico/helm.svg?c';
import { Datatable } from '@@/datatables';
import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState';
import { columns } from './columns';
import { HelmRepositoryDatatableActions } from './HelmRepositoryDatatableActions';
import { useHelmRepositories } from './helm-repositories.service';
import { HelmRepository } from './types';
const storageKey = 'helmRepository';
const settingsStore = createPersistedStore(storageKey);
export function HelmRepositoryDatatable() {
const { user } = useCurrentUser();
const helmReposQuery = useHelmRepositories(user.Id);
const tableState = useTableState(settingsStore, storageKey);
const helmRepos = useMemo(() => {
const helmRepos = [];
if (helmReposQuery.data?.GlobalRepository) {
const helmrepository: HelmRepository = {
Global: true,
URL: helmReposQuery.data.GlobalRepository,
Id: 0,
UserId: 0,
};
helmRepos.push(helmrepository);
}
return [...helmRepos, ...(helmReposQuery.data?.UserRepositories ?? [])];
}, [
helmReposQuery.data?.GlobalRepository,
helmReposQuery.data?.UserRepositories,
]);
return (
<Datatable
dataset={helmRepos}
settingsManager={tableState}
columns={columns}
title="Helm Repositories"
titleIcon={helm}
renderTableActions={(selectedRows) => (
<HelmRepositoryDatatableActions selectedItems={selectedRows} />
)}
emptyContentLabel="No Helm repository found"
isLoading={helmReposQuery.isLoading}
isRowSelectable={(row) => !row.original.Global}
/>
);
}

View file

@ -0,0 +1,60 @@
import { useRouter } from '@uirouter/react';
import { Plus, Trash2 } from 'lucide-react';
import { pluralize } from '@/portainer/helpers/strings';
import { confirmDestructive } from '@@/modals/confirm';
import { Button } from '@@/buttons';
import { HelmRepository } from './types';
import { useDeleteHelmRepositoriesMutation } from './helm-repositories.service';
interface Props {
selectedItems: HelmRepository[];
}
export function HelmRepositoryDatatableActions({ selectedItems }: Props) {
const router = useRouter();
const deleteHelmRepoMutation = useDeleteHelmRepositoriesMutation();
return (
<>
<Button
disabled={selectedItems.length < 1}
color="dangerlight"
onClick={() => onDeleteClick(selectedItems)}
data-cy="credentials-deleteButton"
icon={Trash2}
>
Remove
</Button>
<Button
onClick={() =>
router.stateService.go('portainer.account.createHelmRepository')
}
data-cy="credentials-addButton"
icon={Plus}
>
Add Helm Repository
</Button>
</>
);
async function onDeleteClick(selectedItems: HelmRepository[]) {
const confirmed = await confirmDestructive({
title: 'Confirm action',
message: `Are you sure you want to remove the selected Helm ${pluralize(
selectedItems.length,
'repository',
'repositories'
)}?`,
});
if (!confirmed) {
return;
}
deleteHelmRepoMutation.mutate(selectedItems);
}
}

View file

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { HelmRepository } from '../types';
export const columnHelper = createColumnHelper<HelmRepository>();

View file

@ -0,0 +1,3 @@
import { url } from './url';
export const columns = [url];

View file

@ -0,0 +1,3 @@
import { columnHelper } from './helper';
export const url = columnHelper.accessor('URL', { id: 'url' });

View file

@ -0,0 +1,108 @@
import { useMutation, useQuery, useQueryClient } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { success as notifySuccess } from '@/portainer/services/notifications';
import { withError } from '@/react-tools/react-query';
import { pluralize } from '@/portainer/helpers/strings';
import {
CreateHelmRepositoryPayload,
HelmRepository,
HelmRepositories,
} from './types';
export async function createHelmRepository(
helmRepository: CreateHelmRepositoryPayload
) {
try {
const { data } = await axios.post<{ helmRepository: HelmRepository }>(
buildUrl(helmRepository.UserId),
helmRepository
);
return data.helmRepository;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to create Helm repository');
}
}
export async function getHelmRepositories(userId: number) {
try {
const { data } = await axios.get<HelmRepositories>(buildUrl(userId));
return data;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to get Helm repositories');
}
}
export async function deleteHelmRepository(repo: HelmRepository) {
try {
await axios.delete<HelmRepository[]>(buildUrl(repo.UserId, repo.Id));
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to delete Helm repository');
}
}
export async function deleteHelmRepositories(repos: HelmRepository[]) {
try {
await Promise.all(repos.map((repo) => deleteHelmRepository(repo)));
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to delete Helm repositories');
}
}
export function useDeleteHelmRepositoryMutation() {
const queryClient = useQueryClient();
return useMutation(deleteHelmRepository, {
onSuccess: (_, helmRepository) => {
notifySuccess('Helm repository deleted successfully', helmRepository.URL);
return queryClient.invalidateQueries(['helmrepositories']);
},
...withError('Unable to delete Helm repository'),
});
}
export function useDeleteHelmRepositoriesMutation() {
const queryClient = useQueryClient();
return useMutation(deleteHelmRepositories, {
onSuccess: () => {
notifySuccess(
'Success',
`Helm ${pluralize(
deleteHelmRepositories.length,
'repository',
'repositories'
)} deleted successfully`
);
return queryClient.invalidateQueries(['helmrepositories']);
},
...withError('Unable to delete Helm repositories'),
});
}
export function useHelmRepositories(userId: number) {
return useQuery('helmrepositories', () => getHelmRepositories(userId), {
staleTime: 20,
...withError('Unable to retrieve Helm repositories'),
});
}
export function useCreateHelmRepositoryMutation() {
const queryClient = useQueryClient();
return useMutation(createHelmRepository, {
onSuccess: (_, payload) => {
notifySuccess('Helm repository created successfully', payload.URL);
return queryClient.invalidateQueries(['helmrepositories']);
},
...withError('Unable to create Helm repository'),
});
}
function buildUrl(userId: number, helmRepositoryId?: number) {
if (helmRepositoryId) {
return `/users/${userId}/helm/repositories/${helmRepositoryId}`;
}
return `/users/${userId}/helm/repositories`;
}

View file

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

View file

@ -0,0 +1,20 @@
export interface CreateHelmRepositoryPayload {
UserId: number;
URL: string;
}
export interface HelmRepositoryFormValues {
URL: string;
}
export interface HelmRepository {
Id: number;
UserId: number;
URL: string;
Global: boolean;
}
export interface HelmRepositories {
UserRepositories: HelmRepository[];
GlobalRepository: string;
}

View file

@ -0,0 +1,28 @@
import { PageHeader } from '@@/PageHeader';
import { Widget, WidgetBody } from '@@/Widget';
import { CreateHelmRepositoryForm } from './CreateHelmRespositoriesForm';
export function CreateHelmRepositoriesView() {
return (
<>
<PageHeader
title="Create Helm repository"
breadcrumbs={[
{ label: 'My account', link: 'portainer.account' },
{ label: 'Create Helm repository' },
]}
/>
<div className="row">
<div className="col-sm-12">
<Widget>
<WidgetBody>
<CreateHelmRepositoryForm />
</WidgetBody>
</Widget>
</div>
</div>
</>
);
}

View file

@ -0,0 +1,41 @@
import { useRouter } from '@uirouter/react';
import { useCurrentUser } from '@/react/hooks/useUser';
import {
CreateHelmRepositoryPayload,
HelmRepositoryFormValues,
} from '../../AccountView/HelmRepositoryDatatable/types';
import {
useHelmRepositories,
useCreateHelmRepositoryMutation,
} from '../../AccountView/HelmRepositoryDatatable/helm-repositories.service';
import { HelmRepositoryForm } from '../components/HelmRepositoryForm';
export function CreateHelmRepositoryForm() {
const router = useRouter();
const currentUser = useCurrentUser();
const createHelmRepositoryMutation = useCreateHelmRepositoryMutation();
const helmReposQuery = useHelmRepositories(currentUser.user.Id);
return (
<HelmRepositoryForm
isLoading={createHelmRepositoryMutation.isLoading}
onSubmit={onSubmit}
URLs={helmReposQuery.data?.UserRepositories.map((x) => x.URL) || []}
/>
);
function onSubmit(values: HelmRepositoryFormValues) {
const payload: CreateHelmRepositoryPayload = {
...values,
UserId: currentUser.user.Id,
};
createHelmRepositoryMutation.mutate(payload, {
onSuccess: () => {
router.stateService.go('portainer.account');
},
});
}
}

View file

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

View file

@ -0,0 +1,19 @@
import { object, string } from 'yup';
import { isValidUrl } from '@@/form-components/validate-url';
export function noDuplicateURLsSchema(urls: string[]) {
return string()
.required('URL is required')
.test('not existing name', 'URL is already added', (newName) =>
urls.every((name) => name !== newName)
);
}
export function validationSchema(urls: string[]) {
return object().shape({
URL: noDuplicateURLsSchema(urls)
.test('valid-url', 'Invalid URL', (value) => !value || isValidUrl(value))
.required('URL is required'),
});
}

View file

@ -0,0 +1,74 @@
import { Field, Form, Formik } from 'formik';
import { useRouter } from '@uirouter/react';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { LoadingButton } from '@@/buttons/LoadingButton';
import { Button } from '@@/buttons';
import { HelmRepositoryFormValues } from '../../AccountView/HelmRepositoryDatatable/types';
import { validationSchema } from './CreateHelmRepositoryForm.validation';
type Props = {
isEditing?: boolean;
isLoading: boolean;
onSubmit: (formValues: HelmRepositoryFormValues) => void;
URLs: string[];
};
const defaultInitialValues: HelmRepositoryFormValues = {
URL: '',
};
export function HelmRepositoryForm({
isEditing = false,
isLoading,
onSubmit,
URLs,
}: Props) {
const router = useRouter();
return (
<Formik<HelmRepositoryFormValues>
initialValues={defaultInitialValues}
enableReinitialize
validationSchema={() => validationSchema(URLs)}
onSubmit={(values) => onSubmit(values)}
validateOnMount
>
{({ values, errors, handleSubmit, isValid, dirty }) => (
<Form className="form-horizontal" onSubmit={handleSubmit} noValidate>
<FormControl inputId="url" label="URL" errors={errors.URL} required>
<Field
as={Input}
name="URL"
value={values.URL}
autoComplete="off"
id="URL"
/>
</FormControl>
<div className="form-group">
<div className="col-sm-12 mt-3">
<LoadingButton
disabled={!isValid || !dirty}
isLoading={isLoading}
loadingText="Saving Helm repository..."
>
{isEditing ? 'Update Helm repository' : 'Save Helm repository'}
</LoadingButton>
{isEditing && (
<Button
color="default"
onClick={() => router.stateService.go('portainer.account')}
>
Cancel
</Button>
)}
</div>
</div>
</Form>
)}
</Formik>
);
}

View file

@ -23,7 +23,7 @@ export function ScreenBannerFieldset() {
<SwitchField
labelClass="col-sm-3 col-lg-2"
label="Login screen banner"
checked={isEnabled}
checked
name="toggle_login_banner"
disabled={isDemoQuery.data}
onChange={(checked) => setIsEnabled(checked)}

View file

@ -12,7 +12,7 @@ export function HelmSection() {
<FormSection title="Helm Repository">
<div className="mb-2">
<TextTip color="blue">
You can specify the URL to your own helm repository here. See the{' '}
You can specify the URL to your own Helm repository here. See the{' '}
<a
href="https://helm.sh/docs/topics/chart_repository/"
target="_blank"