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

feat(edge/update): select endpoints to update [EE-4043] (#7602)

This commit is contained in:
Chaim Lev-Ari 2022-09-18 14:42:18 +03:00 committed by GitHub
parent 36e7981ab7
commit 4d123895ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1192 additions and 130 deletions

View file

@ -32,6 +32,7 @@ export function NavTabs<T extends string | number = string>({
className={clsx({
active: option.id === selectedId,
[styles.parent]: !option.children,
disabled,
})}
key={option.id}
>
@ -53,7 +54,7 @@ export function NavTabs<T extends string | number = string>({
))}
</ul>
{selected && selected.children && (
<div className="tab-content">{selected.children}</div>
<div className="tab-content mt-3">{selected.children}</div>
)}
</div>
);

View file

@ -40,7 +40,7 @@ export const Checkbox = forwardRef<HTMLInputElement, Props>(
}, [resolvedRef, indeterminate]);
return (
<div className="md-checkbox" title={title || label}>
<div className="md-checkbox flex" title={title || label}>
<input
id={id}
type="checkbox"

View file

@ -13,6 +13,10 @@ async function getEdgeGroups() {
}
}
export function useEdgeGroups() {
return useQuery(['edge', 'groups'], getEdgeGroups);
export function useEdgeGroups<T = EdgeGroup[]>({
select,
}: {
select?: (groups: EdgeGroup[]) => T;
} = {}) {
return useQuery(['edge', 'groups'], getEdgeGroups, { select });
}

View file

@ -16,7 +16,7 @@ import { ScheduleType } from '../types';
import { useCreateMutation } from '../queries/create';
import { FormValues } from '../common/types';
import { validation } from '../common/validation';
import { UpdateTypeTabs } from '../common/UpdateTypeTabs';
import { ScheduleTypeSelector } from '../common/ScheduleTypeSelector';
import { useList } from '../queries/list';
import { EdgeGroupsField } from '../common/EdgeGroupsField';
import { NameField } from '../common/NameField';
@ -25,8 +25,8 @@ const initialValues: FormValues = {
name: '',
groupIds: [],
type: ScheduleType.Update,
version: 'latest',
time: Math.floor(Date.now() / 1000) + 60 * 60,
environments: {},
};
export function CreateView() {
@ -56,23 +56,15 @@ export function CreateView() {
<Widget.Body>
<Formik
initialValues={initialValues}
onSubmit={(values) => {
createMutation.mutate(values, {
onSuccess() {
notifySuccess('Success', 'Created schedule successfully');
router.stateService.go('^');
},
});
}}
onSubmit={handleSubmit}
validateOnMount
validationSchema={() => validation(schedules)}
>
{({ isValid }) => (
<FormikForm className="form-horizontal">
<NameField />
<EdgeGroupsField />
<UpdateTypeTabs />
<ScheduleTypeSelector />
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
@ -93,4 +85,13 @@ export function CreateView() {
</div>
</>
);
function handleSubmit(values: FormValues) {
createMutation.mutate(values, {
onSuccess() {
notifySuccess('Success', 'Created schedule successfully');
router.stateService.go('^');
},
});
}
}

View file

@ -14,7 +14,7 @@ import { PageHeader } from '@@/PageHeader';
import { Widget } from '@@/Widget';
import { LoadingButton } from '@@/buttons';
import { UpdateTypeTabs } from '../common/UpdateTypeTabs';
import { ScheduleTypeSelector } from '../common/ScheduleTypeSelector';
import { useItem } from '../queries/useItem';
import { validation } from '../common/validation';
import { useUpdateMutation } from '../queries/useUpdateMutation';
@ -24,6 +24,8 @@ import { EdgeGroupsField } from '../common/EdgeGroupsField';
import { EdgeUpdateSchedule } from '../types';
import { FormValues } from '../common/types';
import { ScheduleDetails } from './ScheduleDetails';
export function ItemView() {
useRedirectFeatureFlag(FeatureFlag.EdgeRemoteUpdate);
@ -53,11 +55,28 @@ export function ItemView() {
const item = itemQuery.data;
const schedules = schedulesQuery.data;
const initialValues: FormValues = {
name: item.name,
groupIds: item.groupIds,
type: item.type,
time: item.time,
environments: Object.fromEntries(
Object.entries(item.status).map(([envId, status]) => [
parseInt(envId, 10),
status.targetVersion,
])
),
};
return (
<>
<PageHeader
title="Update & Rollback"
breadcrumbs={['Edge agent update and rollback', item.name]}
breadcrumbs={[
{ label: 'Edge agent update and rollback', link: '^' },
item.name,
]}
/>
<div className="row">
@ -66,7 +85,7 @@ export function ItemView() {
<Widget.Title title="Update & Rollback Scheduler" icon={Settings} />
<Widget.Body>
<Formik
initialValues={item}
initialValues={initialValues}
onSubmit={(values) => {
updateMutation.mutate(
{ id, values },
@ -82,7 +101,9 @@ export function ItemView() {
);
}}
validateOnMount
validationSchema={() => updateValidation(item, schedules)}
validationSchema={() =>
updateValidation(item.id, item.time, schedules)
}
>
{({ isValid }) => (
<FormikForm className="form-horizontal">
@ -90,7 +111,11 @@ export function ItemView() {
<EdgeGroupsField disabled={isDisabled} />
<UpdateTypeTabs disabled={isDisabled} />
{isDisabled ? (
<ScheduleDetails schedule={item} />
) : (
<ScheduleTypeSelector />
)}
<div className="form-group">
<div className="col-sm-12">
@ -115,10 +140,11 @@ export function ItemView() {
}
function updateValidation(
item: EdgeUpdateSchedule,
itemId: EdgeUpdateSchedule['id'],
scheduledTime: number,
schedules: EdgeUpdateSchedule[]
): SchemaOf<{ name: string } | FormValues> {
return item.time > Date.now() / 1000
? validation(schedules, item.id)
: object({ name: nameValidation(schedules, item.id) });
return scheduledTime > Date.now() / 1000
? validation(schedules, itemId)
: object({ name: nameValidation(schedules, itemId) });
}

View file

@ -0,0 +1,67 @@
import _ from 'lodash';
import { NavTabs } from '@@/NavTabs';
import { EdgeUpdateSchedule, ScheduleType } from '../types';
import { ScheduledTimeField } from '../common/ScheduledTimeField';
export function ScheduleDetails({
schedule,
}: {
schedule: EdgeUpdateSchedule;
}) {
return (
<div className="form-group">
<div className="col-sm-12">
<NavTabs
options={[
{
id: ScheduleType.Update,
label: 'Update',
children: <UpdateDetails schedule={schedule} />,
},
{
id: ScheduleType.Rollback,
label: 'Rollback',
children: <UpdateDetails schedule={schedule} />,
},
]}
selectedId={schedule.type}
onSelect={() => {}}
disabled
/>
</div>
</div>
);
}
function UpdateDetails({ schedule }: { schedule: EdgeUpdateSchedule }) {
const schedulesCount = Object.values(
_.groupBy(
schedule.status,
(status) => `${status.currentVersion}-${status.targetVersion}`
)
).map((statuses) => ({
count: statuses.length,
currentVersion: statuses[0].currentVersion,
targetVersion: statuses[0].targetVersion,
}));
return (
<>
<div className="form-group">
<div className="col-sm-12">
{schedulesCount.map(({ count, currentVersion, targetVersion }) => (
<div key={`${currentVersion}-${targetVersion}`}>
{count} edge device(s) selected for{' '}
{schedule.type === ScheduleType.Rollback ? 'rollback' : 'update'}{' '}
from v{currentVersion} to v{targetVersion}
</div>
))}
</div>
</div>
<ScheduledTimeField disabled />
</>
);
}

View file

@ -28,13 +28,13 @@ function StatusCell({
return 'No related environments';
}
const error = statusList.find((s) => s.Type === StatusType.Failed);
const error = statusList.find((s) => s.status === StatusType.Failed);
if (error) {
return `Failed: (ID: ${error.environmentId}) ${error.Error}`;
return `Failed: (ID: ${error.environmentId}) ${error.error}`;
}
const pending = statusList.find((s) => s.Type === StatusType.Pending);
const pending = statusList.find((s) => s.status === StatusType.Pending);
if (pending) {
return 'Pending';

View file

@ -0,0 +1,109 @@
import _ from 'lodash';
import { Clock } from 'react-feather';
import { Environment } from '@/portainer/environments/types';
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { ActiveSchedule } from '../queries/useActiveSchedules';
import { ScheduleType } from '../types';
export function ActiveSchedulesNotice({
selectedEdgeGroupIds,
activeSchedules,
environments,
}: {
selectedEdgeGroupIds: EdgeGroup['Id'][];
activeSchedules: ActiveSchedule[];
environments: Environment[];
}) {
const groupsQuery = useEdgeGroups();
if (!groupsQuery.data) {
return null;
}
// environmentId -> {currentVersion, targetVersion}
const environmentScheduleGroup = Object.fromEntries(
activeSchedules.map((schedule) => [
schedule.environmentId,
{
currentVersion:
environments.find((env) => env.Id === schedule.environmentId)?.Agent
.Version || '',
targetVersion: schedule.targetVersion,
type: schedule.type,
},
])
);
const edgeGroups = groupsQuery.data
.filter((edgeGroup) => selectedEdgeGroupIds.includes(edgeGroup.Id))
.map((edgeGroup) => ({
edgeGroupId: edgeGroup.Id,
edgeGroupName: edgeGroup.Name,
schedules: Object.values(
_.groupBy(
_.compact(
edgeGroup.Endpoints.map((eId) => environmentScheduleGroup[eId])
),
(schedule) =>
`${schedule.currentVersion}_${schedule.targetVersion}_${schedule.type}`
)
).map((schedules) => ({
currentVersion: schedules[0].currentVersion,
targetVersion: schedules[0].targetVersion,
scheduleCount: schedules.length,
type: schedules[0].type,
})),
}))
.filter((group) => group.schedules.length > 0);
if (edgeGroups.length === 0) {
return null;
}
return (
<div className="form-group">
<div className="col-sm-12 space-y-1">
{edgeGroups.map(({ edgeGroupId, edgeGroupName, schedules }) =>
schedules.map(
({ currentVersion, scheduleCount, targetVersion, type }) => (
<ActiveSchedulesNoticeItem
currentVersion={currentVersion || 'unknown version'}
key={`${edgeGroupId}-${currentVersion}-${targetVersion}`}
name={edgeGroupName}
scheduleCount={scheduleCount}
version={targetVersion}
scheduleType={type}
/>
)
)
)}
</div>
</div>
);
}
function ActiveSchedulesNoticeItem({
name,
scheduleCount,
version,
currentVersion,
scheduleType,
}: {
name: string;
scheduleCount: number;
version: string;
currentVersion: string;
scheduleType: ScheduleType;
}) {
return (
<div className="flex items-center gap-1 text-sm">
<Clock className="feather" />
{scheduleCount} edge devices in {name} are scheduled for{' '}
{scheduleType === ScheduleType.Rollback ? 'rollback' : 'update'} from{' '}
{currentVersion} to {version}
</div>
);
}

View file

@ -0,0 +1,73 @@
import _ from 'lodash';
import { Environment } from '@/portainer/environments/types';
import { TextTip } from '@@/Tip/TextTip';
import { ActiveSchedule } from '../queries/useActiveSchedules';
import { useSupportedAgentVersions } from '../queries/useSupportedAgentVersions';
import { EnvironmentSelectionItem } from './EnvironmentSelectionItem';
import { compareVersion } from './utils';
interface Props {
environments: Environment[];
activeSchedules: ActiveSchedule[];
disabled?: boolean;
}
export function EnvironmentSelection({
environments,
activeSchedules,
disabled,
}: Props) {
const supportedAgentVersionsQuery = useSupportedAgentVersions({
select: (versions) =>
versions.map((version) => ({ label: version, value: version })),
});
if (!supportedAgentVersionsQuery.data) {
return null;
}
const supportedAgentVersions = supportedAgentVersionsQuery.data;
const latestVersion = _.last(supportedAgentVersions)?.value;
const environmentsToUpdate = environments.filter(
(env) =>
activeSchedules.every((schedule) => schedule.environmentId !== env.Id) &&
compareVersion(env.Agent.Version, latestVersion)
);
const versionGroups = Object.entries(
_.mapValues(
_.groupBy(environmentsToUpdate, (env) => env.Agent.Version),
(envs) => envs.map((env) => env.Id)
)
);
if (environmentsToUpdate.length === 0) {
return (
<TextTip>
The are no update options available for yor selected groups(s)
</TextTip>
);
}
return (
<div className="form-group">
<div className="col-sm-12">
{versionGroups.map(([version, environmentIds]) => (
<EnvironmentSelectionItem
currentVersion={version}
environmentIds={environmentIds}
key={version}
versions={supportedAgentVersions}
disabled={disabled}
/>
))}
</div>
</div>
);
}

View file

@ -0,0 +1,85 @@
import { useField } from 'formik';
import _ from 'lodash';
import { useState, ChangeEvent } from 'react';
import { EnvironmentId } from '@/portainer/environments/types';
import { Select } from '@@/form-components/Input';
import { Checkbox } from '@@/form-components/Checkbox';
import { FormValues } from './types';
import { compareVersion } from './utils';
interface Props {
currentVersion: string;
environmentIds: EnvironmentId[];
versions: { label: string; value: string }[];
disabled?: boolean;
}
export function EnvironmentSelectionItem({
environmentIds,
versions,
currentVersion = 'unknown',
disabled,
}: Props) {
const [{ value }, , { setValue }] =
useField<FormValues['environments']>('environments');
const isChecked = environmentIds.every((envId) => !!value[envId]);
const supportedVersions = versions.filter(
({ value }) => compareVersion(currentVersion, value) // versions that are bigger than the current version
);
const maxVersion = _.last(supportedVersions)?.value;
const [selectedVersion, setSelectedVersion] = useState(
value[environmentIds[0]] || maxVersion || ''
);
return (
<div className="flex items-center">
<Checkbox
className="flex items-center"
id={`version_checkbox_${currentVersion}`}
checked={isChecked}
onChange={() => handleChange(!isChecked)}
disabled={disabled}
/>
<span className="font-normal flex items-center whitespace-nowrap gap-1">
{environmentIds.length} edge agents update from v{currentVersion} to
<Select
disabled={disabled}
value={selectedVersion}
options={supportedVersions}
onChange={handleVersionChange}
/>
</span>
</div>
);
function handleVersionChange(e: ChangeEvent<HTMLSelectElement>) {
const version = e.target.value;
setSelectedVersion(version);
if (isChecked) {
handleChange(isChecked, version);
}
}
function handleChange(isChecked: boolean, version: string = selectedVersion) {
const newValue = !isChecked
? Object.fromEntries(
Object.entries(value).filter(
([envId]) => !environmentIds.includes(parseInt(envId, 10))
)
)
: {
...value,
...Object.fromEntries(
environmentIds.map((envId) => [envId, version])
),
};
setValue(newValue);
}
}

View file

@ -0,0 +1,95 @@
import { useFormikContext } from 'formik';
import { useEffect, useMemo } from 'react';
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
import { TextTip } from '@@/Tip/TextTip';
import { usePreviousVersions } from '../queries/usePreviousVersions';
import { FormValues } from './types';
import { useEdgeGroupsEnvironmentIds } from './useEdgeGroupsEnvironmentIds';
import { ScheduledTimeField } from './ScheduledTimeField';
export function RollbackScheduleDetailsFieldset() {
const environmentsCount = useSelectedEnvironmentsCount();
const { isLoading } = useSelectEnvironmentsOnMount();
const groupNames = useGroupNames();
if (isLoading || !groupNames) {
return null;
}
return (
<div className="mt-3">
{environmentsCount > 0 ? (
<div className="form-group">
<div className="col-sm-12">
{environmentsCount} edge device(s) from {groupNames} will rollback
to their previous versions
</div>
</div>
) : (
<TextTip>
The are no rollback options available for yor selected groups(s)
</TextTip>
)}
<ScheduledTimeField />
</div>
);
}
function useSelectedEnvironmentsCount() {
const {
values: { environments },
} = useFormikContext<FormValues>();
return Object.keys(environments).length;
}
function useSelectEnvironmentsOnMount() {
const previousVersionsQuery = usePreviousVersions();
const {
values: { groupIds },
setFieldValue,
} = useFormikContext<FormValues>();
const edgeGroupsEnvironmentIds = useEdgeGroupsEnvironmentIds(groupIds);
const envIdsToUpdate = useMemo(
() =>
previousVersionsQuery.data
? Object.fromEntries(
edgeGroupsEnvironmentIds
.map((id) => [id, previousVersionsQuery.data[id] || ''] as const)
.filter(([, version]) => !!version)
)
: [],
[edgeGroupsEnvironmentIds, previousVersionsQuery.data]
);
useEffect(() => {
setFieldValue('environments', envIdsToUpdate);
}, [envIdsToUpdate, setFieldValue]);
return { isLoading: previousVersionsQuery.isLoading };
}
function useGroupNames() {
const {
values: { groupIds },
} = useFormikContext<FormValues>();
const groupsQuery = useEdgeGroups({
select: (groups) => Object.fromEntries(groups.map((g) => [g.Id, g.Name])),
});
if (!groupsQuery.data) {
return null;
}
return groupIds.map((id) => groupsQuery.data[id]).join(', ');
}

View file

@ -6,13 +6,10 @@ import { NavTabs } from '@@/NavTabs';
import { ScheduleType } from '../types';
import { FormValues } from './types';
import { ScheduledTimeField } from './ScheduledTimeField';
import { UpdateScheduleDetailsFieldset } from './UpdateScheduleDetailsFieldset';
import { RollbackScheduleDetailsFieldset } from './RollbackScheduleDetailsFieldset';
interface Props {
disabled?: boolean;
}
export function UpdateTypeTabs({ disabled }: Props) {
export function ScheduleTypeSelector() {
const [{ value }, , { setValue }] = useField<FormValues['type']>('type');
return (
@ -23,31 +20,22 @@ export function UpdateTypeTabs({ disabled }: Props) {
{
id: ScheduleType.Update,
label: 'Update',
children: <ScheduleDetails disabled={disabled} />,
children: <UpdateScheduleDetailsFieldset />,
},
{
id: ScheduleType.Rollback,
label: 'Rollback',
children: <ScheduleDetails disabled={disabled} />,
children: <RollbackScheduleDetailsFieldset />,
},
]}
selectedId={value}
onSelect={(value) => setValue(value)}
disabled={disabled}
/>
</div>
</div>
);
}
function ScheduleDetails({ disabled }: Props) {
return (
<div>
<ScheduledTimeField disabled={disabled} />
</div>
);
}
export function typeValidation() {
return number()
.oneOf([ScheduleType.Rollback, ScheduleType.Update])

View file

@ -0,0 +1,64 @@
import { useFormikContext } from 'formik';
import { useCurrentStateAndParams } from '@uirouter/react';
import { EdgeTypes, EnvironmentId } from '@/portainer/environments/types';
import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList';
import { useActiveSchedules } from '../queries/useActiveSchedules';
import { ScheduledTimeField } from './ScheduledTimeField';
import { FormValues } from './types';
import { EnvironmentSelection } from './EnvironmentSelection';
import { ActiveSchedulesNotice } from './ActiveSchedulesNotice';
import { useEdgeGroupsEnvironmentIds } from './useEdgeGroupsEnvironmentIds';
export function UpdateScheduleDetailsFieldset() {
const { values } = useFormikContext<FormValues>();
const edgeGroupsEnvironmentIds = useEdgeGroupsEnvironmentIds(values.groupIds);
const environments = useEnvironments(edgeGroupsEnvironmentIds);
const activeSchedules = useRelevantActiveSchedules(edgeGroupsEnvironmentIds);
return (
<>
<ActiveSchedulesNotice
selectedEdgeGroupIds={values.groupIds}
activeSchedules={activeSchedules}
environments={environments}
/>
<EnvironmentSelection
activeSchedules={activeSchedules}
environments={environments}
/>
<ScheduledTimeField />
</>
);
}
function useEnvironments(environmentsIds: Array<EnvironmentId>) {
const environmentsQuery = useEnvironmentList(
{ endpointIds: environmentsIds, types: EdgeTypes },
undefined,
undefined,
environmentsIds.length > 0
);
return environmentsQuery.environments;
}
function useRelevantActiveSchedules(environmentIds: EnvironmentId[]) {
const { params } = useCurrentStateAndParams();
const scheduleId = params.id ? parseInt(params.id, 10) : 0;
const activeSchedulesQuery = useActiveSchedules(environmentIds);
return (
activeSchedulesQuery.data?.filter(
(schedule) => schedule.scheduleId !== scheduleId
) || []
);
}

View file

@ -1,3 +1,4 @@
import { EnvironmentId } from '@/portainer/environments/types';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
import { ScheduleType } from '../types';
@ -6,6 +7,6 @@ export interface FormValues {
name: string;
groupIds: EdgeGroup['Id'][];
type: ScheduleType;
version: string;
time: number;
environments: Record<EnvironmentId, string>;
}

View file

@ -0,0 +1,26 @@
import _ from 'lodash';
import { useMemo } from 'react';
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
import { EdgeGroup } from '@/react/edge/edge-groups/types';
export function useEdgeGroupsEnvironmentIds(
edgeGroupsIds: Array<EdgeGroup['Id']>
) {
const groupsQuery = useEdgeGroups({
select: (groups) =>
Object.fromEntries(groups.map((g) => [g.Id, g.Endpoints])),
});
return useMemo(
() =>
_.uniq(
_.compact(
edgeGroupsIds.flatMap((id) =>
groupsQuery.data ? groupsQuery.data[id] : []
)
)
),
[edgeGroupsIds, groupsQuery.data]
);
}

View file

@ -0,0 +1,23 @@
import semverCompare from 'semver-compare';
export function compareVersion(
currentVersion: string,
version = '',
bigger = false
) {
if (!currentVersion) {
return true;
}
// if supplied version is not a string, e.g develop
if (!version.includes('.')) {
return true;
}
if (bigger) {
return semverCompare(currentVersion, version) > 0;
}
// env version is less than the supplied
return semverCompare(currentVersion, version) < 0;
}

View file

@ -1,15 +1,14 @@
import { array, number, object, SchemaOf, string } from 'yup';
import { array, number, object } from 'yup';
import { EdgeUpdateSchedule } from '../types';
import { nameValidation } from './NameField';
import { FormValues } from './types';
import { typeValidation } from './UpdateTypeTabs';
import { typeValidation } from './ScheduleTypeSelector';
export function validation(
schedules: EdgeUpdateSchedule[],
currentId?: EdgeUpdateSchedule['id']
): SchemaOf<FormValues> {
) {
return object({
groupIds: array().min(1, 'At least one group is required'),
name: nameValidation(schedules, currentId),
@ -17,6 +16,6 @@ export function validation(
time: number()
.min(Date.now() / 1000)
.required(),
version: string().required(),
environments: object().default({}),
});
}

View file

@ -1,6 +1,12 @@
import { EnvironmentId } from '@/portainer/environments/types';
import { EdgeUpdateSchedule } from '../types';
export const queryKeys = {
list: () => ['edge', 'update_schedules'] as const,
item: (id: EdgeUpdateSchedule['id']) => [...queryKeys.list(), id] as const,
activeSchedules: (environmentIds: EnvironmentId[]) =>
[queryKeys.list(), 'active', { environmentIds }] as const,
supportedAgentVersions: () => [queryKeys.list(), 'agent_versions'] as const,
previousVersions: () => [queryKeys.list(), 'previous_versions'] as const,
};

View file

@ -2,6 +2,16 @@ import { EdgeUpdateSchedule } from '../types';
export const BASE_URL = '/edge_update_schedules';
export function buildUrl(id?: EdgeUpdateSchedule['id']) {
return !id ? BASE_URL : `${BASE_URL}/${id}`;
export function buildUrl(id?: EdgeUpdateSchedule['id'], action?: string) {
let url = BASE_URL;
if (id) {
url += `/${id}`;
}
if (action) {
url += `/${action}`;
}
return url;
}

View file

@ -0,0 +1,41 @@
import { useQuery } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/portainer/environments/types';
import { EdgeUpdateSchedule, ScheduleType, StatusType } from '../types';
import { queryKeys } from './query-keys';
import { buildUrl } from './urls';
export interface ActiveSchedule {
environmentId: EnvironmentId;
scheduleId: EdgeUpdateSchedule['id'];
targetVersion: string;
status: StatusType;
error: string;
type: ScheduleType;
}
async function getActiveSchedules(environmentIds: EnvironmentId[]) {
try {
const { data } = await axios.post<ActiveSchedule[]>(
buildUrl(undefined, 'active'),
{ environmentIds }
);
return data;
} catch (err) {
throw parseAxiosError(
err as Error,
'Failed to get list of edge update schedules'
);
}
}
export function useActiveSchedules(environmentIds: EnvironmentId[]) {
return useQuery(
queryKeys.activeSchedules(environmentIds),
() => getActiveSchedules(environmentIds),
{ enabled: environmentIds.length > 0 }
);
}

View file

@ -0,0 +1,29 @@
import { useQuery } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/portainer/environments/types';
import { queryKeys } from './query-keys';
import { buildUrl } from './urls';
export function usePreviousVersions<T = Record<EnvironmentId, string>>({
select,
}: { select?: (data: Record<EnvironmentId, string>) => T } = {}) {
return useQuery(queryKeys.previousVersions(), getPreviousVersions, {
select,
});
}
async function getPreviousVersions() {
try {
const { data } = await axios.get<Record<EnvironmentId, string>>(
buildUrl(undefined, 'previous_versions')
);
return data;
} catch (err) {
throw parseAxiosError(
err as Error,
'Failed to get list of edge update schedules'
);
}
}

View file

@ -0,0 +1,30 @@
import { useQuery } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { queryKeys } from './query-keys';
import { buildUrl } from './urls';
export function useSupportedAgentVersions<T = string[]>({
select,
}: { select?: (data: string[]) => T } = {}) {
return useQuery(
queryKeys.supportedAgentVersions(),
getSupportedAgentVersions,
{ select }
);
}
async function getSupportedAgentVersions() {
try {
const { data } = await axios.get<string[]>(
buildUrl(undefined, 'agent_versions')
);
return data;
} catch (err) {
throw parseAxiosError(
err as Error,
'Failed to get list of edge update schedules'
);
}
}

View file

@ -14,8 +14,10 @@ export enum StatusType {
}
interface Status {
Type: StatusType;
Error: string;
status: StatusType;
error: string;
targetVersion: string;
currentVersion: string;
}
export type EdgeUpdateSchedule = {
@ -27,5 +29,4 @@ export type EdgeUpdateSchedule = {
status: { [key: EnvironmentId]: Status };
created: number;
createdBy: UserId;
version: string;
};