Path: blob/main/components/dashboard/src/data/ide-options/ide-options-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 { getGitpodService } from "../../service/service";8import { IDEOption, IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";9import { DisableScope, Scope } from "../workspaces/workspace-classes-query";10import { useOrgSettingsQuery } from "../organizations/org-settings-query";11import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";12import { useMemo } from "react";13import { useConfiguration } from "../configurations/configuration-queries";14import { useDeepCompareMemoize } from "use-deep-compare-effect";1516const DEFAULT_WS_EDITOR = "code";1718export const useIDEOptions = () => {19return useQuery(20["ide-options"],21async () => {22return await getGitpodService().server.getIDEOptions();23},24{25staleTime: 1000 * 60 * 60 * 1, // 1h26cacheTime: 1000 * 60 * 60 * 1, // 1h27},28);29};3031export const useIDEVersionsQuery = (pinnableIdeIdList?: string[]) => {32return useQuery(33["ide-versions", pinnableIdeIdList?.join(",")],34async () => {35const updatedVal: Record<string, string[]> = {};36if (!pinnableIdeIdList) {37return updatedVal;38}39const ideVersionsResult = await Promise.all(40pinnableIdeIdList.map((ide) => getGitpodService().server.getIDEVersions(ide)),41);42for (let i = 0; i < pinnableIdeIdList.length; i++) {43const versions = ideVersionsResult[i]!;44updatedVal[pinnableIdeIdList[i]] = versions;45}46return updatedVal;47},48{49staleTime: 1000 * 60 * 10, // 10m50cacheTime: 1000 * 60 * 10, // 10m51},52);53};5455export type AllowedWorkspaceEditor = IDEOption & {56id: string;57isDisabledInScope?: boolean;58disableScope?: DisableScope;59isComputedDefault?: boolean;60};6162interface FilterOptions {63filterOutDisabled: boolean;64userDefault?: string;65ignoreScope?: DisableScope[];66}67export const useAllowedWorkspaceEditorsMemo = (configurationId: string | undefined, options?: FilterOptions) => {68const { data: orgSettings, isLoading: isLoadingOrgSettings } = useOrgSettingsQuery();69const { data: installationOptions, isLoading: isLoadingInstallationCls } = useIDEOptions();70const { data: configuration, isLoading: isLoadingConfiguration } = useConfiguration(configurationId);71const isLoading = isLoadingOrgSettings || isLoadingInstallationCls || isLoadingConfiguration;72const depItems = [73installationOptions,74options?.ignoreScope,75orgSettings,76configuration?.workspaceSettings?.restrictedEditorNames,77];78const data = useMemo(() => {79return getAllowedWorkspaceEditors(80installationOptions,81orgSettings,82configuration?.workspaceSettings?.restrictedEditorNames,83options,84);85// react useMemo is using `Object.is` to compare dependencies so array / object will make re-render re-call useMemo,86// see also https://react.dev/reference/react/useMemo#every-time-my-component-renders-the-calculation-in-usememo-re-runs87//88// eslint-disable-next-line react-hooks/exhaustive-deps89}, [useDeepCompareMemoize(depItems)]);90return { ...data, isLoading, usingConfigurationId: configuration?.id };91};9293const getAllowedWorkspaceEditors = (94installationOptions: IDEOptions | undefined,95orgSettings: Pick<OrganizationSettings, "restrictedEditorNames"> | undefined,96repoRestrictedEditorNames: string[] | undefined,97options?: FilterOptions,98) => {99let data: AllowedWorkspaceEditor[] = [];100const baseDefault = options?.userDefault ?? DEFAULT_WS_EDITOR;101102if (installationOptions?.options) {103data = Object.entries(installationOptions.options)104.map(([key, value]) => ({105...value,106id: key,107}))108.sort(IdeOptionsSorter);109}110let scope: Scope = "installation";111if (data.length === 0) {112return { data, scope, computedDefault: baseDefault, availableOptions: [] };113}114if (115!options?.ignoreScope?.includes("organization") &&116orgSettings?.restrictedEditorNames &&117orgSettings.restrictedEditorNames.length > 0118) {119data = data.map((d) => ({120...d,121isDisabledInScope: orgSettings.restrictedEditorNames.includes(d.id),122disableScope: "organization",123}));124scope = "organization";125}126if (127!options?.ignoreScope?.includes("configuration") &&128repoRestrictedEditorNames &&129repoRestrictedEditorNames.length > 0130) {131data = data.map((d) => {132if (d.isDisabledInScope) {133return d;134}135return {136...d,137isDisabledInScope: repoRestrictedEditorNames.includes(d.id),138disableScope: "configuration",139};140});141scope = "configuration";142}143144let computedDefault = options?.userDefault;145const allowedList = data.filter((e) => !e.isDisabledInScope);146if (!allowedList.some((d) => d.id === options?.userDefault && !d.isDisabledInScope)) {147computedDefault = allowedList.length > 0 ? allowedList[0].id : baseDefault;148}149data = data.map((e) => {150if (e.id === computedDefault) {151e.isComputedDefault = true;152}153return e;154});155const availableOptions = allowedList.map((e) => e.id);156if (options?.filterOutDisabled) {157return { data: allowedList, scope, computedDefault, availableOptions };158}159return { data, scope, computedDefault, availableOptions };160};161162function IdeOptionsSorter(163a: Pick<IDEOption, "experimental" | "orderKey">,164b: Pick<IDEOption, "experimental" | "orderKey">,165) {166// Prefer experimental options167if (a.experimental && !b.experimental) {168return -1;169}170if (!a.experimental && b.experimental) {171return 1;172}173174if (!a.orderKey || !b.orderKey) {175return 0;176}177178return parseInt(a.orderKey, 10) - parseInt(b.orderKey, 10);179}180181182