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:
parent
afe6cd6df0
commit
273a3f9a10
130 changed files with 3194 additions and 1190 deletions
87
app/react/components/TimeWindowDisplay.tsx
Normal file
87
app/react/components/TimeWindowDisplay.tsx
Normal 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');
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) }} />;
|
||||
}
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -6,12 +6,10 @@
|
|||
.container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.display-text {
|
||||
.copy-button {
|
||||
opacity: 0;
|
||||
margin-left: 7px;
|
||||
color: #23ae89;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { InputList } from './InputList';
|
||||
export { InputList, type ItemProps } from './InputList';
|
||||
|
|
|
@ -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({
|
||||
|
|
27
app/react/components/form-components/useCachedTest.ts
Normal file
27
app/react/components/form-components/useCachedTest.ts
Normal 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;
|
||||
}
|
26
app/react/components/form-components/useCaretPosition.ts
Normal file
26
app/react/components/form-components/useCaretPosition.ts
Normal 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 };
|
||||
}
|
19
app/react/components/form-components/validate-form.ts
Normal file
19
app/react/components/form-components/validate-form.ts
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue