diff --git a/README.md b/README.md index 27837d521..40658a715 100644 --- a/README.md +++ b/README.md @@ -33,5 +33,5 @@ yarn test To run a particular workspace's unit tests from root: ```bash -yarn test --selectProjects [workspace-name] +yarn test --project [workspace-name] ``` diff --git a/apps/parsley/src/gql/generated/types.ts b/apps/parsley/src/gql/generated/types.ts index a2acfeaaf..2476c67d2 100644 --- a/apps/parsley/src/gql/generated/types.ts +++ b/apps/parsley/src/gql/generated/types.ts @@ -1091,8 +1091,7 @@ export type MutationAddAnnotationIssueArgs = { }; export type MutationAddFavoriteProjectArgs = { - identifier?: InputMaybe; - opts?: InputMaybe; + opts: AddFavoriteProjectInput; }; export type MutationAttachProjectToNewRepoArgs = { @@ -1135,16 +1134,11 @@ export type MutationCreatePublicKeyArgs = { }; export type MutationDeactivateStepbackTaskArgs = { - buildVariantName?: InputMaybe; - opts?: InputMaybe; - projectId?: InputMaybe; - taskName?: InputMaybe; + opts: DeactivateStepbackTaskInput; }; export type MutationDefaultSectionToRepoArgs = { - opts?: InputMaybe; - projectId?: InputMaybe; - section?: InputMaybe; + opts: DefaultSectionToRepoInput; }; export type MutationDeleteDistroArgs = { @@ -1204,9 +1198,7 @@ export type MutationOverrideTaskDependenciesArgs = { }; export type MutationPromoteVarsToRepoArgs = { - opts?: InputMaybe; - projectId?: InputMaybe; - varNames?: InputMaybe>; + opts: PromoteVarsToRepoInput; }; export type MutationRemoveAnnotationIssueArgs = { @@ -1217,8 +1209,7 @@ export type MutationRemoveAnnotationIssueArgs = { }; export type MutationRemoveFavoriteProjectArgs = { - identifier?: InputMaybe; - opts?: InputMaybe; + opts: RemoveFavoriteProjectInput; }; export type MutationRemoveItemFromCommitQueueArgs = { @@ -2095,14 +2086,12 @@ export type QueryProjectArgs = { export type QueryProjectEventsArgs = { before?: InputMaybe; - identifier?: InputMaybe; limit?: InputMaybe; - projectIdentifier?: InputMaybe; + projectIdentifier: Scalars["String"]["input"]; }; export type QueryProjectSettingsArgs = { - identifier?: InputMaybe; - projectIdentifier?: InputMaybe; + projectIdentifier: Scalars["String"]["input"]; }; export type QueryRepoEventsArgs = { @@ -2604,8 +2593,6 @@ export type Task = { status: Scalars["String"]["output"]; stepbackInfo?: Maybe; tags: Array; - /** @deprecated Use files instead */ - taskFiles: TaskFiles; taskGroup?: Maybe; taskGroupMaxHosts?: Maybe; /** taskLogs returns the tail 100 lines of the task's logs. */ @@ -2689,8 +2676,6 @@ export type TaskFiles = { /** TaskFilterOptions defines the parameters that are used when fetching tasks from a Version. */ export type TaskFilterOptions = { baseStatuses?: InputMaybe>; - /** @deprecated Use includeNeverActivatedTasks instead */ - includeEmptyActivation?: InputMaybe; includeNeverActivatedTasks?: InputMaybe; limit?: InputMaybe; page?: InputMaybe; diff --git a/apps/spruce/cypress/integration/projectSettings/access.ts b/apps/spruce/cypress/integration/projectSettings/access.ts index d50d0d1ab..831d6d898 100644 --- a/apps/spruce/cypress/integration/projectSettings/access.ts +++ b/apps/spruce/cypress/integration/projectSettings/access.ts @@ -38,6 +38,7 @@ describe("Access page", () => { cy.validateToast("success", "Successfully updated project"); // Assert persistence cy.reload(); + saveButtonEnabled(false); cy.get("@usernameInput").should("not.exist"); }); diff --git a/apps/spruce/src/components/DayPicker/index.tsx b/apps/spruce/src/components/DayPicker/index.tsx index f6b756c3d..1ec9fac0f 100644 --- a/apps/spruce/src/components/DayPicker/index.tsx +++ b/apps/spruce/src/components/DayPicker/index.tsx @@ -2,19 +2,11 @@ import { useCallback, useState } from "react"; import styled from "@emotion/styled"; import { palette } from "@leafygreen-ui/palette"; import { transitionDuration } from "@leafygreen-ui/tokens"; +import { days } from "constants/fieldMaps"; import { size } from "constants/tokens"; const { gray, white } = palette; -const days = [ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", -]; const emptyState = new Array(days.length).fill(false); type DayPickerState = Array; diff --git a/apps/spruce/src/components/Spawn/editHostModal/getFormSchema.tsx b/apps/spruce/src/components/Spawn/editHostModal/getFormSchema.tsx index 10c20765c..cc992bee7 100644 --- a/apps/spruce/src/components/Spawn/editHostModal/getFormSchema.tsx +++ b/apps/spruce/src/components/Spawn/editHostModal/getFormSchema.tsx @@ -15,9 +15,8 @@ interface Props { canEditRdpPassword: boolean; canEditSshKeys: boolean; disableExpirationCheckbox: boolean; - hostUptimeValidation?: { + hostUptimeWarnings?: { enabledHoursCount: number; - errors: string[]; warnings: string[]; }; instanceTypes: string[]; @@ -32,7 +31,7 @@ export const getFormSchema = ({ canEditRdpPassword, canEditSshKeys, disableExpirationCheckbox, - hostUptimeValidation, + hostUptimeWarnings, instanceTypes, myPublicKeys, noExpirationCheckboxTooltip, @@ -41,7 +40,7 @@ export const getFormSchema = ({ }: Props): ReturnType => { const expirationDetails = getExpirationDetailsSchema({ disableExpirationCheckbox, - hostUptimeValidation, + hostUptimeWarnings, noExpirationCheckboxTooltip, timeZone, }); diff --git a/apps/spruce/src/components/Spawn/getFormSchema.tsx b/apps/spruce/src/components/Spawn/getFormSchema.tsx index a0723f14e..7a0b63051 100644 --- a/apps/spruce/src/components/Spawn/getFormSchema.tsx +++ b/apps/spruce/src/components/Spawn/getFormSchema.tsx @@ -16,16 +16,15 @@ import { const today = new Date(); type HostUptimeProps = { - hostUptimeValidation?: { + hostUptimeWarnings?: { enabledHoursCount: number; - errors: string[]; warnings: string[]; }; timeZone?: string; }; const getHostUptimeSchema = ({ - hostUptimeValidation, + hostUptimeWarnings, timeZone, }: HostUptimeProps) => ({ schema: { @@ -160,12 +159,11 @@ const getHostUptimeSchema = ({ "ui:descriptionNode": (
), "ui:showLabel": false, - "ui:warnings": hostUptimeValidation?.warnings, - "ui:errors": hostUptimeValidation?.errors, + "ui:warnings": hostUptimeWarnings?.warnings, }, }, }); @@ -183,9 +181,8 @@ const Details: React.FC<{ timeZone: string; totalUptimeHours: number }> = ({ type ExpirationProps = { disableExpirationCheckbox: boolean; - hostUptimeValidation?: { + hostUptimeWarnings?: { enabledHoursCount: number; - errors: string[]; warnings: string[]; }; noExpirationCheckboxTooltip?: string; @@ -194,12 +191,12 @@ type ExpirationProps = { export const getExpirationDetailsSchema = ({ disableExpirationCheckbox, - hostUptimeValidation, + hostUptimeWarnings, noExpirationCheckboxTooltip, timeZone, }: ExpirationProps) => { const defaultExpiration = getDefaultExpiration(); - const hostUptime = getHostUptimeSchema({ hostUptimeValidation, timeZone }); + const hostUptime = getHostUptimeSchema({ hostUptimeWarnings, timeZone }); return { schema: { title: "Expiration Details", diff --git a/apps/spruce/src/components/Spawn/index.tsx b/apps/spruce/src/components/Spawn/index.tsx index a0f4c22d7..93e365d81 100644 --- a/apps/spruce/src/components/Spawn/index.tsx +++ b/apps/spruce/src/components/Spawn/index.tsx @@ -3,7 +3,9 @@ export { DetailsCard } from "./DetailsCard"; export { MountVolumeSelect } from "./MountVolumeSelect"; export { defaultSleepSchedule, + getEnabledHoursCount, getHostUptimeFromGql, - validateUptimeSchedule, + getHostUptimeWarnings, + isNullSleepSchedule, validator, } from "./utils"; diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/getFormSchema.tsx b/apps/spruce/src/components/Spawn/spawnHostModal/getFormSchema.tsx index 740cefa89..9ba434055 100644 --- a/apps/spruce/src/components/Spawn/spawnHostModal/getFormSchema.tsx +++ b/apps/spruce/src/components/Spawn/spawnHostModal/getFormSchema.tsx @@ -25,9 +25,8 @@ interface Props { isVirtualWorkStation: boolean; name?: string; }[]; - hostUptimeValidation?: { + hostUptimeWarnings?: { enabledHoursCount: number; - errors: string[]; warnings: string[]; }; isMigration: boolean; @@ -47,7 +46,7 @@ export const getFormSchema = ({ disableExpirationCheckbox, distroIdQueryParam, distros, - hostUptimeValidation, + hostUptimeWarnings, isMigration, isVirtualWorkstation, myPublicKeys, @@ -74,7 +73,7 @@ export const getFormSchema = ({ const expirationDetails = getExpirationDetailsSchema({ disableExpirationCheckbox, - hostUptimeValidation, + hostUptimeWarnings, noExpirationCheckboxTooltip, timeZone, }); diff --git a/apps/spruce/src/components/Spawn/utils/hostUptime.test.ts b/apps/spruce/src/components/Spawn/utils/hostUptime.test.ts index b99e877f2..0f176dd56 100644 --- a/apps/spruce/src/components/Spawn/utils/hostUptime.test.ts +++ b/apps/spruce/src/components/Spawn/utils/hostUptime.test.ts @@ -1,8 +1,9 @@ import { getHostUptimeFromGql, + getHostUptimeWarnings, + getNextHostStart, getSleepSchedule, matchesDefaultUptimeSchedule, - validateUptimeSchedule, validator, } from "./hostUptime"; @@ -81,7 +82,7 @@ describe("validator", () => { }, }, // @ts-expect-error - { expirationDetails: { hostUptime: { addError: f } } }, + { expirationDetails: { hostUptime: { details: { addError: f } } } }, ); expect(f).toHaveBeenCalledTimes(0); }); @@ -106,7 +107,7 @@ describe("validator", () => { }, }, // @ts-expect-error - { expirationDetails: { hostUptime: { addError: f } } }, + { expirationDetails: { hostUptime: { details: { addError: f } } } }, ); expect(f).toHaveBeenCalledTimes(1); }); @@ -131,7 +132,7 @@ describe("validator", () => { }, }, // @ts-expect-error - { expirationDetails: { hostUptime: { addError: f } } }, + { expirationDetails: { hostUptime: { details: { addError: f } } } }, ); expect(f).toHaveBeenCalledTimes(0); }); @@ -282,58 +283,100 @@ describe("getSleepSchedule", () => { }); }); -describe("validateUptimeSchedule", () => { +describe("getHostUptimeWarnings", () => { it("returns no errors when under recommended time", () => { expect( - validateUptimeSchedule({ - enabledWeekdays: [false, false, true, true, true, true, false], + getHostUptimeWarnings({ + enabledHoursCount: 60, + enabledWeekdaysCount: 5, runContinuously: false, - startTime: - "Sun Dec 31 1899 08:00:00 GMT+0000 (Coordinated Universal Time)", - stopTime: - "Sun Dec 31 1899 20:00:00 GMT+0000 (Coordinated Universal Time)", - useDefaultUptimeSchedule: true, }), - ).toStrictEqual({ - enabledHoursCount: 60, - errors: [], - warnings: [], - }); + ).toStrictEqual([]); }); it("returns a warning when over recommended time", () => { expect( - validateUptimeSchedule({ - enabledWeekdays: [false, true, true, true, true, true, true], + getHostUptimeWarnings({ + enabledHoursCount: 144, + enabledWeekdaysCount: 6, runContinuously: true, - startTime: - "Sun Dec 31 1899 08:00:00 GMT+0000 (Coordinated Universal Time)", - stopTime: - "Sun Dec 31 1899 20:00:00 GMT+0000 (Coordinated Universal Time)", - useDefaultUptimeSchedule: false, }), - ).toStrictEqual({ - enabledHoursCount: 144, - errors: [], - warnings: ["Consider pausing your host for 2 days per week."], - }); + ).toStrictEqual(["Consider pausing your host for 2 days per week."]); }); - it("returns an error when over allowed time", () => { + it("does not return a warning when over allowed time", () => { expect( - validateUptimeSchedule({ - enabledWeekdays: [true, true, true, true, true, true, true], + getHostUptimeWarnings({ + enabledHoursCount: 168, + enabledWeekdaysCount: 7, runContinuously: true, - startTime: - "Sun Dec 31 1899 08:00:00 GMT+0000 (Coordinated Universal Time)", - stopTime: - "Sun Dec 31 1899 20:00:00 GMT+0000 (Coordinated Universal Time)", - useDefaultUptimeSchedule: false, }), - ).toStrictEqual({ - enabledHoursCount: 168, - errors: ["Please pause your host for at least 1 day per week."], - warnings: [], + ).toStrictEqual([]); + }); +}); + +describe("getNextHostStart", () => { + it("calculates the next start with time", () => { + const sched = { + dailyStartTime: "08:00", + dailyStopTime: "20:00", + permanentlyExempt: true, + shouldKeepOff: true, + timeZone: "America/New_York", + wholeWeekdaysOff: [0, 6], + }; + const monday = new Date(null, null); + expect(getNextHostStart(sched, monday)).toStrictEqual({ + nextStartDay: "Tuesday", + nextStartTime: "8:00", + }); + }); + + it("calculates the next start with time when current day is off", () => { + const sched = { + dailyStartTime: "08:00", + dailyStopTime: "20:00", + permanentlyExempt: true, + shouldKeepOff: true, + timeZone: "America/New_York", + wholeWeekdaysOff: [0, 1, 6], + }; + const monday = new Date(null, null); + expect(getNextHostStart(sched, monday)).toStrictEqual({ + nextStartDay: "Tuesday", + nextStartTime: "8:00", + }); + }); + + it("calculates the next start when running continuously", () => { + const sched = { + dailyStartTime: "", + dailyStopTime: "", + permanentlyExempt: true, + shouldKeepOff: true, + timeZone: "America/New_York", + wholeWeekdaysOff: [0, 6], + }; + const monday = new Date(null, null); + expect(getNextHostStart(sched, monday)).toStrictEqual({ + nextStartDay: "Monday", + nextStartTime: null, + }); + }); + + it("calculates the next start when running continuously and current day is off", () => { + const sched = { + dailyStartTime: "", + dailyStopTime: "", + permanentlyExempt: true, + shouldKeepOff: true, + timeZone: "America/New_York", + wholeWeekdaysOff: [0, 1, 6], + }; + const monday = new Date(null, null); + expect(getNextHostStart(sched, monday)).toStrictEqual({ + nextStartDay: "Tuesday", + nextStartTime: null, }); }); }); diff --git a/apps/spruce/src/components/Spawn/utils/hostUptime.ts b/apps/spruce/src/components/Spawn/utils/hostUptime.ts index 8fe9a7f55..b8492ddba 100644 --- a/apps/spruce/src/components/Spawn/utils/hostUptime.ts +++ b/apps/spruce/src/components/Spawn/utils/hostUptime.ts @@ -1,7 +1,8 @@ -import { differenceInHours, parse } from "date-fns"; +import { differenceInHours, isTomorrow, parse } from "date-fns"; import { ValidateProps } from "components/SpruceForm"; -import { SleepScheduleInput } from "gql/generated/types"; -import { MyHost } from "types/spawn"; +import { days } from "constants/fieldMaps"; +import { SleepSchedule, SleepScheduleInput } from "gql/generated/types"; +import { Optional } from "types/utils"; import { arraySymmetricDifference } from "utils/array"; import { isProduction } from "utils/environmentVariables"; @@ -10,6 +11,7 @@ const hoursInDay = 24; const defaultStartHour = 8; const defaultStopHour = 20; const defaultScheduleWeeklyHourCount = 60; +const defaultScheduleWeekdaysCount = 5; const suggestedUptimeHours = (daysInWeek - 2) * hoursInDay; export const maxUptimeHours = (daysInWeek - 1) * hoursInDay; @@ -27,95 +29,67 @@ export type HostUptime = { runContinuously: boolean; }; }; + details?: null; }; type ValidateInput = { - enabledWeekdays: boolean[]; - stopTime: string; + enabledHoursCount: number; + enabledWeekdaysCount: number; runContinuously: boolean; - startTime: string; - useDefaultUptimeSchedule: boolean; }; -export const validateUptimeSchedule = ({ - enabledWeekdays, +export const getHostUptimeWarnings = ({ + enabledHoursCount, + enabledWeekdaysCount, runContinuously, - startTime, - stopTime, - useDefaultUptimeSchedule, -}: ValidateInput): { - enabledHoursCount: number; - errors: string[]; - warnings: string[]; -} => { - if (useDefaultUptimeSchedule) { - return { - enabledHoursCount: defaultScheduleWeeklyHourCount, - errors: [], - warnings: [], - }; - } - - const { enabledHoursCount, enabledWeekdaysCount } = getEnabledHoursCount({ - enabledWeekdays, - stopTime, - runContinuously, - startTime, - }); - +}: ValidateInput): string[] => { if (enabledHoursCount > maxUptimeHours) { - // Return error based on whether runContinously enabled - if (runContinuously) { - return { - enabledHoursCount, - errors: ["Please pause your host for at least 1 day per week."], - warnings: [], - }; - } - const hourlyRequirement = Math.floor(maxUptimeHours / enabledWeekdaysCount); - return { - enabledHoursCount, - errors: [ - `Please reduce your host uptime to a max of ${hourlyRequirement} hours per day.`, - ], - warnings: [], - }; + // This state represents an error, which is handled by the RJSF validator. + return []; } if (enabledHoursCount > suggestedUptimeHours) { // Return warning based on whether runContinuously enabled if (runContinuously) { - return { - enabledHoursCount, - errors: [], - warnings: ["Consider pausing your host for 2 days per week."], - }; + return ["Consider pausing your host for 2 days per week."]; } const hourlySuggestion = Math.floor( suggestedUptimeHours / enabledWeekdaysCount, ); - return { - enabledHoursCount, - errors: [], - warnings: [ - `Consider running your host for ${hourlySuggestion} hours per day or fewer.`, - ], - }; + return [ + `Consider running your host for ${hourlySuggestion} hours per day or fewer.`, + ]; } + // No error - return { - enabledHoursCount, - errors: [], - warnings: [], - }; + return []; }; -export const getEnabledHoursCount = ({ - enabledWeekdays, - runContinuously, - startTime, - stopTime, -}: Omit) => { +export const getEnabledHoursCount = ( + hostUptime: HostUptime, +): { enabledHoursCount: number; enabledWeekdaysCount: number } => { + if (!hostUptime) { + return { + enabledHoursCount: defaultScheduleWeeklyHourCount, + enabledWeekdaysCount: defaultScheduleWeekdaysCount, + }; + } + + const { + sleepSchedule: { + enabledWeekdays, + timeSelection: { runContinuously, startTime, stopTime }, + }, + useDefaultUptimeSchedule, + } = hostUptime; + + if (useDefaultUptimeSchedule) { + return { + enabledHoursCount: defaultScheduleWeeklyHourCount, + enabledWeekdaysCount: defaultScheduleWeekdaysCount, + }; + } + const enabledWeekdaysCount = enabledWeekdays?.filter((day) => day).length ?? 0; const enabledHoursCount = runContinuously @@ -165,17 +139,16 @@ const toTimeString = (date: Date): string => minute: "2-digit", }); -export const defaultSleepSchedule: Omit = { +export const defaultSleepSchedule: Optional = { dailyStartTime: toTimeString(defaultStartDate), dailyStopTime: toTimeString(defaultStopDate), permanentlyExempt: false, - // TODO: Add pause shouldKeepOff: false, wholeWeekdaysOff: [0, 6], }; export const getHostUptimeFromGql = ( - sleepSchedule: MyHost["sleepSchedule"], + sleepSchedule: Optional, ): HostUptime => { const { dailyStartTime, dailyStopTime, wholeWeekdaysOff } = sleepSchedule; @@ -210,7 +183,7 @@ export const getHostUptimeFromGql = ( }; export const matchesDefaultUptimeSchedule = ( - sleepSchedule: MyHost["sleepSchedule"], + sleepSchedule: Optional, ): boolean => { const { dailyStartTime, dailyStopTime, wholeWeekdaysOff } = sleepSchedule; @@ -235,22 +208,88 @@ export const validator = (({ expirationDetails }, errors) => { if (!hostUptime || noExpiration === false) return errors; const { sleepSchedule, useDefaultUptimeSchedule } = hostUptime; - const { enabledWeekdays, timeSelection } = sleepSchedule; + const { timeSelection } = sleepSchedule; if (useDefaultUptimeSchedule) { return errors; } - const { enabledHoursCount } = getEnabledHoursCount({ - enabledWeekdays, - ...timeSelection, - }); + const { enabledHoursCount, enabledWeekdaysCount } = + getEnabledHoursCount(hostUptime); if (enabledHoursCount > maxUptimeHours) { - errors.expirationDetails?.hostUptime?.addError("Insufficient hours"); + // Return error based on whether runContinously enabled + if (timeSelection?.runContinuously) { + // @ts-expect-error + errors.expirationDetails?.hostUptime?.details?.addError?.( + "Please pause your host for at least 1 day per week.", + ); + return errors; + } + const hourlyRequirement = Math.floor(maxUptimeHours / enabledWeekdaysCount); + // @ts-expect-error + errors.expirationDetails?.hostUptime?.details?.addError?.( + `Please reduce your host uptime to a max of ${hourlyRequirement} hours per day.`, + ); + return errors; } return errors; }) satisfies ValidateProps<{ expirationDetails?: { hostUptime?: HostUptime; noExpiration: boolean }; }>; + +/** + * isNullSleepSchedule indicates that the sleep schedule is unset. + * TODO: When sleep schedules are generally released, we should instead check whether noExpiration is set. + * Until then, noExpiration is insufficient because a user may have an unexpirable host running without a sleep schedule. + * @param sleepSchedule - sleepSchedule as returned from Evergreen + * @returns boolean indicating whether the sleep schedule is effectively unset. + */ +export const isNullSleepSchedule = ( + sleepSchedule: Optional, +) => { + if (!sleepSchedule) return true; + + const { dailyStartTime, dailyStopTime, wholeWeekdaysOff } = sleepSchedule; + if (dailyStartTime !== "") return false; + if (dailyStopTime !== "") return false; + if (arraySymmetricDifference(wholeWeekdaysOff, []).length > 0) return false; + return true; +}; + +export const getNextHostStart = ( + { dailyStartTime, wholeWeekdaysOff }: SleepSchedule, + todayDate: Date, +): { + nextStartDay: string; + nextStartTime: string | null; +} => { + if (dailyStartTime) { + const nextStartDate = parse(dailyStartTime, "HH:mm", todayDate); + do { + nextStartDate.setDate(nextStartDate.getDate() + 1); + } while (wholeWeekdaysOff.includes(nextStartDate.getDay())); + const nextStartDay: string = isTomorrow(nextStartDate) + ? "tomorrow" + : days[nextStartDate.getDay()]; + const nextStartTime: string = `${nextStartDate.getHours()}:${nextStartDate.getMinutes().toString().padStart(2, "0")}`; + return { nextStartDay, nextStartTime }; + } + + const nextStartDate = todayDate; + if (!wholeWeekdaysOff.includes(nextStartDate.getDay())) { + // Find the next planned off day in the schedule + do { + nextStartDate.setDate(nextStartDate.getDate() + 1); + } while (!wholeWeekdaysOff.includes(nextStartDate.getDay())); + } + // Find the first active day after that break + do { + nextStartDate.setDate(nextStartDate.getDate() + 1); + } while (wholeWeekdaysOff.includes(nextStartDate.getDay())); + const nextStartDay: string = isTomorrow(nextStartDate) + ? "tomorrow" + : days[nextStartDate.getDay()]; + return { nextStartDay, nextStartTime: null }; +}; diff --git a/apps/spruce/src/components/Spawn/utils/index.ts b/apps/spruce/src/components/Spawn/utils/index.ts index 97ce3b90b..81be4dac3 100644 --- a/apps/spruce/src/components/Spawn/utils/index.ts +++ b/apps/spruce/src/components/Spawn/utils/index.ts @@ -6,9 +6,12 @@ export { defaultSleepSchedule, defaultStartDate, defaultStopDate, + getEnabledHoursCount, getHostUptimeFromGql, + getHostUptimeWarnings, + getNextHostStart, getSleepSchedule, - validateUptimeSchedule, + isNullSleepSchedule, validator, } from "./hostUptime"; export type { HostUptime } from "./hostUptime"; diff --git a/apps/spruce/src/components/SpruceForm/FieldTemplates/index.tsx b/apps/spruce/src/components/SpruceForm/FieldTemplates/index.tsx index 53af4b31a..9a6165e68 100644 --- a/apps/spruce/src/components/SpruceForm/FieldTemplates/index.tsx +++ b/apps/spruce/src/components/SpruceForm/FieldTemplates/index.tsx @@ -18,6 +18,7 @@ export const DefaultFieldTemplate: React.FC = ({ hidden, id, label, + rawErrors, schema, uiSchema, }) => { @@ -27,7 +28,7 @@ export const DefaultFieldTemplate: React.FC = ({ const showLabel = uiSchema["ui:showLabel"] ?? true; const fieldDataCy = uiSchema["ui:field-data-cy"]; const descriptionNode = uiSchema["ui:descriptionNode"]; - const errors = uiSchema["ui:errors"] ?? []; + const errors = uiSchema["ui:errors"] ?? (rawErrors?.length ? rawErrors : []); const warnings = uiSchema["ui:warnings"] ?? []; return ( !hidden && ( diff --git a/apps/spruce/src/constants/fieldMaps.ts b/apps/spruce/src/constants/fieldMaps.ts index 3020dece9..3fd45e10f 100644 --- a/apps/spruce/src/constants/fieldMaps.ts +++ b/apps/spruce/src/constants/fieldMaps.ts @@ -195,3 +195,13 @@ export const notificationFields = { buildBreak: "Build break", commitQueue: "Commit queue", }; + +export const days = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; diff --git a/apps/spruce/src/gql/generated/types.ts b/apps/spruce/src/gql/generated/types.ts index ff10acba4..58a5c5940 100644 --- a/apps/spruce/src/gql/generated/types.ts +++ b/apps/spruce/src/gql/generated/types.ts @@ -1091,8 +1091,7 @@ export type MutationAddAnnotationIssueArgs = { }; export type MutationAddFavoriteProjectArgs = { - identifier?: InputMaybe; - opts?: InputMaybe; + opts: AddFavoriteProjectInput; }; export type MutationAttachProjectToNewRepoArgs = { @@ -1135,16 +1134,11 @@ export type MutationCreatePublicKeyArgs = { }; export type MutationDeactivateStepbackTaskArgs = { - buildVariantName?: InputMaybe; - opts?: InputMaybe; - projectId?: InputMaybe; - taskName?: InputMaybe; + opts: DeactivateStepbackTaskInput; }; export type MutationDefaultSectionToRepoArgs = { - opts?: InputMaybe; - projectId?: InputMaybe; - section?: InputMaybe; + opts: DefaultSectionToRepoInput; }; export type MutationDeleteDistroArgs = { @@ -1204,9 +1198,7 @@ export type MutationOverrideTaskDependenciesArgs = { }; export type MutationPromoteVarsToRepoArgs = { - opts?: InputMaybe; - projectId?: InputMaybe; - varNames?: InputMaybe>; + opts: PromoteVarsToRepoInput; }; export type MutationRemoveAnnotationIssueArgs = { @@ -1217,8 +1209,7 @@ export type MutationRemoveAnnotationIssueArgs = { }; export type MutationRemoveFavoriteProjectArgs = { - identifier?: InputMaybe; - opts?: InputMaybe; + opts: RemoveFavoriteProjectInput; }; export type MutationRemoveItemFromCommitQueueArgs = { @@ -2095,14 +2086,12 @@ export type QueryProjectArgs = { export type QueryProjectEventsArgs = { before?: InputMaybe; - identifier?: InputMaybe; limit?: InputMaybe; - projectIdentifier?: InputMaybe; + projectIdentifier: Scalars["String"]["input"]; }; export type QueryProjectSettingsArgs = { - identifier?: InputMaybe; - projectIdentifier?: InputMaybe; + projectIdentifier: Scalars["String"]["input"]; }; export type QueryRepoEventsArgs = { @@ -2604,8 +2593,6 @@ export type Task = { status: Scalars["String"]["output"]; stepbackInfo?: Maybe; tags: Array; - /** @deprecated Use files instead */ - taskFiles: TaskFiles; taskGroup?: Maybe; taskGroupMaxHosts?: Maybe; /** taskLogs returns the tail 100 lines of the task's logs. */ @@ -2689,8 +2676,6 @@ export type TaskFiles = { /** TaskFilterOptions defines the parameters that are used when fetching tasks from a Version. */ export type TaskFilterOptions = { baseStatuses?: InputMaybe>; - /** @deprecated Use includeNeverActivatedTasks instead */ - includeEmptyActivation?: InputMaybe; includeNeverActivatedTasks?: InputMaybe; limit?: InputMaybe; page?: InputMaybe; @@ -5322,8 +5307,7 @@ export type UpdatePublicKeyMutation = { }; export type UpdateSpawnHostStatusMutationVariables = Exact<{ - hostId: Scalars["String"]["input"]; - action: SpawnHostStatusActions; + updateSpawnHostStatusInput: UpdateSpawnHostStatusInput; }>; export type UpdateSpawnHostStatusMutation = { diff --git a/apps/spruce/src/gql/mocks/getSpruceConfig.ts b/apps/spruce/src/gql/mocks/getSpruceConfig.ts index 2f88dacf5..107926e19 100644 --- a/apps/spruce/src/gql/mocks/getSpruceConfig.ts +++ b/apps/spruce/src/gql/mocks/getSpruceConfig.ts @@ -74,7 +74,7 @@ export const getUserSettingsMock: ApolloMock< data: { userSettings: { __typename: "UserSettings", - dateFormat: "MM/DD/YYYY", + dateFormat: "MM/dd/yyyy", githubUser: { lastKnownAs: "user", __typename: "GithubUser", diff --git a/apps/spruce/src/gql/mutations/update-spawn-host-status.graphql b/apps/spruce/src/gql/mutations/update-spawn-host-status.graphql index 3568b6132..5b33bec7c 100644 --- a/apps/spruce/src/gql/mutations/update-spawn-host-status.graphql +++ b/apps/spruce/src/gql/mutations/update-spawn-host-status.graphql @@ -1,8 +1,9 @@ mutation UpdateSpawnHostStatus( - $hostId: String! - $action: SpawnHostStatusActions! + $updateSpawnHostStatusInput: UpdateSpawnHostStatusInput! ) { - updateSpawnHostStatus(hostId: $hostId, action: $action) { + updateSpawnHostStatus( + updateSpawnHostStatusInput: $updateSpawnHostStatusInput + ) { id status } diff --git a/apps/spruce/src/pages/spawn/spawnHost/EditSpawnHostButton.tsx b/apps/spruce/src/pages/spawn/spawnHost/EditSpawnHostButton.tsx index c73ff0d28..da22a7244 100644 --- a/apps/spruce/src/pages/spawn/spawnHost/EditSpawnHostButton.tsx +++ b/apps/spruce/src/pages/spawn/spawnHost/EditSpawnHostButton.tsx @@ -33,6 +33,7 @@ export const EditSpawnHostButton: React.FC = ({ trigger={