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