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:
parent
0f1e77a6d5
commit
515b02813b
59 changed files with 1819 additions and 833 deletions
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { TimeWindowPicker } from './TimeWindowPicker';
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue