mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
refactor(ui): move react components to react codebase [EE-3354] (#8258)
* refactor(ui): move react components to react codebase [EE-3354] * refactor(app): move bocx selector options * refactor(react): spearate portainer components * fix(app): fix imports
This commit is contained in:
parent
f9a09301a8
commit
b98c71f1ab
66 changed files with 312 additions and 294 deletions
|
@ -1,8 +1,3 @@
|
|||
// theme icons
|
||||
import automode from '@/assets/ico/theme/auto.svg?c';
|
||||
import darkmode from '@/assets/ico/theme/darkmode.svg?c';
|
||||
import lightmode from '@/assets/ico/theme/lightmode.svg?c';
|
||||
import highcontrastmode from '@/assets/ico/theme/highcontrastmode.svg?c';
|
||||
// general icons
|
||||
import heartbeatup from '@/assets/ico/heartbeat-up.svg?c';
|
||||
import heartbeatdown from '@/assets/ico/heartbeat-down.svg?c';
|
||||
|
@ -47,10 +42,6 @@ const placeholder = Placeholder;
|
|||
export const SvgIcons = {
|
||||
heartbeatup,
|
||||
heartbeatdown,
|
||||
automode,
|
||||
darkmode,
|
||||
lightmode,
|
||||
highcontrastmode,
|
||||
dataflow,
|
||||
dockericon,
|
||||
git,
|
||||
|
|
27
app/react/docker/networks/CreateView/macvlanOptions.tsx
Normal file
27
app/react/docker/networks/CreateView/macvlanOptions.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Share2, Sliders } from 'lucide-react';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
export function getOptions(
|
||||
hasNetworks: boolean
|
||||
): ReadonlyArray<BoxSelectorOption<string>> {
|
||||
return [
|
||||
{
|
||||
id: 'network_config',
|
||||
icon: Sliders,
|
||||
iconType: 'badge',
|
||||
label: 'Configuration',
|
||||
description: 'I want to configure a network before deploying it',
|
||||
value: 'local',
|
||||
},
|
||||
{
|
||||
id: 'network_deploy',
|
||||
icon: Share2,
|
||||
iconType: 'badge',
|
||||
label: 'Creation',
|
||||
description: 'I want to create a network from a configuration',
|
||||
value: 'swarm',
|
||||
disabled: () => !hasNetworks,
|
||||
},
|
||||
] as const;
|
||||
}
|
|
@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from 'react-query';
|
|||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
import { useIntegratedLicenseInfo } from '@/portainer/license-management/use-license.service';
|
||||
import { useIntegratedLicenseInfo } from '@/react/portainer/licenses/use-license.service';
|
||||
|
||||
export function useAssociateDeviceMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
|
22
app/react/edge/edge-groups/CreateView/group-type-options.tsx
Normal file
22
app/react/edge/edge-groups/CreateView/group-type-options.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { List, Tag } from 'lucide-react';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
export const groupTypeOptions: ReadonlyArray<BoxSelectorOption<boolean>> = [
|
||||
{
|
||||
id: 'static-group',
|
||||
value: false,
|
||||
label: 'Static',
|
||||
description: 'Manually select Edge environments',
|
||||
icon: List,
|
||||
iconType: 'badge',
|
||||
},
|
||||
{
|
||||
id: 'dynamic-group',
|
||||
value: true,
|
||||
label: 'Dynamic',
|
||||
description: 'Automatically associate environments via tags',
|
||||
icon: Tag,
|
||||
iconType: 'badge',
|
||||
},
|
||||
] as const;
|
23
app/react/edge/edge-groups/CreateView/tag-options.tsx
Normal file
23
app/react/edge/edge-groups/CreateView/tag-options.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Tag } from 'lucide-react';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
export const tagOptions: ReadonlyArray<BoxSelectorOption<boolean>> = [
|
||||
{
|
||||
id: 'or-selector',
|
||||
value: true,
|
||||
label: 'Partial Match',
|
||||
description:
|
||||
'Associate any environment matching at least one of the selected tags',
|
||||
icon: Tag,
|
||||
iconType: 'badge',
|
||||
},
|
||||
{
|
||||
id: 'and-selector',
|
||||
value: false,
|
||||
label: 'Full Match',
|
||||
description: 'Associate any environment matching all of the selected tags',
|
||||
icon: Tag,
|
||||
iconType: 'badge',
|
||||
},
|
||||
];
|
22
app/react/edge/edge-jobs/CreateView/cron-method-options.tsx
Normal file
22
app/react/edge/edge-jobs/CreateView/cron-method-options.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Calendar, Edit } from 'lucide-react';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
export const cronMethodOptions: ReadonlyArray<BoxSelectorOption<string>> = [
|
||||
{
|
||||
id: 'config_basic',
|
||||
value: 'basic',
|
||||
icon: Calendar,
|
||||
iconType: 'badge',
|
||||
label: 'Basic configuration',
|
||||
description: 'Select date from calendar',
|
||||
},
|
||||
{
|
||||
id: 'config_advanced',
|
||||
value: 'advanced',
|
||||
icon: Edit,
|
||||
iconType: 'badge',
|
||||
label: 'Advanced configuration',
|
||||
description: 'Write your own cron rule',
|
||||
},
|
||||
] as const;
|
|
@ -0,0 +1,30 @@
|
|||
import { AlignJustify, Sliders } from 'lucide-react';
|
||||
|
||||
import { KubernetesApplicationPlacementTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
export const placementOptions: ReadonlyArray<BoxSelectorOption<number>> = [
|
||||
{
|
||||
id: 'placement_hard',
|
||||
value: KubernetesApplicationPlacementTypes.MANDATORY,
|
||||
icon: Sliders,
|
||||
iconType: 'badge',
|
||||
label: 'Mandatory',
|
||||
description: (
|
||||
<>
|
||||
Schedule this application <b>ONLY</b> on nodes that match <b>ALL</b>{' '}
|
||||
Rules
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'placement_soft',
|
||||
value: KubernetesApplicationPlacementTypes.PREFERRED,
|
||||
icon: AlignJustify,
|
||||
iconType: 'badge',
|
||||
label: 'Preferred',
|
||||
description:
|
||||
'Schedule this application on nodes that match the rules if possible',
|
||||
},
|
||||
] as const;
|
|
@ -4,7 +4,7 @@ import { EnvironmentId } from '@/react/portainer/environments/types';
|
|||
|
||||
export async function getIsRBACEnabled(environmentId: EnvironmentId) {
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
const { data } = await axios.get<boolean>(
|
||||
`kubernetes/${environmentId}/rbac_enabled`
|
||||
);
|
||||
return data;
|
23
app/react/kubernetes/configs/CreateView/options.tsx
Normal file
23
app/react/kubernetes/configs/CreateView/options.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { KubernetesConfigurationKinds } from 'Kubernetes/models/configuration/models';
|
||||
import { FileCode, Lock } from 'lucide-react';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
export const typeOptions: ReadonlyArray<BoxSelectorOption<number>> = [
|
||||
{
|
||||
id: 'type_basic',
|
||||
value: KubernetesConfigurationKinds.CONFIGMAP,
|
||||
icon: FileCode,
|
||||
iconType: 'badge',
|
||||
label: 'ConfigMap',
|
||||
description: 'This configuration holds non-sensitive information',
|
||||
},
|
||||
{
|
||||
id: 'type_secret',
|
||||
value: KubernetesConfigurationKinds.SECRET,
|
||||
icon: Lock,
|
||||
iconType: 'badge',
|
||||
label: 'Secret',
|
||||
description: 'This configuration holds sensitive information',
|
||||
},
|
||||
] as const;
|
|
@ -1,6 +1,7 @@
|
|||
import { server, rest } from '@/setup-tests/server';
|
||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
||||
import { LicenseType } from '@/portainer/license-management/types';
|
||||
|
||||
import { LicenseType } from '../licenses/types';
|
||||
|
||||
import { LicenseNodePanel } from './LicenseNodePanel';
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { LicenseType } from '@/portainer/license-management/types';
|
||||
import { useLicenseInfo } from '@/portainer/license-management/use-license.service';
|
||||
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { InformationPanel } from '@@/InformationPanel';
|
||||
|
||||
import { useNodesCount } from '../system/useNodesCount';
|
||||
import { useLicenseInfo } from '../licenses/use-license.service';
|
||||
import { LicenseType } from '../licenses/types';
|
||||
|
||||
export function LicenseNodePanel() {
|
||||
const nodesValid = useNodesValid();
|
||||
|
|
34
app/react/portainer/account/AccountView/theme-options.tsx
Normal file
34
app/react/portainer/account/AccountView/theme-options.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Eye, Moon, Sun, RefreshCw } from 'lucide-react';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
export const options = [
|
||||
{
|
||||
id: 'light',
|
||||
icon: <BadgeIcon icon={Sun} />,
|
||||
label: 'Light Theme',
|
||||
description: 'Default color mode',
|
||||
value: 'light',
|
||||
},
|
||||
{
|
||||
id: 'dark',
|
||||
icon: <BadgeIcon icon={Moon} />,
|
||||
label: 'Dark Theme',
|
||||
description: 'Dark color mode',
|
||||
value: 'dark',
|
||||
},
|
||||
{
|
||||
id: 'highcontrast',
|
||||
icon: <BadgeIcon icon={Eye} />,
|
||||
label: 'High Contrast',
|
||||
description: 'High contrast color mode',
|
||||
value: 'highcontrast',
|
||||
},
|
||||
{
|
||||
id: 'auto',
|
||||
icon: <BadgeIcon icon={RefreshCw} />,
|
||||
label: 'Auto',
|
||||
description: 'Sync with system theme',
|
||||
value: 'auto',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,155 @@
|
|||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { success as notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import {
|
||||
CreateGitCredentialPayload,
|
||||
GitCredential,
|
||||
UpdateGitCredentialPayload,
|
||||
} from './types';
|
||||
|
||||
export async function createGitCredential(
|
||||
gitCredential: CreateGitCredentialPayload
|
||||
) {
|
||||
try {
|
||||
await axios.post(buildGitUrl(gitCredential.userId), gitCredential);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to create git credential');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGitCredentials(userId: number) {
|
||||
try {
|
||||
const { data } = await axios.get<GitCredential[]>(buildGitUrl(userId));
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to get git credentials');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGitCredential(userId: number, id: number) {
|
||||
try {
|
||||
const { data } = await axios.get<GitCredential>(buildGitUrl(userId, id));
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to get git credential');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteGitCredential(credential: GitCredential) {
|
||||
try {
|
||||
await axios.delete<GitCredential[]>(
|
||||
buildGitUrl(credential.userId, credential.id)
|
||||
);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to delete git credential');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateGitCredential(
|
||||
credential: Partial<UpdateGitCredentialPayload>,
|
||||
userId: number,
|
||||
id: number
|
||||
) {
|
||||
try {
|
||||
const { data } = await axios.put(buildGitUrl(userId, id), credential);
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to update credential');
|
||||
}
|
||||
}
|
||||
|
||||
export function useUpdateGitCredentialMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
({
|
||||
credential,
|
||||
userId,
|
||||
id,
|
||||
}: {
|
||||
credential: UpdateGitCredentialPayload;
|
||||
userId: number;
|
||||
id: number;
|
||||
}) => updateGitCredential(credential, userId, id),
|
||||
{
|
||||
onSuccess: (_, data) => {
|
||||
notifySuccess(
|
||||
'Git credential updated successfully',
|
||||
data.credential.name
|
||||
);
|
||||
return queryClient.invalidateQueries(['gitcredentials']);
|
||||
},
|
||||
meta: {
|
||||
error: {
|
||||
title: 'Failure',
|
||||
message: 'Unable to update credential',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useDeleteGitCredentialMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteGitCredential, {
|
||||
onSuccess: (_, credential) => {
|
||||
notifySuccess('Git Credential deleted successfully', credential.name);
|
||||
return queryClient.invalidateQueries(['gitcredentials']);
|
||||
},
|
||||
meta: {
|
||||
error: {
|
||||
title: 'Failure',
|
||||
message: 'Unable to delete git credential',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useGitCredentials(userId: number) {
|
||||
return useQuery('gitcredentials', () => getGitCredentials(userId), {
|
||||
staleTime: 20,
|
||||
meta: {
|
||||
error: {
|
||||
title: 'Failure',
|
||||
message: 'Unable to retrieve git credentials',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useGitCredential(userId: number, id: number) {
|
||||
return useQuery(['gitcredentials', id], () => getGitCredential(userId, id), {
|
||||
meta: {
|
||||
error: {
|
||||
title: 'Failure',
|
||||
message: 'Unable to retrieve git credential',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateGitCredentialMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(createGitCredential, {
|
||||
onSuccess: (_, payload) => {
|
||||
notifySuccess('Credentials created successfully', payload.name);
|
||||
return queryClient.invalidateQueries(['gitcredentials']);
|
||||
},
|
||||
meta: {
|
||||
error: {
|
||||
title: 'Failure',
|
||||
message: 'Unable to create credential',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildGitUrl(userId: number, credentialId?: number) {
|
||||
return credentialId
|
||||
? `/users/${userId}/gitcredentials/${credentialId}`
|
||||
: `/users/${userId}/gitcredentials`;
|
||||
}
|
35
app/react/portainer/account/git-credentials/types.ts
Normal file
35
app/react/portainer/account/git-credentials/types.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
PaginationTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@@/datatables/types';
|
||||
|
||||
export interface GitCredentialTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
||||
|
||||
export interface GitCredentialFormValues {
|
||||
name: string;
|
||||
username?: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface CreateGitCredentialPayload {
|
||||
userId: number;
|
||||
name: string;
|
||||
username?: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UpdateGitCredentialPayload {
|
||||
name: string;
|
||||
username?: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export type GitCredential = {
|
||||
id: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
username: string;
|
||||
creationDate: number;
|
||||
};
|
38
app/react/portainer/environments/ItemView/tls-options.tsx
Normal file
38
app/react/portainer/environments/ItemView/tls-options.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { Shield } from 'lucide-react';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
export const tlsOptions: ReadonlyArray<BoxSelectorOption<string>> = [
|
||||
{
|
||||
id: 'tls_client_ca',
|
||||
value: 'tls_client_ca',
|
||||
icon: Shield,
|
||||
iconType: 'badge',
|
||||
label: 'TLS with server and client verification',
|
||||
description: 'Use client certificates and server verification',
|
||||
},
|
||||
{
|
||||
id: 'tls_client_noca',
|
||||
value: 'tls_client_noca',
|
||||
icon: Shield,
|
||||
iconType: 'badge',
|
||||
label: 'TLS with client verification only',
|
||||
description: 'Use client certificates without server verification',
|
||||
},
|
||||
{
|
||||
id: 'tls_ca',
|
||||
value: 'tls_ca',
|
||||
icon: Shield,
|
||||
iconType: 'badge',
|
||||
label: 'TLS with server verification only',
|
||||
description: 'Only verify the server certificate',
|
||||
},
|
||||
{
|
||||
id: 'tls_only',
|
||||
value: 'tls_only',
|
||||
icon: Shield,
|
||||
iconType: 'badge',
|
||||
label: 'TLS only',
|
||||
description: 'No server/client verification',
|
||||
},
|
||||
] as const;
|
|
@ -0,0 +1,15 @@
|
|||
.canvas-container {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.kvm-maximized {
|
||||
position: fixed;
|
||||
background: white;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 1000;
|
||||
max-height: 100% !important;
|
||||
overflow-y: scroll;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { KVM } from '@open-amt-cloud-toolkit/ui-toolkit-react/reactjs/src/kvm.bundle';
|
||||
|
||||
import './KVMControl.css';
|
||||
|
||||
export interface KVMControlProps {
|
||||
deviceId: string;
|
||||
server: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export function KVMControl({ deviceId, server, token }: KVMControlProps) {
|
||||
if (!deviceId || !server || !token) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<KVM
|
||||
deviceId={deviceId}
|
||||
mpsServer={`https://${server}/mps/ws/relay`}
|
||||
authToken={token}
|
||||
mouseDebounceTime="200"
|
||||
canvasHeight="100%"
|
||||
canvasWidth="100%"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { KVMControl } from './KVMControl';
|
|
@ -3,7 +3,7 @@ import { boolean, number, object, SchemaOf, string } from 'yup';
|
|||
|
||||
import { GitAuthModel } from '@/react/portainer/gitops/types';
|
||||
import { useDebounce } from '@/react/hooks/useDebounce';
|
||||
import { GitCredential } from '@/portainer/views/account/git-credential/types';
|
||||
import { GitCredential } from '@/react/portainer/account/git-credentials/types';
|
||||
|
||||
import { SwitchField } from '@@/form-components/SwitchField';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useGitCredentials } from '@/portainer/views/account/git-credential/gitCredential.service';
|
||||
import { GitCredential } from '@/portainer/views/account/git-credential/types';
|
||||
import { GitCredential } from '@/react/portainer/account/git-credentials/types';
|
||||
import { useGitCredentials } from '@/react/portainer/account/git-credentials/git-credentials.service';
|
||||
import { useUser } from '@/react/hooks/useUser';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Form, Formik } from 'formik';
|
|||
import { rest } from 'msw';
|
||||
|
||||
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||
import { GitCredential } from '@/portainer/views/account/git-credential/types';
|
||||
import { GitCredential } from '@/react/portainer/account/git-credentials/types';
|
||||
|
||||
import { GitForm, buildGitValidationSchema } from './GitForm';
|
||||
import { GitFormModel } from './types';
|
||||
|
|
|
@ -5,12 +5,13 @@ import { ComposePathField } from '@/react/portainer/gitops/ComposePathField';
|
|||
import { RefField } from '@/react/portainer/gitops/RefField';
|
||||
import { GitFormUrlField } from '@/react/portainer/gitops/GitFormUrlField';
|
||||
import { GitFormModel } from '@/react/portainer/gitops/types';
|
||||
import { GitCredential } from '@/portainer/views/account/git-credential/types';
|
||||
import { TimeWindowDisplay } from '@/react/portainer/gitops/TimeWindowDisplay';
|
||||
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
import { TimeWindowDisplay } from '@@/TimeWindowDisplay';
|
||||
import { validateForm } from '@@/form-components/validate-form';
|
||||
|
||||
import { GitCredential } from '../account/git-credentials/types';
|
||||
|
||||
import { AdditionalFileField } from './AdditionalFilesField';
|
||||
import { gitAuthValidation, AuthFieldset } from './AuthFieldset';
|
||||
import { AutoUpdateFieldset } from './AutoUpdateFieldset';
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
|||
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { withEdition } from '../portainer/feature-flags/withEdition';
|
||||
import { withEdition } from '../feature-flags/withEdition';
|
||||
|
||||
const TimeWindowDisplayWrapper = withEdition(TimeWindowDisplay, 'BE');
|
||||
|
23
app/react/portainer/init/InitAdminView/restore-options.tsx
Normal file
23
app/react/portainer/init/InitAdminView/restore-options.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Download, Upload } from 'lucide-react';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
||||
|
||||
export const restoreOptions: ReadonlyArray<BoxSelectorOption<string>> = [
|
||||
{
|
||||
id: 'restore_file',
|
||||
value: 'file',
|
||||
icon: Upload,
|
||||
iconType: 'badge',
|
||||
label: 'Upload backup file',
|
||||
},
|
||||
{
|
||||
id: 'restore_s3',
|
||||
value: 's3',
|
||||
icon: Download,
|
||||
iconType: 'badge',
|
||||
label: 'Retrieve from S3',
|
||||
feature: FeatureId.S3_RESTORE,
|
||||
},
|
||||
] as const;
|
43
app/react/portainer/licenses/license.service.test.ts
Normal file
43
app/react/portainer/licenses/license.service.test.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { server, rest } from '@/setup-tests/server';
|
||||
|
||||
import { getLicenses } from './license.service';
|
||||
import type { License } from './types';
|
||||
|
||||
describe('getLicenses', () => {
|
||||
it('on success should return the server body', async () => {
|
||||
const catchFn = jest.fn();
|
||||
const thenFn = jest.fn();
|
||||
|
||||
const data: License[] = [];
|
||||
server.use(
|
||||
rest.get('/api/licenses', (req, res, ctx) => res(ctx.json(data)))
|
||||
);
|
||||
|
||||
const promise = getLicenses();
|
||||
|
||||
await promise.then(thenFn).catch(catchFn);
|
||||
|
||||
expect(catchFn).not.toHaveBeenCalled();
|
||||
expect(thenFn).toHaveBeenCalledWith(data);
|
||||
});
|
||||
|
||||
it('on failure should return the server message', async () => {
|
||||
const catchFn = jest.fn();
|
||||
const thenFn = jest.fn();
|
||||
|
||||
const message = 'message';
|
||||
const details = 'details';
|
||||
|
||||
server.use(
|
||||
rest.get('/api/licenses', (req, res, ctx) =>
|
||||
res(ctx.status(400), ctx.json({ message, details }))
|
||||
)
|
||||
);
|
||||
|
||||
const promise = getLicenses();
|
||||
await promise.then(thenFn, catchFn);
|
||||
|
||||
expect(catchFn).toHaveBeenCalledWith(new Error(message));
|
||||
expect(thenFn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
128
app/react/portainer/licenses/license.service.ts
Normal file
128
app/react/portainer/licenses/license.service.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
import _ from 'lodash';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import axios from '@/portainer/services/axios';
|
||||
|
||||
import { License, LicenseInfo } from './types';
|
||||
|
||||
type Listener = (info: LicenseInfo) => void;
|
||||
|
||||
interface Store {
|
||||
data?: LicenseInfo;
|
||||
lastLoaded?: number;
|
||||
invalidated: boolean;
|
||||
listeners: Listener[];
|
||||
}
|
||||
|
||||
const store: Store = {
|
||||
listeners: [],
|
||||
invalidated: true,
|
||||
};
|
||||
|
||||
export async function getLicenses() {
|
||||
try {
|
||||
const { data } = await axios.get<License[]>(buildUrl());
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
const axiosError = e as AxiosError;
|
||||
throw new Error(axiosError.response?.data.message);
|
||||
}
|
||||
}
|
||||
|
||||
interface AttachResponse {
|
||||
licenses: License[];
|
||||
failedKeys: Record<string, string>;
|
||||
}
|
||||
|
||||
export async function attachLicense(licenseKeys: string[]) {
|
||||
try {
|
||||
const { data } = await axios.post<AttachResponse>(buildUrl(), {
|
||||
licenseKeys,
|
||||
});
|
||||
|
||||
if (Object.keys(data.failedKeys).length === licenseKeys.length) {
|
||||
return data;
|
||||
}
|
||||
|
||||
store.invalidated = true;
|
||||
getLicenseInfo();
|
||||
return data;
|
||||
} catch (e) {
|
||||
const axiosError = e as AxiosError;
|
||||
if (axiosError.response?.status === 401) {
|
||||
throw new Error(
|
||||
'Your session has expired, please refresh the browser and log in again.'
|
||||
);
|
||||
}
|
||||
throw new Error(axiosError.response?.data.message);
|
||||
}
|
||||
}
|
||||
|
||||
interface RemoveResponse {
|
||||
failedKeys: Record<string, string>;
|
||||
}
|
||||
|
||||
export async function removeLicense(licenseKeys: string[]) {
|
||||
try {
|
||||
const { data } = await axios.post<RemoveResponse>(buildUrl('remove'), {
|
||||
licenseKeys,
|
||||
});
|
||||
if (Object.keys(data.failedKeys).length === licenseKeys.length) {
|
||||
return data;
|
||||
}
|
||||
|
||||
store.invalidated = true;
|
||||
getLicenseInfo();
|
||||
return data;
|
||||
} catch (e) {
|
||||
const axiosError = e as AxiosError;
|
||||
throw new Error(axiosError.response?.data.message);
|
||||
}
|
||||
}
|
||||
|
||||
export function resetState() {
|
||||
store.invalidated = true;
|
||||
store.data = undefined;
|
||||
}
|
||||
|
||||
export async function getLicenseInfo() {
|
||||
try {
|
||||
if (
|
||||
store.data &&
|
||||
!store.invalidated &&
|
||||
store.lastLoaded &&
|
||||
Math.abs(store.lastLoaded - Date.now()) < 1000 * 30
|
||||
) {
|
||||
return store.data;
|
||||
}
|
||||
|
||||
const { data: info } = await axios.get<LicenseInfo>(buildUrl('info'));
|
||||
store.data = info;
|
||||
store.lastLoaded = Date.now();
|
||||
store.invalidated = false;
|
||||
store.listeners.forEach((listener) => listener(info));
|
||||
|
||||
return info;
|
||||
} catch (e) {
|
||||
const axiosError = e as AxiosError;
|
||||
throw new Error(axiosError.response?.data.message);
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribe(listener: Listener) {
|
||||
store.listeners.push(listener);
|
||||
}
|
||||
|
||||
export function unsubscribe(listener: Listener) {
|
||||
_.remove<Listener>(store.listeners, listener);
|
||||
}
|
||||
|
||||
function buildUrl(action = '') {
|
||||
let url = 'licenses';
|
||||
|
||||
if (action) {
|
||||
url += `/${action}`;
|
||||
}
|
||||
return url;
|
||||
}
|
45
app/react/portainer/licenses/types.ts
Normal file
45
app/react/portainer/licenses/types.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
// matches https://github.com/portainer/liblicense/blob/master/liblicense.go#L66-L74
|
||||
export enum Edition {
|
||||
CE = 1,
|
||||
BE,
|
||||
EE,
|
||||
}
|
||||
|
||||
// matches https://github.com/portainer/liblicense/blob/master/liblicense.go#L60-L64
|
||||
|
||||
export enum LicenseType {
|
||||
Trial = 1,
|
||||
Subscription,
|
||||
}
|
||||
|
||||
// matches https://github.com/portainer/liblicense/blob/master/liblicense.go#L35-L50
|
||||
export interface License {
|
||||
id: string;
|
||||
company: string;
|
||||
created: number;
|
||||
email: string;
|
||||
expiresAfter: number;
|
||||
licenseKey: string;
|
||||
nodes: number;
|
||||
productEdition: Edition;
|
||||
revoked: boolean;
|
||||
revokedAt: number;
|
||||
type: LicenseType;
|
||||
version: number;
|
||||
reference: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
// matches https://github.com/portainer/portainer-ee/blob/c4575bf528583fe1682267db4ee40a11a905f611/api/portainer.go#L588-L597
|
||||
export interface LicenseInfo {
|
||||
productEdition: Edition;
|
||||
company: string;
|
||||
email: string;
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
nodes: number;
|
||||
type: LicenseType;
|
||||
valid: boolean;
|
||||
enforcedAt: number;
|
||||
enforced: boolean;
|
||||
}
|
37
app/react/portainer/licenses/use-license.service.ts
Normal file
37
app/react/portainer/licenses/use-license.service.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { error as notifyError } from '@/portainer/services/notifications';
|
||||
import { useNodesCount } from '@/react/portainer/system/useNodesCount';
|
||||
|
||||
import { getLicenseInfo } from './license.service';
|
||||
import { LicenseInfo, LicenseType } from './types';
|
||||
|
||||
export function useLicenseInfo() {
|
||||
const { isLoading, data: info } = useQuery<LicenseInfo, Error>(
|
||||
'licenseInfo',
|
||||
() => getLicenseInfo(),
|
||||
{
|
||||
onError(error) {
|
||||
notifyError('Failure', error as Error, 'Failed to get license info');
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { isLoading, info };
|
||||
}
|
||||
|
||||
export function useIntegratedLicenseInfo() {
|
||||
const { isLoading: isLoadingNodes, data: nodesCount = 0 } = useNodesCount();
|
||||
|
||||
const { isLoading: isLoadingLicense, info } = useLicenseInfo();
|
||||
if (
|
||||
isLoadingLicense ||
|
||||
isLoadingNodes ||
|
||||
!info ||
|
||||
info.type === LicenseType.Trial
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { licenseInfo: info as LicenseInfo, usedNodes: nodesCount };
|
||||
}
|
62
app/react/portainer/registries/CreateView/options.tsx
Normal file
62
app/react/portainer/registries/CreateView/options.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { Edit } from 'lucide-react';
|
||||
|
||||
import Docker from '@/assets/ico/vendor/docker.svg?c';
|
||||
import Ecr from '@/assets/ico/vendor/ecr.svg?c';
|
||||
import Quay from '@/assets/ico/vendor/quay.svg?c';
|
||||
import Proget from '@/assets/ico/vendor/proget.svg?c';
|
||||
import Azure from '@/assets/ico/vendor/azure.svg?c';
|
||||
import Gitlab from '@/assets/ico/vendor/gitlab.svg?c';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
export const options = [
|
||||
{
|
||||
id: 'registry_dockerhub',
|
||||
icon: Docker,
|
||||
label: 'DockerHub',
|
||||
description: 'DockerHub authenticated account',
|
||||
value: '6',
|
||||
},
|
||||
{
|
||||
id: 'registry_aws_ecr',
|
||||
icon: Ecr,
|
||||
label: 'AWS ECR',
|
||||
description: 'Amazon elastic container registry',
|
||||
value: '7',
|
||||
},
|
||||
{
|
||||
id: 'registry_quay',
|
||||
icon: Quay,
|
||||
label: 'Quay.io',
|
||||
description: 'Quay container registry',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
id: 'registry_proget',
|
||||
icon: Proget,
|
||||
label: 'ProGet',
|
||||
description: 'ProGet container registry',
|
||||
value: '5',
|
||||
},
|
||||
{
|
||||
id: 'registry_azure',
|
||||
icon: Azure,
|
||||
label: 'Azure',
|
||||
description: 'Azure container registry',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
id: 'registry_gitlab',
|
||||
icon: Gitlab,
|
||||
label: 'GitLab',
|
||||
description: 'GitLab container registry',
|
||||
value: '4',
|
||||
},
|
||||
{
|
||||
id: 'registry_custom',
|
||||
icon: <BadgeIcon icon={Edit} />,
|
||||
label: 'Custom registry',
|
||||
description: 'Define your own registry',
|
||||
value: '3',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,40 @@
|
|||
import { ArrowDownCircle } from 'lucide-react';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import Microsoft from '@/assets/ico/vendor/microsoft.svg?c';
|
||||
import Ldap from '@/assets/ico/ldap.svg?c';
|
||||
import OAuth from '@/assets/ico/oauth.svg?c';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
export const options = [
|
||||
{
|
||||
id: 'auth_internal',
|
||||
icon: <BadgeIcon icon={ArrowDownCircle} />,
|
||||
label: 'Internal',
|
||||
description: 'Internal authentication mechanism',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
id: 'auth_ldap',
|
||||
icon: Ldap,
|
||||
label: 'LDAP',
|
||||
description: 'LDAP authentication',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
id: 'auth_ad',
|
||||
icon: Microsoft,
|
||||
label: 'Microsoft Active Directory',
|
||||
description: 'AD authentication',
|
||||
value: 4,
|
||||
feature: FeatureId.HIDE_INTERNAL_AUTH,
|
||||
},
|
||||
{
|
||||
id: 'auth_oauth',
|
||||
icon: OAuth,
|
||||
label: 'OAuth',
|
||||
description: 'OAuth authentication',
|
||||
value: 3,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,28 @@
|
|||
import { Edit } from 'lucide-react';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import Openldap from '@/assets/ico/vendor/openldap.svg?c';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
const SERVER_TYPES = {
|
||||
CUSTOM: 0,
|
||||
OPEN_LDAP: 1,
|
||||
AD: 2,
|
||||
};
|
||||
|
||||
export const options = [
|
||||
{
|
||||
id: 'ldap_custom',
|
||||
icon: <BadgeIcon icon={Edit} />,
|
||||
label: 'Custom',
|
||||
value: SERVER_TYPES.CUSTOM,
|
||||
},
|
||||
{
|
||||
id: 'ldap_openldap',
|
||||
icon: Openldap,
|
||||
label: 'OpenLDAP',
|
||||
value: SERVER_TYPES.OPEN_LDAP,
|
||||
feature: FeatureId.EXTERNAL_AUTH_LDAP,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,42 @@
|
|||
import { Edit } from 'lucide-react';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
import Microsoft from '@/assets/ico/vendor/microsoft.svg?c';
|
||||
import Google from '@/assets/ico/vendor/google.svg?c';
|
||||
import Github from '@/assets/ico/vendor/github.svg?c';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
export const options = [
|
||||
{
|
||||
id: 'microsoft',
|
||||
icon: Microsoft,
|
||||
label: 'Microsoft',
|
||||
description: 'Microsoft OAuth provider',
|
||||
value: 'microsoft',
|
||||
feature: FeatureId.HIDE_INTERNAL_AUTH,
|
||||
},
|
||||
{
|
||||
id: 'google',
|
||||
icon: Google,
|
||||
label: 'Google',
|
||||
description: 'Google OAuth provider',
|
||||
value: 'google',
|
||||
feature: FeatureId.HIDE_INTERNAL_AUTH,
|
||||
},
|
||||
{
|
||||
id: 'github',
|
||||
icon: Github,
|
||||
label: 'Github',
|
||||
description: 'Github OAuth provider',
|
||||
value: 'github',
|
||||
feature: FeatureId.HIDE_INTERNAL_AUTH,
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
icon: <BadgeIcon icon={Edit} />,
|
||||
label: 'Custom',
|
||||
description: 'Custom OAuth provider',
|
||||
value: 'custom',
|
||||
},
|
||||
];
|
22
app/react/portainer/settings/SettingsView/backup-options.tsx
Normal file
22
app/react/portainer/settings/SettingsView/backup-options.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { DownloadCloud, UploadCloud } from 'lucide-react';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { BadgeIcon } from '@@/BadgeIcon';
|
||||
|
||||
export const options = [
|
||||
{
|
||||
id: 'backup_file',
|
||||
icon: <BadgeIcon icon={DownloadCloud} />,
|
||||
label: 'Download backup file',
|
||||
value: 'file',
|
||||
},
|
||||
{
|
||||
id: 'backup_s3',
|
||||
icon: <BadgeIcon icon={UploadCloud} />,
|
||||
label: 'Store in S3',
|
||||
description: 'Define a cron schedule',
|
||||
value: 's3',
|
||||
feature: FeatureId.S3_BACKUP_SETTING,
|
||||
},
|
||||
];
|
Loading…
Add table
Add a link
Reference in a new issue