Path: blob/main/components/dashboard/src/data/workspaces/workspace-classes-query.ts
2501 views
/**1* Copyright (c) 2023 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { useQuery } from "@tanstack/react-query";7import { workspaceClient } from "../../service/public-api";8import { WorkspaceClass } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";9import { useOrgSettingsQuery } from "../organizations/org-settings-query";10import { Configuration, WorkspaceSettings } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";11import { useMemo } from "react";12import { PlainMessage } from "@bufbuild/protobuf";13import { useConfiguration } from "../configurations/configuration-queries";14import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";15import { useDeepCompareMemoize } from "use-deep-compare-effect";1617export const DEFAULT_WS_CLASS = "g1-standard";1819export const useWorkspaceClasses = () => {20return useQuery<WorkspaceClass[]>({21queryKey: ["workspace-classes"],22queryFn: async () => {23const response = await workspaceClient.listWorkspaceClasses({});24return response.workspaceClasses;25},26cacheTime: 1000 * 60 * 60, // 1h27staleTime: 1000 * 60 * 60, // 1h28});29};3031export type Scope = "organization" | "configuration" | "installation";32export type DisableScope = "organization" | "configuration";33export type AllowedWorkspaceClass = PlainMessage<WorkspaceClass> & {34isDisabledInScope?: boolean;35disableScope?: DisableScope;36isComputedDefaultClass?: boolean;37};3839// getNextDefaultClass returns smaller closest one if or larger closest one if there's no smaller ones40export const getNextDefaultClass = (allClasses: AllowedWorkspaceClass[], defaultClass?: string) => {41const availableClasses = allClasses.filter((e) => !e.isDisabledInScope);42if (availableClasses.length === 0) {43return undefined;44}45if (defaultClass) {46if (availableClasses.some((cls) => cls.id === defaultClass)) {47return defaultClass;48}49}50const defaultIndexInAll = allClasses.findIndex((cls) => cls.id === defaultClass);51if (defaultIndexInAll === -1) {52return undefined;53}54// remove unavailable default class55const sortedClasses = [56...allClasses.slice(0, defaultIndexInAll).reverse(),57...allClasses.slice(defaultIndexInAll, allClasses.length),58].filter((cls) => !cls.isDisabledInScope);59if (sortedClasses.length > 0) {60return sortedClasses[0].id;61}62return undefined;63};6465export const getAllowedWorkspaceClasses = (66installationClasses: WorkspaceClass[] | undefined,67orgSettings: Pick<OrganizationSettings, "allowedWorkspaceClasses"> | undefined,68repoRestrictedClass: WorkspaceSettings["restrictedWorkspaceClasses"] | undefined,69repoDefaultClass: WorkspaceSettings["workspaceClass"] | undefined,70options?: { filterOutDisabled: boolean; ignoreScope?: DisableScope[] },71) => {72let data: AllowedWorkspaceClass[] = installationClasses ?? [];73let scope: Scope = "installation";74if (data.length === 0) {75return { data, scope, computedDefaultClass: DEFAULT_WS_CLASS };76}77if (78!options?.ignoreScope?.includes("organization") &&79orgSettings?.allowedWorkspaceClasses &&80orgSettings.allowedWorkspaceClasses.length > 081) {82data = data.map((cls) => ({83...cls,84isDisabledInScope: !orgSettings.allowedWorkspaceClasses.includes(cls.id),85disableScope: "organization",86}));87scope = "organization";88}89if (!options?.ignoreScope?.includes("configuration") && repoRestrictedClass && repoRestrictedClass.length > 0) {90data = data.map((cls) => {91if (cls.isDisabledInScope) {92return cls;93}94return {95...cls,96isDisabledInScope: repoRestrictedClass.includes(cls.id),97disableScope: "configuration",98};99});100scope = "configuration";101}102const computedDefaultClass = getNextDefaultClass(data, repoDefaultClass ?? DEFAULT_WS_CLASS) ?? DEFAULT_WS_CLASS;103data = data.map((e) => {104if (e.id === computedDefaultClass) {105e.isComputedDefaultClass = true;106}107return e;108});109if (options?.filterOutDisabled) {110return { data: data.filter((e) => !e.isDisabledInScope), scope, computedDefaultClass };111}112return { data, scope, computedDefaultClass };113};114115export const useAllowedWorkspaceClassesMemo = (116configurationId?: Configuration["id"],117options?: { filterOutDisabled: boolean; ignoreScope?: DisableScope[] },118) => {119const { data: orgSettings, isLoading: isLoadingOrgSettings } = useOrgSettingsQuery();120const { data: installationClasses, isLoading: isLoadingInstallationCls } = useWorkspaceClasses();121// empty configurationId will return undefined122const { data: configuration, isLoading: isLoadingConfiguration } = useConfiguration(configurationId);123124const isLoading = isLoadingOrgSettings || isLoadingInstallationCls || isLoadingConfiguration;125126const depItems = [127installationClasses,128orgSettings,129options,130configuration?.workspaceSettings?.restrictedWorkspaceClasses,131configuration?.workspaceSettings?.workspaceClass,132];133const data = useMemo(() => {134return getAllowedWorkspaceClasses(135installationClasses,136orgSettings,137configuration?.workspaceSettings?.restrictedWorkspaceClasses,138configuration?.workspaceSettings?.workspaceClass,139options,140);141// react useMemo is using `Object.is` to compare dependencies so array / object will make re-render re-call useMemo,142// see also https://react.dev/reference/react/useMemo#every-time-my-component-renders-the-calculation-in-usememo-re-runs143//144// eslint-disable-next-line react-hooks/exhaustive-deps145}, [useDeepCompareMemoize(depItems)]);146return { ...data, isLoading };147};148149150