1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

refactor(gitops): migrate git form to react [EE-4849] (#8268)

This commit is contained in:
Chaim Lev-Ari 2023-02-23 01:43:33 +05:30 committed by GitHub
parent afe6cd6df0
commit 273a3f9a10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
130 changed files with 3194 additions and 1190 deletions

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 '../portainer/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

@ -1,5 +1,6 @@
import { PropsWithChildren } from 'react';
import { AlertCircle } from 'lucide-react';
import clsx from 'clsx';
import { Icon, IconMode } from '@@/Icon';
@ -8,17 +9,18 @@ type Color = 'orange' | 'blue';
export interface Props {
icon?: React.ReactNode;
color?: Color;
className?: string;
}
export function TextTip({
color = 'orange',
icon = AlertCircle,
className,
children,
}: PropsWithChildren<Props>) {
return (
<p className="small inline-flex items-center gap-1">
<Icon icon={icon} mode={getMode(color)} className="shrink-0" />
<p className={clsx('small inline-flex items-center gap-1', className)}>
<Icon icon={icon} mode={getMode(color)} />
<span className="text-muted">{children}</span>
</p>
);

View file

@ -1,12 +1,12 @@
import { HelpCircle } from 'lucide-react';
import { useMemo } from 'react';
import { ReactNode, useMemo } from 'react';
import sanitize from 'sanitize-html';
import { TooltipWithChildren, Position } from '../TooltipWithChildren';
export interface Props {
position?: Position;
message: string;
message: ReactNode;
className?: string;
setHtmlMessage?: boolean;
}
@ -19,7 +19,7 @@ export function Tooltip({
}: Props) {
// allow angular views to set html messages for the tooltip
const htmlMessage = useMemo(() => {
if (setHtmlMessage) {
if (setHtmlMessage && typeof message === 'string') {
// eslint-disable-next-line react/no-danger
return <div dangerouslySetInnerHTML={{ __html: sanitize(message) }} />;
}

View file

@ -21,6 +21,8 @@ export function ButtonGroup({
function sizeClass(size: Size | undefined) {
switch (size) {
case 'small':
return 'btn-group-sm';
case 'xsmall':
return 'btn-group-xs';
case 'large':

View file

@ -6,12 +6,10 @@
.container {
display: flex;
align-items: baseline;
margin-top: 10px;
}
.display-text {
.copy-button {
opacity: 0;
margin-left: 7px;
color: #23ae89;
}

View file

@ -1,4 +1,4 @@
import { PropsWithChildren } from 'react';
import { ComponentProps, PropsWithChildren } from 'react';
import clsx from 'clsx';
import { Check, Copy } from 'lucide-react';
@ -14,6 +14,7 @@ export interface Props {
fadeDelay?: number;
displayText?: string;
className?: string;
color?: ComponentProps<typeof Button>['color'];
}
export function CopyButton({
@ -21,6 +22,7 @@ export function CopyButton({
fadeDelay = 1000,
displayText = 'copied',
className,
color,
children,
}: PropsWithChildren<Props>) {
const { handleCopy, copiedSuccessfully } = useCopy(copyText, fadeDelay);
@ -29,19 +31,20 @@ export function CopyButton({
<div className={styles.container}>
<Button
className={className}
color={color}
size="small"
onClick={handleCopy}
title="Copy Value"
type="button"
icon={Copy}
>
<Icon icon={Copy} />
{children}
</Button>
<span
className={clsx(
copiedSuccessfully && styles.fadeout,
styles.displayText,
styles.copyButton,
'space-left',
'vertical-center'
)}

View file

@ -2,6 +2,7 @@ import clsx from 'clsx';
import { PropsWithChildren, ReactNode } from 'react';
import { ButtonGroup, Size } from '@@/buttons/ButtonGroup';
import { Button } from '@@/buttons';
import styles from './ButtonSelector.module.css';
@ -59,10 +60,12 @@ function OptionItem({
readOnly,
}: PropsWithChildren<OptionItemProps>) {
return (
<label
className={clsx('btn btn-primary', {
<Button
color="light"
as="label"
disabled={disabled || readOnly}
className={clsx({
active: selected,
disabled: readOnly || disabled,
})}
>
{children}
@ -73,6 +76,6 @@ function OptionItem({
disabled={disabled}
readOnly={readOnly}
/>
</label>
</Button>
);
}

View file

@ -1,4 +1,4 @@
import { PropsWithChildren, ReactNode } from 'react';
import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
import clsx from 'clsx';
import { Tooltip } from '@@/Tip/Tooltip';
@ -11,11 +11,12 @@ export interface Props {
inputId?: string;
label: ReactNode;
size?: Size;
tooltip?: string;
tooltip?: ComponentProps<typeof Tooltip>['message'];
setTooltipHtmlMessage?: ComponentProps<typeof Tooltip>['setHtmlMessage'];
children: ReactNode;
errors?: ReactNode;
required?: boolean;
setTooltipHtmlMessage?: boolean;
className?: string;
}
export function FormControl({
@ -25,12 +26,14 @@ export function FormControl({
tooltip = '',
children,
errors,
required,
className,
required = false,
setTooltipHtmlMessage,
}: PropsWithChildren<Props>) {
return (
<div
className={clsx(
className,
'form-group',
'after:clear-both after:table after:content-[""]' // to fix issues with float
)}
@ -50,12 +53,7 @@ export function FormControl({
<div className={sizeClassChildren(size)}>
{children}
{errors && (
<span className="help-block">
<FormError>{errors}</FormError>
</span>
)}
{errors && <FormError>{errors}</FormError>}
</div>
</div>
);

View file

@ -10,7 +10,9 @@ interface Props {
export function FormError({ children, className }: PropsWithChildren<Props>) {
return (
<p className={clsx(`text-muted small vertical-center`, className)}>
<p
className={clsx(`text-muted small vertical-center help-block`, className)}
>
<Icon icon={AlertTriangle} className="icon-warning" />
<span className="text-warning">{children}</span>
</p>

View file

@ -1,14 +1,24 @@
import clsx from 'clsx';
import { InputHTMLAttributes } from 'react';
import { forwardRef, InputHTMLAttributes, Ref } from 'react';
export const InputWithRef = forwardRef<
HTMLInputElement,
InputHTMLAttributes<HTMLInputElement>
>(
// eslint-disable-next-line react/jsx-props-no-spreading
(props, ref) => <Input {...props} mRef={ref} />
);
export function Input({
className,
mRef: ref,
...props
}: InputHTMLAttributes<HTMLInputElement>) {
}: InputHTMLAttributes<HTMLInputElement> & { mRef?: Ref<HTMLInputElement> }) {
return (
<input
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
className={clsx('form-control', className)}
/>
);

View file

@ -1 +1 @@
export { InputList } from './InputList';
export { InputList, type ItemProps } from './InputList';

View file

@ -1,4 +1,5 @@
import clsx from 'clsx';
import { ComponentProps } from 'react';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
@ -13,14 +14,14 @@ export interface Props {
onChange(value: boolean): void;
name?: string;
tooltip?: string;
tooltip?: ComponentProps<typeof Tooltip>['message'];
setTooltipHtmlMessage?: ComponentProps<typeof Tooltip>['setHtmlMessage'];
labelClass?: string;
switchClass?: string;
fieldClass?: string;
dataCy?: string;
disabled?: boolean;
featureId?: FeatureId;
setTooltipHtmlMessage?: boolean;
}
export function SwitchField({

View file

@ -0,0 +1,27 @@
import { useRef } from 'react';
import { TestContext, TestFunction } from 'yup';
function cacheTest<T, TContext>(
asyncValidate: TestFunction<T, TContext>
): TestFunction<T, TContext> {
let valid = true;
let value: T | undefined;
return async (newValue: T, context: TestContext<TContext>) => {
if (newValue !== value) {
value = newValue;
const response = await asyncValidate.call(context, newValue, context);
valid = !!response;
}
return valid;
};
}
export function useCachedValidation<T, TContext>(
test: TestFunction<T, TContext>
) {
const ref = useRef(cacheTest(test));
return ref.current;
}

View file

@ -0,0 +1,26 @@
import { useRef, useState, useCallback, useEffect } from 'react';
export function useCaretPosition<
T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement
>() {
const node = useRef<T>(null);
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
const updateCaret = useCallback(() => {
if (node.current) {
const { selectionStart, selectionEnd } = node.current;
setStart(selectionStart || 0);
setEnd(selectionEnd || 0);
}
}, []);
useEffect(() => {
if (node.current) {
node.current.setSelectionRange(start, end);
}
});
return { start, end, ref: node, updateCaret };
}

View file

@ -0,0 +1,19 @@
import { yupToFormErrors } from 'formik';
import { SchemaOf } from 'yup';
export async function validateForm<T>(
schemaBuilder: () => SchemaOf<T>,
formValues: T
) {
const validationSchema = schemaBuilder();
try {
await validationSchema.validate(formValues, {
strict: true,
abortEarly: false,
});
return undefined;
} catch (error) {
return yupToFormErrors<T>(error);
}
}