1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-10 00:05:24 +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:
Chaim Lev-Ari 2023-02-28 17:32:29 +02:00 committed by GitHub
parent f9a09301a8
commit b98c71f1ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 312 additions and 294 deletions

View file

@ -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';

View file

@ -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();

View 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',
},
];

View file

@ -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`;
}

View 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;
};

View 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;

View file

@ -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;
}

View file

@ -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%"
/>
);
}

View file

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

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -0,0 +1,87 @@
import moment from 'moment';
import 'moment-timezone';
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { TextTip } from '@@/Tip/TextTip';
import { withEdition } from '../feature-flags/withEdition';
const TimeWindowDisplayWrapper = withEdition(TimeWindowDisplay, 'BE');
export { TimeWindowDisplayWrapper as TimeWindowDisplay };
function TimeWindowDisplay() {
const currentEnvQuery = useCurrentEnvironment(false);
if (!currentEnvQuery.data) {
return null;
}
const { ChangeWindow } = currentEnvQuery.data;
if (!ChangeWindow.Enabled) {
return null;
}
const timezone = moment.tz.guess();
const isDST = moment().isDST();
const { startTime: startTimeLocal, endTime: endTimeLocal } = utcToTime(
{ startTime: ChangeWindow.StartTime, endTime: ChangeWindow.EndTime },
timezone
);
const { startTime: startTimeUtc, endTime: endTimeUtc } = parseInterval(
ChangeWindow.StartTime,
ChangeWindow.EndTime
);
return (
<TextTip color="orange">
A change window is enabled, automatic updates will not occur outside of{' '}
<span className="font-bold">
{shortTime(startTimeUtc)} - {shortTime(endTimeUtc)} UTC (
{shortTime(startTimeLocal)} -{shortTime(endTimeLocal)}{' '}
{isDST ? 'DST' : ''} {timezone})
</span>
.
</TextTip>
);
}
function utcToTime(
utcTime: { startTime: string; endTime: string },
timezone: string
) {
const startTime = moment
.tz(utcTime.startTime, 'HH:mm', 'GMT')
.tz(timezone)
.format('HH:mm');
const endTime = moment
.tz(utcTime.endTime, 'HH:mm', 'GMT')
.tz(timezone)
.format('HH:mm');
return parseInterval(startTime, endTime);
}
function parseTime(originalTime: string) {
const [startHour, startMin] = originalTime.split(':');
const time = new Date();
time.setHours(parseInt(startHour, 10));
time.setMinutes(parseInt(startMin, 10));
return time;
}
function parseInterval(startTime: string, endTime: string) {
return {
startTime: parseTime(startTime),
endTime: parseTime(endTime),
};
}
function shortTime(time: Date) {
return moment(time).format('h:mm a');
}

View 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;

View 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();
});
});

View 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;
}

View 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;
}

View 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 };
}

View 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',
},
];

View file

@ -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,
},
];

View file

@ -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,
},
];

View file

@ -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',
},
];

View 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,
},
];