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

feat(k8sconfigure): migrate configure to react [EE-5524] (#10218)

This commit is contained in:
Ali 2023-09-05 18:06:36 +02:00 committed by GitHub
parent 0f1e77a6d5
commit 515b02813b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1819 additions and 833 deletions

View file

@ -0,0 +1,109 @@
import { ChevronUp, ChevronDown } from 'lucide-react';
import moment from 'moment';
import { Button } from '@@/buttons';
import { Input } from '@@/form-components/Input';
import { utcToTimeZone } from './utils';
const valueFormat = 'HH:mm';
const displayFormat = 'hh:mm';
const minuteIncrement = 5;
type Props = {
utcTime: string;
onChange: (time: string) => void;
timeZone?: string;
};
export function TimePickerInput({
utcTime,
onChange,
timeZone = 'UTC',
}: Props) {
const localTime12h = utcToTimeZone(utcTime, timeZone, displayFormat);
const localTime24h = utcToTimeZone(utcTime, timeZone, valueFormat);
const [hours, minutes] = localTime12h.split(':');
return (
<div className="flex items-center">
<div className="flex flex-col">
<Button
color="link"
size="medium"
className="!ml-0 w-full"
icon={ChevronUp}
onClick={() => {
const newTime = moment(localTime24h, valueFormat)
.add(1, 'hours')
.format(valueFormat);
onChange(newTime);
}}
/>
<Input
type="text"
value={hours}
className="w-12 !cursor-default text-center"
disabled
/>
<Button
color="link"
size="medium"
className="!ml-0 w-full"
icon={ChevronDown}
onClick={() => {
const newTime = moment(localTime24h, valueFormat)
.subtract(1, 'hours')
.format(valueFormat);
onChange(newTime);
}}
/>
</div>
:
<div className="flex flex-col">
<Button
color="link"
size="medium"
className="!ml-0 w-full"
icon={ChevronUp}
onClick={() => {
const newTime = moment(localTime24h, valueFormat)
.add(minuteIncrement, 'minutes')
.format(valueFormat);
onChange(newTime);
}}
/>
<Input
type="text"
value={minutes}
className="w-12 !cursor-default text-center"
disabled
/>
<Button
color="link"
size="medium"
className="!ml-0 w-full"
icon={ChevronDown}
onClick={() => {
const newTime = moment(localTime24h, valueFormat)
.subtract(minuteIncrement, 'minutes')
.format(valueFormat);
onChange(newTime);
}}
/>
</div>
<Button
color="default"
className="h-[34px]"
onClick={() => {
const newTime = moment(localTime24h, valueFormat)
.add(12, 'hours')
.format(valueFormat);
onChange(newTime);
}}
>
{moment(localTime24h, valueFormat).format('A')}
</Button>
</div>
);
}

View file

@ -0,0 +1,99 @@
import moment from 'moment';
import { Button } from '@@/buttons';
import { Alert } from '@@/Alert';
import { EndpointChangeWindow } from '../../types';
import { TimeWindowPickerInputGroup } from './TimeWindowPickerInputGroup';
import { formatUTCTime, utcToTimeZone } from './utils';
type Props = {
/**
* The current start and end time values. in 'HH:mm' format (e.g. '00:00') and in UTC timezone.
*/
values: EndpointChangeWindow;
initialValues: EndpointChangeWindow;
onChange: ({
changeWindow,
timeZone,
}: {
changeWindow: EndpointChangeWindow;
timeZone?: string;
}) => void;
isEditMode: boolean;
setIsEditMode: (isEditMode: boolean) => void;
timeZone?: string;
initialTimeZone?: string;
};
const summaryTimeFormat = 'h:mmA';
export function TimeWindowPicker({
values,
initialValues,
onChange,
isEditMode,
setIsEditMode,
timeZone = moment.tz.guess(),
initialTimeZone,
}: Props) {
return (
<div className="mb-4 flex flex-col gap-y-2">
{isEditMode && (
<TimeWindowPickerInputGroup
values={values}
onChange={onChange}
timeZone={timeZone}
/>
)}
<Alert color="info" className="[&>div]:!text-xs">
<span>
GitOps updates to stacks or applications outside{' '}
<span className="font-bold">{`${formatUTCTime(
values.StartTime,
summaryTimeFormat
)} - ${formatUTCTime(
values.EndTime,
summaryTimeFormat
)} UTC (${utcToTimeZone(
values.StartTime,
timeZone,
summaryTimeFormat
)} - ${utcToTimeZone(values.EndTime, timeZone, summaryTimeFormat)} ${
moment().isDST() ? ' DST' : ''
} ${timeZone})`}</span>{' '}
will not occur.
</span>
</Alert>
{values.Enabled && (
<div className="flex w-full">
{!isEditMode && (
<Button
color="default"
className="!ml-0"
onClick={() => setIsEditMode(true)}
>
Edit Change Window
</Button>
)}
{isEditMode && (
<Button
color="default"
className="!ml-0"
onClick={() => {
setIsEditMode(false);
onChange({
changeWindow: initialValues,
timeZone: initialTimeZone,
});
}}
>
Cancel
</Button>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,130 @@
import moment from 'moment';
import { useMemo } from 'react';
import { Select } from '@@/form-components/ReactSelect';
import { Option } from '@@/form-components/PortainerSelect';
import { EndpointChangeWindow } from '../../types';
import { timeZoneToUtc, utcToTimeZone } from './utils';
import { TimePickerInput } from './TimePickerInput';
type Props = {
/**
* The current start and end time values. in 'HH:mm' format (e.g. '00:00') and in UTC timezone.
*/
values: EndpointChangeWindow;
onChange: ({
changeWindow,
timeZone,
}: {
changeWindow: EndpointChangeWindow;
timeZone: string;
}) => void;
timeZone?: string;
};
export function TimeWindowPickerInputGroup({
values,
onChange,
timeZone = moment.tz.guess(),
}: Props) {
// all unique timezones for all countries as options
const timeZoneOptions = useMemo(() => {
const countries = moment.tz.countries();
const zones = countries.flatMap((country) =>
moment.tz.zonesForCountry(country)
);
return [...new Set(zones)]
.sort()
.concat('UTC')
.map((zone) => ({
label: zone,
value: zone,
}));
}, []);
// set the initial timezone to the user's timezone if it is not set
if (!timeZone) {
const newTimeZone = moment.tz.guess();
onChange({
changeWindow: {
...values,
StartTime: timeZoneToUtc(values.StartTime, newTimeZone),
EndTime: timeZoneToUtc(values.EndTime, newTimeZone),
},
timeZone: newTimeZone,
});
}
return (
<div className="flex flex-wrap items-center gap-x-5">
<div className="flex items-center gap-x-5">
<TimePickerInput
utcTime={values.StartTime}
timeZone={timeZone}
onChange={(time) =>
onChange({
changeWindow: {
...values,
StartTime: timeZoneToUtc(time, timeZone),
},
timeZone,
})
}
/>
to
<TimePickerInput
utcTime={values.EndTime}
timeZone={timeZone}
onChange={(time) =>
onChange({
changeWindow: {
...values,
EndTime: timeZoneToUtc(time, timeZone),
},
timeZone,
})
}
/>
</div>
<Select<Option<string>>
options={timeZoneOptions}
value={{ value: timeZone, label: timeZone }}
className="w-72 min-w-fit"
onChange={(newTimeZone) => {
if (!newTimeZone) return;
// update the utc time so that the local time displayed remains the same
const updatedStartTime = onTimezoneChangeUpdateUTCTime(
values.StartTime,
timeZone,
newTimeZone.value
);
const updatedEndTime = onTimezoneChangeUpdateUTCTime(
values.EndTime,
timeZone,
newTimeZone.value
);
onChange({
changeWindow: {
...values,
StartTime: updatedStartTime,
EndTime: updatedEndTime,
},
timeZone: newTimeZone.value,
});
}}
/>
</div>
);
}
function onTimezoneChangeUpdateUTCTime(
utcTime: string,
oldTimeZone: string,
newTimeZone: string
) {
const localTime = utcToTimeZone(utcTime, oldTimeZone);
const newUtcTime = timeZoneToUtc(localTime, newTimeZone);
return newUtcTime;
}

View file

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

View file

@ -0,0 +1,36 @@
import moment from 'moment';
/**
* Converts a UTC time to the same format in the given timezone.
* @param utcTime The UTC time to convert in 'HH:mm' format.
* @param timeZone The timezone to convert the UTC time to.
* @param format The format to convert the time to.
* @returns The converted time in the same format as the input.
*/
export function utcToTimeZone(
utcTime: string,
timeZone: string,
format = 'HH:mm'
) {
return moment.utc(utcTime, 'HH:mm').tz(timeZone).format(format);
}
/**
* Converts a time in the given timezone to the same format in UTC.
* @param time The time to convert in 'HH:mm' format.
* @param timeZone The timezone to convert the time to UTC.
* @returns The converted time in the same format as the input.
*/
export function timeZoneToUtc(time: string, timeZone: string) {
return moment.tz(time, 'HH:mm', timeZone).utc().format('HH:mm');
}
/**
* Formats a UTC time string to the specified format.
* @param utcTime - The UTC time string to format in 'HH:mm' format.
* @param format - The format to use. Defaults to 'HH:mm'.
* @returns The formatted time string.
*/
export function formatUTCTime(utcTime: string, format = 'HH:mm') {
return moment.utc(utcTime, 'HH:mm').format(format);
}

View file

@ -1,8 +1,8 @@
import { EnvironmentId } from '../types';
export const queryKeys = {
export const environmentQueryKeys = {
base: () => ['environments'] as const,
item: (id: EnvironmentId) => [...queryKeys.base(), id] as const,
item: (id: EnvironmentId) => [...environmentQueryKeys.base(), id] as const,
registries: (environmentId: EnvironmentId) =>
[...queryKeys.base(), environmentId, 'registries'] as const,
[...environmentQueryKeys.base(), environmentId, 'registries'] as const,
};

View file

@ -2,10 +2,10 @@ import { useQuery } from 'react-query';
import { getAgentVersions } from '../environment.service';
import { queryKeys } from './query-keys';
import { environmentQueryKeys } from './query-keys';
export function useAgentVersionsList() {
return useQuery([...queryKeys.base(), 'agentVersions'], () =>
return useQuery([...environmentQueryKeys.base(), 'agentVersions'], () =>
getAgentVersions()
);
}

View file

@ -7,14 +7,14 @@ import {
} from '@/react/portainer/environments/types';
import { withError } from '@/react-tools/react-query';
import { queryKeys } from './query-keys';
import { environmentQueryKeys } from './query-keys';
export function useEnvironment<T = Environment | null>(
id?: EnvironmentId,
select?: (environment: Environment | null) => T
) {
return useQuery(
id ? queryKeys.item(id) : [],
id ? environmentQueryKeys.item(id) : [],
() => (id ? getEndpoint(id) : null),
{
select,

View file

@ -8,7 +8,7 @@ import {
getEnvironments,
} from '../environment.service';
import { queryKeys } from './query-keys';
import { environmentQueryKeys } from './query-keys';
export const ENVIRONMENTS_POLLING_INTERVAL = 30000; // in ms
@ -60,7 +60,7 @@ export function useEnvironmentList(
) {
const { isLoading, data } = useQuery(
[
...queryKeys.base(),
...environmentQueryKeys.base(),
{
page,
pageLimit,

View file

@ -5,14 +5,14 @@ import { EnvironmentId } from '../types';
import { Registry } from '../../registries/types/registry';
import { useGenericRegistriesQuery } from '../../registries/queries/useRegistries';
import { queryKeys } from './query-keys';
import { environmentQueryKeys } from './query-keys';
export function useEnvironmentRegistries<T = Array<Registry>>(
environmentId: EnvironmentId,
queryOptions: { select?(data: Array<Registry>): T; enabled?: boolean } = {}
) {
return useGenericRegistriesQuery(
queryKeys.registries(environmentId),
environmentQueryKeys.registries(environmentId),
() => getEnvironmentRegistries(environmentId),
queryOptions
);

View file

@ -5,6 +5,9 @@ import {
EnvironmentId,
EnvironmentStatusMessage,
Environment,
KubernetesSettings,
DeploymentOptions,
EndpointChangeWindow,
} from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { TagId } from '@/portainer/tags/types';
@ -12,17 +15,17 @@ import { TagId } from '@/portainer/tags/types';
import { EnvironmentGroupId } from '../environment-groups/types';
import { buildUrl } from '../environment.service/utils';
import { queryKeys } from './query-keys';
import { environmentQueryKeys } from './query-keys';
export function useUpdateEnvironmentMutation() {
const queryClient = useQueryClient();
return useMutation(updateEnvironment, {
...withInvalidate(queryClient, [queryKeys.base()]),
...withInvalidate(queryClient, [environmentQueryKeys.base()]),
...withError('Unable to update environment'),
});
}
export interface UpdatePayload {
export interface UpdateEnvironmentPayload extends Partial<Environment> {
TLSCACert?: File;
TLSCert?: File;
TLSKey?: File;
@ -42,15 +45,18 @@ export interface UpdatePayload {
AzureAuthenticationKey: string;
IsSetStatusMessage: boolean;
StatusMessage: Partial<EnvironmentStatusMessage>;
StatusMessage: EnvironmentStatusMessage;
Kubernetes?: KubernetesSettings;
DeploymentOptions?: DeploymentOptions | null;
ChangeWindow?: EndpointChangeWindow;
}
async function updateEnvironment({
export async function updateEnvironment({
id,
payload,
}: {
id: EnvironmentId;
payload: Partial<UpdatePayload>;
payload: Partial<UpdateEnvironmentPayload>;
}) {
try {
await uploadTLSFilesForEndpoint(

View file

@ -16,7 +16,7 @@ import { EnvironmentId } from '../types';
import { buildUrl } from '../environment.service/utils';
import { EnvironmentGroupId } from '../environment-groups/types';
import { queryKeys } from './query-keys';
import { environmentQueryKeys } from './query-keys';
export function useUpdateEnvironmentsRelationsMutation() {
const queryClient = useQueryClient();
@ -25,7 +25,7 @@ export function useUpdateEnvironmentsRelationsMutation() {
updateEnvironmentRelations,
mutationOptions(
withInvalidate(queryClient, [
queryKeys.base(),
environmentQueryKeys.base(),
edgeGroupQueryKeys.base(),
groupQueryKeys.base(),
tagKeys.all,

View file

@ -127,7 +127,7 @@ export type DeploymentOptions = {
/**
* EndpointChangeWindow determine when GitOps stack/app updates may occur
*/
interface EndpointChangeWindow {
export interface EndpointChangeWindow {
Enabled: boolean;
StartTime: string;
EndTime: string;

View file

@ -3,7 +3,7 @@ import { useRouter } from '@uirouter/react';
import _ from 'lodash';
import { Wand2 } from 'lucide-react';
import { useAnalytics } from '@/angulartics.matomo/analytics-services';
import { useAnalytics } from '@/react/hooks/useAnalytics';
import { Button } from '@@/buttons';
import { PageHeader } from '@@/PageHeader';

View file

@ -9,7 +9,7 @@ import {
Environment,
EnvironmentId,
} from '@/react/portainer/environments/types';
import { useAnalytics } from '@/angulartics.matomo/analytics-services';
import { useAnalytics } from '@/react/hooks/useAnalytics';
import { Stepper } from '@@/Stepper';
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';

View file

@ -1,7 +1,7 @@
import { Wand2, Plug2 } from 'lucide-react';
import { EnvironmentType } from '@/react/portainer/environments/types';
import { useAnalytics } from '@/angulartics.matomo/analytics-services';
import { useAnalytics } from '@/react/hooks/useAnalytics';
import DockerIcon from '@/assets/ico/vendor/docker-icon.svg?c';
import Kube from '@/assets/ico/kube.svg?c';