{"version":3,"file":"main-dblMDaXd.js","sources":["../../src/contexts/ThemeProvider.tsx","../../src/contexts/SegmentProvider.tsx","../../src/assets/gitpod-logo.svg","../../src/assets/onboarding-layers.png","../../src/assets/avatar.jpg","../../src/contexts/OnboardingQuestionnaireContext.tsx","../../src/hooks/use-onboarding-questionnaire.ts","../../src/components/onboarding/OnboardingSidebar.tsx","../../src/components/PageSuspense.tsx","../../src/components/AccountInfo.tsx","../../src/components/OrgIcon.tsx","../../src/components/SwitchOrgModal.tsx","../../src/desktop.ts","../../src/utils/platform.ts","../../src/components/OrgSwitcher.tsx","../../src/components/CloseableFullPage.tsx","../../src/components/Breadcrumb.tsx","../../src/hooks/use-theme.ts","../../src/components/ThemeToggle.tsx","../../src/components/settings/SettingsSidebar.tsx","../../src/components/settings/SettingsModalLayout.tsx","../../src/menu-item.ts","../../src/components/NotAuthorized.tsx","../../src/assets/icons/geist/IconArrowDown.tsx","../../src/components/podkit/tables/Table.tsx","../../src/hooks/use-available-runners.ts","../../src/routes/environments/EnvironmentPhaseTag.tsx","../../src/routes/environment-inventory/Inventory.tsx","../../src/routes/environment-inventory/EnvironmentInventoryPage.tsx","../../src/routes/git-authentications/GitAuthenticationsPage.tsx","../../src/components/podkit/forms/TagsInput.tsx","../../src/routes/manage-organization/DeleteOrganizationModal.tsx","../../src/routes/manage-organization/OrganizationSettings.tsx","../../src/routes/manage-organization/ManageOrganizationPage.tsx","../../src/routes/join-organization/join-organization.ts","../../src/assets/icons/geist/IconPlus.tsx","../../src/routes/members/InviteMembersPage.tsx","../../src/components/podkit/forms/RadioListField.tsx","../../src/routes/members/MembersList.tsx","../../src/routes/members/MembersPage.tsx","../../src/queries/personal-access-tokens-queries.ts","../../src/routes/personal-access-tokens/time-format.tsx","../../src/routes/personal-access-tokens/PersonalAccessTokenCard.tsx","../../src/routes/personal-access-tokens/NewPersonalAccessTokenModal.tsx","../../src/routes/personal-access-tokens/DeletePersonalAccessTokenModal.tsx","../../src/routes/personal-access-tokens/PersonalAccessTokensList.tsx","../../src/routes/personal-access-tokens/PersonalAccessTokensPage.tsx","../../src/routes/runners/details/runner-configuration-keys.tsx","../../src/assets/icons/geist/IconInfoFilled.tsx","../../src/assets/icons/geist/IconLock.tsx","../../src/routes/runners/details/RunnerDetailsSection.tsx","../../src/components/Tooltip.tsx","../../src/routes/runners/DeleteRunnerModal.tsx","../../src/routes/runners/RenameRunnerModal.tsx","../../src/routes/runners/RunnerIcon.tsx","../../src/assets/aws.svg","../../src/routes/runners/RunnerCard.tsx","../../src/routes/runners/RunnerSetupURL.ts","../../src/routes/runners/details/CloudFormationStack.tsx","../../src/assets/icons/geist/IconChip.tsx","../../src/assets/icons/geist/IconPlusSquare.tsx","../../src/components/onboarding/EnvironmentClassToggle.tsx","../../src/components/podkit/checkbox/Checkbox.tsx","../../src/components/podkit/checkbox/CheckboxInputField.tsx","../../src/routes/runners/details/EnvironmentClassEditModal.tsx","../../src/routes/runners/details/EnvironmentClassAddModal.tsx","../../src/routes/runners/details/EnvironmentClasses.tsx","../../src/routes/runners/details/RunnerSharing.tsx","../../src/assets/icons/geist/IconBitbucket.tsx","../../src/assets/icons/geist/IconGitLab.tsx","../../src/routes/runners/details/SourceControlProviderIcon.tsx","../../src/routes/runners/details/SourceControlProviderAddModal.tsx","../../src/components/podkit/switch/Switch.tsx","../../src/routes/runners/details/SourceControlProviderModal.tsx","../../src/routes/runners/details/SourceControlProviderRemoveModal.tsx","../../src/routes/runners/details/SourceControlProvider.tsx","../../src/routes/runners/details/RunnerDetails.tsx","../../src/assets/icons/geist/IconRefresh.tsx","../../src/routes/runners/details/RunnerDetailsPage.tsx","../../src/routes/runners/NewRunnerForm.tsx","../../src/routes/runners/NewRunnerModal.tsx","../../src/routes/runners/RunnersList.tsx","../../src/routes/runners/RunnersPage.tsx","../../src/components/settings/SettingsRouter.tsx","../../src/components/settings/SettingsModal.tsx","../../src/assets/icons/geist/IconEarlyAccess.tsx","../../src/assets/icons/geist/IconGitpodEngraved.tsx","../../src/assets/icons/geist/IconGrid.tsx","../../src/assets/icons/geist/IconNewEnvironment.tsx","../../src/assets/icons/geist/IconSettings.tsx","../../src/assets/icons/geist/IconApp.tsx","../../src/assets/icons/geist/IconDash.tsx","../../src/hooks/use-local-storage.ts","../../src/components/NudgeDownloadApp.tsx","../../src/components/SidebarButton.tsx","../../src/hooks/use-open-settings-shortcut.ts","../../src/components/podkit/forms/InputWithSuggestions.tsx","../../src/hooks/use-context-url.ts","../../src/components/ContextUrlInput.tsx","../../src/components/EnvironmentTypeSelect.tsx","../../src/hooks/use-environment-types.ts","../../src/routes/environments/details-url.ts","../../src/routes/projects/details/ProjectConstants.ts","../../src/utils/objects.ts","../../src/routes/projects/details/ProjectDetailsForm.tsx","../../src/routes/projects/details/ProjectsDetailsLayout.tsx","../../src/routes/projects/EditProjectModal.tsx","../../src/routes/environments/create/CreateEnvironmentButton.tsx","../../src/routes/projects/ProjectDeleteModal.tsx","../../src/routes/projects/ProjectCard.tsx","../../src/routes/projects/ProjectsList.tsx","../../src/routes/projects/ProjectSelectorModal.tsx","../../src/routes/environments/create/CreateEnvironment.tsx","../../src/assets/icons/geist/IconBoxLink.tsx","../../src/assets/icons/geist/IconBranch.tsx","../../src/hooks/use-environments-by-projects.ts","../../src/routes/environments/SidebarEnvironmentList.tsx","../../src/assets/icons/geist/IconBook.tsx","../../src/assets/icons/geist/IconGauge.tsx","../../src/routes/onboarding/OnboardingSidebarSection.tsx","../../src/components/SidebarLayout.tsx","../../src/hooks/use-no-min-width.ts","../../src/routes/create-organization/CreateOrganizationModal.tsx","../../src/routes/onboarding/components/TimelineElipsis.tsx","../../src/routes/onboarding/components/QuestionnaireTimeline.tsx","../../src/routes/create-organization/CreateOrganizationPage.tsx","../../src/routes/join-organization/JoinOrganization.tsx","../../src/routes/join-organization/JoinOrganizationFromInvite.tsx","../../src/routes/join-organization/JoinOrganizationPage.tsx","../../src/components/podkit/alert/Alert.tsx","../../src/routes/local-runner/LocalRunner.tsx","../../src/assets/gitpod-logo-large.svg","../../src/assets/icons/geist/IconGoogle.tsx","../../src/routes/LoginPage.tsx","../../src/routes/NotFound.tsx","../../src/routes/onboarding/components/OnboardingTabContent.tsx","../../src/routes/onboarding/components/VideoSection.tsx","../../src/routes/onboarding/CreateAProjectPage.tsx","../../src/assets/icons/geist/IconCloud.tsx","../../src/assets/icons/geist/IconZap.tsx","../../src/assets/icons/geist/IconRunner.tsx","../../src/routes/onboarding/HowGitpodWorksPage.tsx","../../src/routes/onboarding/InvitePeoplePage.tsx","../../src/routes/onboarding/use-runner-configuration-progress.ts","../../src/routes/onboarding/components/TimelineDot.tsx","../../src/routes/onboarding/components/TimelinePill.tsx","../../src/routes/onboarding/components/Timeline.tsx","../../src/routes/onboarding/components/TimelineContent.tsx","../../src/assets/apple-silicon.svg","../../src/assets/azure.svg","../../src/assets/gcp.svg","../../src/assets/linux.svg","../../src/routes/onboarding/SetupARunnerPage.tsx","../../src/routes/onboarding/OnboardingContent.tsx","../../src/routes/onboarding/OnboardingPage.tsx","../../src/routes/onboarding/OnboardingQuestionnairePage.tsx","../../src/routes/projects/CreateProjectModal.tsx","../../src/routes/projects/ProjectsPage.tsx","../../src/routes/signin-desktop/SigninDesktopPage.tsx","../../src/app.tsx","../../src/contexts/OnboardingQuestionnaireProvider.tsx","../../src/main.tsx"],"sourcesContent":["import { createContext, useCallback, useEffect, useState } from \"react\";\n\ntype Theme = \"dark\" | \"light\" | \"system\";\n\ntype ThemeProviderProps = {\n children: React.ReactNode;\n defaultTheme?: Theme;\n storageKey?: string;\n};\n\ntype ThemeProviderState = {\n theme: Theme;\n effectiveTheme: \"light\" | \"dark\";\n setTheme: (theme: Theme) => void;\n};\n\nconst initialState: ThemeProviderState = {\n theme: \"system\",\n effectiveTheme: \"light\",\n setTheme: () => null,\n};\n\nexport const ThemeProviderContext = createContext(initialState);\n\nexport function ThemeProvider({\n children,\n defaultTheme = \"system\",\n storageKey = \"gitpod-ui-theme\",\n ...props\n}: ThemeProviderProps) {\n const [theme, setTheme] = useState(() => {\n // Attempt to fetch the stored theme, fallback to default if not found\n const storedTheme = localStorage.getItem(storageKey);\n return storedTheme ? (storedTheme as Theme) : defaultTheme;\n });\n\n const getCurrentTheme = () => (window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\");\n const [effectiveTheme, setEffectiveTheme] = useState<\"light\" | \"dark\">(getCurrentTheme());\n\n const setThemeOnDOM = useCallback((theme: \"dark\" | \"light\") => {\n const root = window.document.documentElement;\n root.classList.remove(\"light\", \"dark\");\n document.documentElement.classList.add(theme);\n let meta = document.querySelector(\"head > meta[name='color-scheme']\");\n if (!meta) {\n meta = document.createElement(\"meta\");\n meta.setAttribute(\"name\", \"color-scheme\");\n meta.setAttribute(\"content\", theme);\n document.head.appendChild(meta);\n }\n meta.setAttribute(\"content\", theme);\n setEffectiveTheme(theme);\n }, []);\n\n useEffect(() => {\n const applyThemeBasedOnSelection = (themeValue: Theme) => {\n const theme = themeValue === \"system\" ? getCurrentTheme() : themeValue;\n setThemeOnDOM(theme);\n };\n\n // Apply theme based on the current selection\n applyThemeBasedOnSelection(theme);\n\n // Set up a listener for changes in the system theme preference if the theme is set to 'system'\n if (theme === \"system\") {\n const handleSystemThemeChange = (event: MediaQueryListEvent) => {\n const newTheme = event.matches ? \"dark\" : \"light\";\n setThemeOnDOM(newTheme);\n };\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\n mediaQuery.addEventListener(\"change\", handleSystemThemeChange);\n\n // Clean up the event listener when the component unmounts or the theme changes\n return () => {\n mediaQuery.removeEventListener(\"change\", handleSystemThemeChange);\n };\n }\n }, [setThemeOnDOM, theme]);\n\n // Update the setTheme function to handle user-initiated theme changes\n const value = {\n theme,\n effectiveTheme,\n setTheme: (newTheme: Theme) => {\n localStorage.setItem(storageKey, newTheme); // Persist the new theme selection\n setTheme(newTheme); // This will re-trigger the useEffect and apply the new theme\n },\n };\n\n return (\n \n {children}\n \n );\n}\n","import { type FC, type PropsWithChildren, useMemo } from \"react\";\nimport { type AnalyticsBrowser } from \"@segment/analytics-next\";\nimport { SegmentContext } from \"@/contexts/SegmentContext\";\n\nexport const SegmentProvider: FC AnalyticsBrowser }> = ({ create, children }) => {\n const context = useMemo(() => {\n if (create) {\n return create();\n }\n return undefined;\n }, [create]);\n\n return {children};\n};\n","export default \"data:image/svg+xml,%3csvg%20width='32'%20height='32'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M18.748%201.594a3.16%203.16%200%200%201-1.178%204.313l-9.437%205.387a.8.8%200%200%200-.403.695v8.456a.8.8%200%200%200%20.403.695l7.47%204.264a.8.8%200%200%200%20.794%200l7.47-4.264a.8.8%200%200%200%20.403-.695v-5.259l-6.715%203.785a3.167%203.167%200%200%201-4.312-1.2%203.16%203.16%200%200%201%201.202-4.308l9.607-5.415c2.927-1.65%206.548.463%206.548%203.82v9.22a6.016%206.016%200%200%201-3.035%205.224l-8.576%204.895a6.03%206.03%200%200%201-5.978%200l-8.576-4.895A6.016%206.016%200%200%201%201.4%2021.087v-9.74a6.016%206.016%200%200%201%203.035-5.225L14.43.417a3.167%203.167%200%200%201%204.318%201.177z'%20fill='url(%23a)'/%3e%3cdefs%3e%3clinearGradient%20id='a'%20x1='23.378'%20y1='4.839'%20x2='8.413'%20y2='28.391'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20stop-color='%23FFB45B'/%3e%3cstop%20offset='1'%20stop-color='%23FF8A00'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e\"","export default \"__VITE_ASSET__CsaK2fmW__\"","export default \"__VITE_ASSET__D2izZNu3__\"","import type {\n OnboardingQuestionnaireId,\n OnboardingQuestionnaireStepData,\n OnboardingQuestionnaireSteps,\n} from \"@/hooks/use-onboarding-questionnaire\";\nimport { createContext } from \"react\";\n\nexport const OnboardingQuestionnaireContext = createContext<{\n steps: OnboardingQuestionnaireSteps;\n complete: (step: OnboardingQuestionnaireId) => void;\n updateData: (data: OnboardingQuestionnaireStepData) => void;\n} | null>(null);\n","import { OnboardingQuestionnaireContext } from \"@/contexts/OnboardingQuestionnaireContext\";\nimport { useContext } from \"react\";\n\nexport enum OnboardingQuestionnaireId {\n CreateOrg = \"create-org\",\n YourRole = \"your-role\",\n WhatToAchieve = \"what-to-achieve\",\n}\n\nexport type OnboardingQuestionnaireStepData =\n | {\n id: OnboardingQuestionnaireId.CreateOrg;\n data?: undefined;\n }\n | {\n id: OnboardingQuestionnaireId.YourRole;\n data?: {\n selection: string;\n };\n }\n | {\n id: OnboardingQuestionnaireId.WhatToAchieve;\n data?: {\n selections: string[];\n };\n };\n\nexport type OnboardingQuestionnaireStep = {\n id: OnboardingQuestionnaireId;\n label: string;\n state: \"active\" | \"done\" | \"todo\";\n} & OnboardingQuestionnaireStepData;\n\nexport type OnboardingQuestionnaireSteps = {\n isLoading: boolean;\n steps: OnboardingQuestionnaireStep[];\n};\n\nexport const useOnboardingQuestionnaire = () => {\n const ctx = useContext(OnboardingQuestionnaireContext);\n\n if (!ctx) {\n throw new Error(\"useOnboardingQuestionnaire must be used within a OnboardingQuestionnaireProvider\");\n }\n\n return ctx;\n};\n\nexport type Question = {\n title: string;\n options: string[];\n multiSelection: boolean;\n};\n\nenum YourRoleOptions {\n DevOps = \"DevOps / Platform / Dev Experience\",\n EngineeringManagement = \"Engineering Management / Leadership\",\n SoftwareEngineering = \"Software Engineering\",\n DataAnalytics = \"Data / Analytics\",\n ProductManagement = \"Product Management\",\n Security = \"Security\",\n DeveloperRelations = \"Developer Relations\",\n Academia = \"Academia (Student, Teacher, Researcher)\",\n Other = \"Other\",\n}\n\nenum WhatToAchieveOptions {\n ImproveProductivity = \"Improve developer productivity & experience\",\n SpeedOnboarding = \"Speed up developer onboarding\",\n ReplaceVDI = \"Replace VDI for development\",\n ImproveSecurity = \"Improve security for development\",\n EnableSelfService = \"Enable developer self-service\",\n Exploring = \"Just exploring for personal use\",\n}\n\nexport const Questions = {\n [OnboardingQuestionnaireId.YourRole]: {\n title: \"What area describes your role best?\",\n options: [\n YourRoleOptions.DevOps,\n YourRoleOptions.EngineeringManagement,\n YourRoleOptions.SoftwareEngineering,\n YourRoleOptions.DataAnalytics,\n YourRoleOptions.ProductManagement,\n YourRoleOptions.Security,\n YourRoleOptions.DeveloperRelations,\n YourRoleOptions.Academia,\n YourRoleOptions.Other,\n ],\n multiSelection: false,\n } satisfies Question,\n [OnboardingQuestionnaireId.WhatToAchieve]: {\n title: \"What are you trying to achieve with Gitpod?\",\n options: [\n WhatToAchieveOptions.ImproveProductivity,\n WhatToAchieveOptions.SpeedOnboarding,\n WhatToAchieveOptions.ReplaceVDI,\n WhatToAchieveOptions.ImproveSecurity,\n WhatToAchieveOptions.EnableSelfService,\n WhatToAchieveOptions.Exploring,\n ],\n multiSelection: true,\n } satisfies Question,\n};\n\nexport type Quote = {\n quote: string;\n author: string;\n};\n\nexport const Quotes: Quote[] = [\n {\n quote: \"Using Gitpod will improve your feature velocity and drastically reduce your cycle times from commit to deploy.\",\n author: \"Senior Staff Software Engineer at Quizlet\",\n },\n {\n quote: \"We're a platform team. Our goal is to help other data teams be productive. When you're 30 data engineers, support takes up all the data platform team's time.\",\n author: \"Lead Platform Engineer, Luminus\",\n },\n {\n quote: \"We're able to reduce exfiltration risks as well as outside actors from accessing our development environments.\",\n author: \"Senior Staff Software Engineer at Quizlet\",\n },\n {\n quote: \"We've solved dev. Now we're focused on delivering value to users.\",\n author: \"Senior Staff Software Engineer at Quizlet\",\n },\n];\n\nexport const QuoteMap: Record = {\n [YourRoleOptions.DevOps]: Quotes[3],\n [YourRoleOptions.EngineeringManagement]: Quotes[0],\n [YourRoleOptions.SoftwareEngineering]: Quotes[0],\n [YourRoleOptions.DataAnalytics]: Quotes[1],\n [YourRoleOptions.ProductManagement]: Quotes[0],\n [YourRoleOptions.Security]: Quotes[2],\n [YourRoleOptions.DeveloperRelations]: Quotes[0],\n [YourRoleOptions.Academia]: Quotes[0],\n [YourRoleOptions.Other]: Quotes[0],\n};\n\nexport type Benefit = {\n title: string;\n description?: string;\n};\n\nexport const Benefits: Benefit[] = [\n { title: \"Free tier & free premium trial\" },\n { title: \"Self-host Gitpod in under 3 minutes\" },\n { title: \"Local environments to replace Docker Desktop\" },\n];\n\nexport const AdditionalBenefits: Benefit[] = [\n {\n title: \"Automate common development workflows\",\n description: \"Seed databases, provision infra, runbooks as one-click actions, configure code assistants, etc.\",\n },\n {\n title: \"Zero latency, secure remote development\",\n description: \"Ditch VDI for development to improve developer productivity without compromising security.\",\n },\n {\n title: \"Onboard new developers in minutes\",\n description: \"A single click launches a fully-prepared environment for every project.\",\n },\n {\n title: \"Dev container support\",\n description: \"Eliminate the need to manually install tools, dependencies and editor extensions.\",\n },\n];\n\nexport const AdditionalBenefitsMap: Record = {\n [WhatToAchieveOptions.ImproveProductivity]: [AdditionalBenefits[0], AdditionalBenefits[3]],\n [WhatToAchieveOptions.SpeedOnboarding]: [AdditionalBenefits[2], AdditionalBenefits[3]],\n [WhatToAchieveOptions.ReplaceVDI]: [AdditionalBenefits[1]],\n [WhatToAchieveOptions.ImproveSecurity]: [AdditionalBenefits[1], AdditionalBenefits[2]],\n [WhatToAchieveOptions.EnableSelfService]: [AdditionalBenefits[0], AdditionalBenefits[3]],\n [WhatToAchieveOptions.Exploring]: [AdditionalBenefits[3]],\n};\n\nexport const YourRoleSegmentTrackingNameMap: Record = {\n [YourRoleOptions.DevOps]: \"Enabler\",\n [YourRoleOptions.EngineeringManagement]: \"Engineering Leader\",\n [YourRoleOptions.SoftwareEngineering]: \"Engineering\",\n [YourRoleOptions.DataAnalytics]: \"Data\",\n [YourRoleOptions.ProductManagement]: \"Product\",\n [YourRoleOptions.Security]: \"Security\",\n [YourRoleOptions.DeveloperRelations]: \"DevRel\",\n [YourRoleOptions.Academia]: \"Academia\",\n [YourRoleOptions.Other]: \"Other\",\n};\n\nexport const WhatToAchieveSegmentTrackingNameMap: Record = {\n [WhatToAchieveOptions.ImproveSecurity]: \"security\",\n [WhatToAchieveOptions.ImproveProductivity]: \"developer_productivity\",\n [WhatToAchieveOptions.SpeedOnboarding]: \"developer_onboarding\",\n [WhatToAchieveOptions.ReplaceVDI]: \"vdi_replacement\",\n [WhatToAchieveOptions.EnableSelfService]: \"developer_self_serve\",\n [WhatToAchieveOptions.Exploring]: \"exploring_individually\",\n};\n","import { Heading1 } from \"@/components/podkit/typography/Headings\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport type React from \"react\";\nimport { type FC } from \"react\";\nimport gitpodLogo from \"@/assets/gitpod-logo.svg\";\nimport { useAuthenticatedUser } from \"@/queries/user-queries\";\nimport onboardingLayersImg from \"@/assets/onboarding-layers.png\";\nimport avatarImg from \"@/assets/avatar.jpg\";\nimport {\n AdditionalBenefitsMap,\n Benefits,\n OnboardingQuestionnaireId,\n QuoteMap,\n Quotes,\n useOnboardingQuestionnaire,\n type Quote,\n} from \"@/hooks/use-onboarding-questionnaire\";\nimport { IconEnvState } from \"@/assets/icons/geist/IconEnvState\";\nimport { coalesce, distinct } from \"@/utils/arrays\";\n\nexport const OnboardingSidebar: FC = () => {\n const { data: user } = useAuthenticatedUser();\n const onboardingQuestionnaire = useOnboardingQuestionnaire();\n\n let quoteComponent: React.ReactNode = undefined;\n const yourRoleStep = onboardingQuestionnaire.steps.steps.find((s) => s.id === OnboardingQuestionnaireId.YourRole)!;\n if (yourRoleStep.state !== \"todo\") {\n let quote: Quote | undefined;\n if (yourRoleStep.state === \"done\" && !yourRoleStep.data) {\n quote = Quotes[0];\n } else if (yourRoleStep.data) {\n quote = QuoteMap[yourRoleStep.data.selection];\n }\n if (quote) {\n quoteComponent = ;\n }\n }\n\n const whatToAcieveStep = onboardingQuestionnaire.steps.steps.find(\n (s) => s.id === OnboardingQuestionnaireId.WhatToAchieve,\n )!;\n const benefits = whatToAcieveStep.state === \"active\" && (\n
\n {Benefits.map((t) => (\n \n ))}\n
\n );\n\n let addtionalBenefits: React.ReactNode = undefined;\n if (whatToAcieveStep.data?.selections.length) {\n addtionalBenefits = (\n
\n Features we think you'll love...\n {distinct(\n coalesce(whatToAcieveStep.data.selections.flatMap((t) => AdditionalBenefitsMap[t])),\n (i) => i.title,\n ).map((t) => (\n \n ))}\n
\n );\n }\n\n return (\n
\n
\n
\n
\n \"Gitpod\n \n Welcome to Gitpod, {user?.name.split(\" \")[0]}\n \n \n Tell us about yourself so we can optimize your experience. It'll only take a minute!\n \n
\n
\n {!quoteComponent && }\n {quoteComponent}\n {benefits}\n {addtionalBenefits}\n
\n
\n
\n
\n );\n};\n\nconst QuoteComponent: FC<{ quote: Quote }> = ({ quote }) => {\n return (\n
\n \n "{quote.quote}"\n {quote.author}\n
\n );\n};\n\nconst BenefitComponent: FC<{ title: string; description?: string }> = ({ title, description }) => {\n return (\n
\n
\n \n {title}\n
\n {description && (\n {description}\n )}\n
\n );\n};\n","import { type FC, type PropsWithChildren, Suspense } from \"react\";\nimport { LoadingState } from \"@/components/podkit/loading/LoadingState\";\n\nexport const PageSuspense: FC = ({ children }) => {\n return (\n \n \n \n }\n >\n {children}\n \n );\n};\n","import { useGetAccount } from \"@/queries/account-queries\";\nimport { type FC } from \"react\";\n\nexport const AccountInfo: FC = () => {\n const { data: account, isLoading } = useGetAccount();\n\n if (isLoading || !account) {\n // TODO(at) add a nice loading state, maybe a skeleton?\n return
;\n }\n\n return (\n
\n \n {account.email}\n \n
\n );\n};\n","import { cn } from \"@/components/podkit/lib/cn\";\nimport { type FC, useMemo } from \"react\";\n\nconst iconColors = [\"#49bbcb\", \"#5e93d1\", \"#72c083\", \"#987ad9\", \"#9b8c9d\", \"#b76491\", \"#de7a31\", \"#f46565\"];\n\nfunction hashCode(s: string) {\n return Math.abs(\n s.split(\"\").reduce(function (a, b) {\n a = (a << 5) - a + b.charCodeAt(0);\n return a & a;\n }, 0),\n );\n}\n\ntype OrgIconProps = {\n orgName: string;\n};\nexport const OrgIcon: FC = ({ orgName }) => {\n const orgInitials = useMemo(() => {\n const initials = orgName\n .split(\" \")\n .filter((w) => !!w && w.length > 1)\n .map((n) => n[0])\n .join(\"\");\n return initials.substring(0, 2).toUpperCase();\n }, [orgName]);\n\n const backgroundColor = iconColors[hashCode(orgName) % iconColors.length];\n\n return (\n \n {orgInitials}\n
\n );\n};\n","import { Button } from \"@/components/flexkit/Button\";\nimport {\n Dialog,\n DialogBody,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/podkit/modal/Modal\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { useLocalRunner } from \"@/hooks/use-local-runner\";\nimport { useOrganization } from \"@/queries/organization-queries\";\nimport { useListEnvironments } from \"@/queries/environment-queries\";\nimport { EnvironmentPhase } from \"gitpod-next-api/gitpod/v1/environment_pb\";\nimport React, { useCallback, useMemo, type FC } from \"react\";\n\ntype SwithLocalRunnerModalProps = {\n onClose: () => void;\n onConfirm: () => void;\n};\n\nexport const SwitchOrgModal: FC = ({ onClose, onConfirm }) => {\n const { data: org } = useOrganization();\n const listEnvironments = useListEnvironments();\n const localRunner = useLocalRunner();\n const isLoading =\n listEnvironments.isLoading ||\n listEnvironments.isPending ||\n listEnvironments.isFetching ||\n listEnvironments.isRefetching ||\n localRunner.loading;\n\n const localActiveEnvs = useMemo(() => {\n if (isLoading) {\n return undefined;\n }\n // we only check first 100 for now\n // TODO(ak) we need new API to get exact count, i.e. use filters by runner id and phases\n // local runner anyway won't be able to have 100 environments\n return listEnvironments.data?.environments.filter(\n (w) =>\n w.metadata?.runnerId === localRunner.status?.currentRunnerId &&\n (w.status?.phase || EnvironmentPhase.UNSPECIFIED) < EnvironmentPhase.STOPPING,\n ).length;\n }, [listEnvironments.data, isLoading, localRunner.status]);\n\n // ensure that we don't call confirm several times becuse of react re-render\n const confirmed = React.useRef(false);\n const confirm = useCallback(() => {\n if (confirmed.current) {\n return;\n }\n confirmed.current = true;\n onConfirm();\n }, [onConfirm]);\n\n if (!org || localActiveEnvs === undefined) {\n return null;\n }\n if (!(window.ipcRenderer && localActiveEnvs > 0)) {\n confirm();\n return null;\n }\n const onOpenChange = (open: boolean) => {\n if (!open) {\n onClose();\n }\n };\n return (\n \n \n \n Switching organization\n \n \n \n \n Stop {localActiveEnvs} local {localActiveEnvs === 1 ? \"environment\" : \"environments\"} and\n switch?\n \n \n \n Any running local environments belonging to {org.name} will be stopped\n \n \n \n \n \n \n \n \n );\n};\n","export const DESKTOP_APP_DOWNLOAD_URL =\n \"https://releases.gitpod.io/desktop/stable/Gitpod.dmg\";\n","/**\n * @returns true if the platform is Mac, iPad or iPhone\n */\nexport const isMacLike = (): boolean => {\n if (navigator.platform.startsWith(\"Mac\") || navigator.platform === \"iPhone\") {\n return true;\n }\n\n return false;\n};\n\n/**\n * @returns either \"⌘\" or \"Ctrl + \" depending on the platform for use in key bindings\n */\nexport const getPlatformModifierKey = (): string => (isMacLike() ? \"⌘\" : \"Ctrl + \");\n\nexport const StartEnvironmentModalKeyBinding = getPlatformModifierKey() + \"O\";\n\nexport const OpenProjectsModalKeyBinding = getPlatformModifierKey() + \"P\";\n\nexport const OpenSettingsModalKeyBinding = getPlatformModifierKey() + \",\";\n","import { IconChevronDown } from \"@/assets/icons/geist/IconChevronDown\";\nimport { AccountInfo } from \"@/components/AccountInfo\";\nimport { Button } from \"@/components/flexkit/Button\";\nimport { OrgIcon } from \"@/components/OrgIcon\";\nimport { cn, type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport { useSettingsModal } from \"@/components/settings/SettingsModal\";\nimport { SwitchOrgModal } from \"@/components/SwitchOrgModal\";\nimport { DESKTOP_APP_DOWNLOAD_URL } from \"@/desktop\";\nimport { getPrincipal, setPrincipal } from \"@/principal\";\nimport { useGetAccount } from \"@/queries/account-queries\";\nimport { useLogout } from \"@/queries/user-queries\";\nimport { isMacLike } from \"@/utils/platform\";\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { CheckIcon } from \"lucide-react\";\nimport React, { useCallback, useState, type FC } from \"react\";\nimport { Link, useNavigate } from \"react-router-dom\";\n\nexport const OrgSwitcher: FC = () => {\n const { data: account, isPending: isLoadingAccount } = useGetAccount();\n const navigate = useNavigate();\n const logout = useLogout();\n const [switchToMemberId, setSwitchToMemberId] = useState();\n const { openSettings } = useSettingsModal();\n\n const principal = getPrincipal();\n const isMac = isMacLike();\n const showDownloadApp = !window.ipcRenderer && isMac;\n\n const membership = principal ? account?.memberships?.find((m) => m.userId === principal) : undefined;\n const orgName = membership?.organizationName;\n const onLogout = useCallback(\n async (e: Event) => {\n e.preventDefault();\n await logout.mutateAsync();\n navigate(\"/\");\n },\n [logout, navigate],\n );\n\n const handleSwitchOrg = useCallback(\n (memberId: string) => {\n if (!memberId || memberId === principal) {\n return;\n }\n setSwitchToMemberId(memberId);\n },\n [principal],\n );\n\n if (!isLoadingAccount && account && !membership) {\n return ;\n }\n\n if (isLoadingAccount || !account || !principal) {\n return
;\n }\n\n return (\n <>\n \n \n \n \n
{orgName}
\n \n \n
\n \n \n \n
{account.email}
\n
\n {(account?.memberships || []).map((m) => (\n handleSwitchOrg(m.userId)}\n >\n
\n \n {m.organizationName}\n
\n
\n {m.userId === principal && }\n
\n \n ))}\n \n openSettings()}>\n Settings\n \n \n {showDownloadApp && (\n \n \n Get the desktop app\n \n \n )}\n \n \n Documentation\n \n \n \n Join or create an organization\n \n \n Logout\n \n \n
\n
\n {!!switchToMemberId && (\n setSwitchToMemberId(undefined)}\n onConfirm={() => {\n setSwitchToMemberId(undefined);\n setPrincipal(switchToMemberId);\n navigate(\"/\", { replace: true });\n }}\n />\n )}\n \n );\n};\n\nconst DropdownMenuItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n PropsWithClassName<{\n inset?: boolean;\n }>\n>(({ className, inset, ...props }, ref) => (\n \n));\nDropdownMenuItem.displayName = DropdownMenu.Item.displayName;\n","import { OrgSwitcher } from \"@/components/OrgSwitcher\";\nimport { Button } from \"@/components/podkit/buttons/Button\";\nimport { cn, type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport { Dialog, DialogContent } from \"@/components/podkit/modal/Modal\";\nimport { X } from \"lucide-react\";\nimport { useEffect, type FC, type PropsWithChildren } from \"react\";\n\ntype Props = {\n onClose?: () => void;\n showOrgSwitcher: boolean;\n \"data-testid\"?: string;\n};\n\nexport const CloseableFullPage: FC = (p) => {\n return (\n \n
\n {p.showOrgSwitcher && }\n {p.onClose && }\n
\n
{p.children}
\n
\n );\n};\n\nexport const EscButton: FC<{ onClose?: () => void }> = ({ onClose }) => {\n return (\n \n
\n \n esc\n
\n \n );\n};\n\nexport const EmptyClosableFullPage: FC<\n {\n onClose?: () => void;\n \"data-testid\"?: string;\n } & PropsWithChildren &\n PropsWithClassName\n> = (p) => {\n const height = window.ipcRenderer ? \"calc(100vh - 4rem)\" : \"calc(100vh - 3.5rem)\";\n return (\n \n \n {p.children}\n
\n \n );\n};\n\nexport const ClosableWithEsc: FC<\n {\n onClose?: () => void;\n \"data-testid\"?: string;\n } & PropsWithChildren &\n PropsWithClassName\n> = (p) => {\n // capture escape key to close the modal\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\" && p.onClose && !e.defaultPrevented) {\n e.preventDefault();\n p.onClose();\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [p]);\n\n return (\n
\n {p.children}\n
\n );\n};\n\ntype ClosableFullPageModalProps = {\n onClose?: () => void;\n \"data-testid\"?: string;\n} & PropsWithChildren &\n PropsWithClassName;\n\nexport const ClosableFullPageModal: FC = (p) => {\n // capture escape key to close the modal\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\" && p.onClose && !e.defaultPrevented) {\n e.stopPropagation();\n e.preventDefault();\n p.onClose();\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown, { capture: false });\n return () => window.removeEventListener(\"keydown\", handleKeyDown, { capture: false });\n }, [p]);\n\n const onOpenChange = (open: boolean) => {\n if (!open) {\n p.onClose?.();\n }\n };\n\n return (\n \n \n {p.children}\n \n \n );\n};\n\nexport const JustifiedContentColumn: FC = (p) => {\n return (\n
\n
{p.children}
\n
\n );\n};\n","import { ChevronRight } from \"lucide-react\";\nimport { useEffect } from \"react\";\nimport { Link, useMatches, useNavigate, type UIMatch } from \"react-router-dom\";\n\nexport default function Breadcrumbs() {\n const matches = useMatches() as UIMatch[];\n const crumbs = matches.filter((match) => Boolean(match.handle?.label));\n\n const parents = crumbs.slice(0, -1);\n const current = crumbs[crumbs.length - 1];\n\n const navigate = useNavigate();\n const pathOfParentPage = parents.length > 0 && parents[parents.length - 1].pathname;\n\n // capture escape key to close the modal\n useEffect(() => {\n if (!pathOfParentPage) {\n return;\n }\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.stopImmediatePropagation();\n e.stopPropagation();\n e.preventDefault();\n navigate(pathOfParentPage);\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown, { capture: true });\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown, { capture: true });\n };\n }, [navigate, pathOfParentPage, current]);\n\n return (\n
    \n {parents.map((crumb, index) => (\n
  1. \n {crumb?.handle?.label}\n {index != crumbs.length - 1 ? : null}\n
  2. \n ))}\n
  3. \n {current?.handle?.label}\n
  4. \n
\n );\n}\n","import { ThemeProviderContext } from \"@/contexts/ThemeProvider\";\nimport { useContext } from \"react\";\n\nexport const useTheme = () => {\n const context = useContext(ThemeProviderContext);\n\n if (context === undefined) throw new Error(\"useTheme must be used within a ThemeProvider\");\n\n return context;\n};\n","import { Button } from \"@/components/podkit/buttons/Button\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/podkit/dropdown/DropDown\";\nimport { Moon, Sun } from \"lucide-react\";\n\nexport function ThemeToggle() {\n const { setTheme } = useTheme();\n\n return (\n \n \n e.stopPropagation()}\n >\n \n \n Toggle theme\n \n \n \n setTheme(\"light\")}>Light\n setTheme(\"dark\")}>Dark\n setTheme(\"system\")}>System\n \n \n );\n}\n","import { cn } from \"@/components/podkit/lib/cn\";\nimport { Heading1 } from \"@/components/podkit/typography/Headings\";\nimport { ThemeToggle } from \"@/components/ThemeToggle\";\nimport { useMembership } from \"@/hooks/use-membership\";\nimport type { MenuItem } from \"@/menu-item\";\nimport { OrganizationRole } from \"gitpod-next-api/gitpod/v1/organization_pb\";\nimport type { FC } from \"react\";\nimport { Link, useLocation } from \"react-router-dom\";\n\nexport const SettingsSidebar: FC<{ menu: MenuItem[] }> = ({ menu }) => {\n const { membership } = useMembership();\n\n const isOrgAdmin = membership?.userRole === OrganizationRole.ADMIN;\n const items = menu.filter((entry) => isOrgAdmin || !entry.isAdminRole);\n const sections = [...new Set(items.map((item) => item.section || \"\"))];\n const itemsBySection = new Map(\n sections.map((section) => [section, items.filter((item) => item.section === section)]),\n );\n\n return (\n <>\n
\n Settings\n
\n {sections.map((section) => (\n
\n
{section}
\n\n {itemsBySection\n .get(section)\n ?.map((item) => )}\n
\n ))}\n
\n
\n \n
\n \n );\n};\n\nconst MenuItemComponent: FC<{ label: string; to: string }> = ({ label, to }) => {\n const location = useLocation();\n const active = location.pathname.includes(to);\n\n return (\n \n {label}\n \n );\n};\n","import Breadcrumbs from \"@/components/Breadcrumb\";\nimport { PageSuspense } from \"@/components/PageSuspense\";\nimport { SettingsSidebar } from \"@/components/settings/SettingsSidebar\";\nimport type { MenuItem } from \"@/menu-item\";\nimport { useEffect, useMemo, type FC } from \"react\";\nimport { Outlet, useMatches, useNavigate, type UIMatch } from \"react-router-dom\";\n\nexport const SettingsModalLayout: FC = () => {\n const matches = useMatches() as UIMatch[];\n\n const menu = useMemo(() => {\n return matches.find((match) => match.handle?.menu)?.handle.menu || [];\n }, [matches]);\n\n const navigate = useNavigate();\n\n /**\n * Reset the navigation when the component is unmounted.\n */\n useEffect(() => {\n return () => {\n navigate(\"/\");\n };\n }, [navigate]);\n\n return (\n
\n
\n \n
\n
\n
\n \n
\n
\n \n \n \n
\n
\n
\n );\n};\n","import type { RouteObject } from \"react-router-dom\";\n\nexport type MenuItem = {\n label: string;\n section: string;\n to: string;\n isAdminRole?: boolean;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function isMenuItem(value: any): value is MenuItem {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return (\n value &&\n typeof value.label === \"string\" &&\n typeof value.section === \"string\" &&\n typeof value.to === \"string\" &&\n (typeof value.isAdminRole === \"boolean\" || value.isAdminRole === undefined)\n );\n}\n\nexport const populateMenu = (route: RouteObject): RouteObject => {\n const menu: MenuItem[] = [];\n if (route.children) {\n for (const child of route.children) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const { handle } = child;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const menuItem = handle?.menuItem;\n if (isMenuItem(menuItem)) {\n menu.push(menuItem);\n handle.menu = menu;\n }\n }\n }\n return route;\n};\n","import { Card } from \"@/components/podkit/Card\";\nimport { Heading3, Subheading } from \"@/components/podkit/typography/Headings\";\nimport { type FC, type ReactNode } from \"react\";\n\nexport const defaultHeading = \"You're not authorized\";\nexport const defaultDescription =\n \"You do not have sufficient permissions to manage this organization. Contact your organization's admin to grant those permissions.\";\n\nexport const NotAuthorized: FC<{ heading?: ReactNode; description?: ReactNode }> = ({\n heading = defaultHeading,\n description = defaultDescription,\n}) => {\n return (\n \n
\n {heading}\n {description}\n
\n
\n );\n};\n","import type { Size } from \"@/assets/icons/geist/Size\";\nimport { type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport type { FC } from \"react\";\n\nexport const IconArrowDown: FC<{ size: Size } & PropsWithClassName> = ({ size, className }) => {\n switch (size) {\n case \"sm\":\n return (\n \n \n \n );\n case \"base\":\n return (\n \n \n \n );\n case \"lg\":\n return (\n \n \n \n );\n }\n};\n","/**\n * Copyright (c) 2023 Gitpod GmbH. All rights reserved.\n * Licensed under the GNU Affero General Public License (AGPL).\n * See License.AGPL.txt in the project root for license information.\n */\n\nimport { type PropsWithClassName, cn } from \"@/components/podkit/lib/cn\";\nimport React from \"react\";\n\nexport type HideableCellProps = {\n hideOnSmallScreen?: boolean;\n};\n\nexport const Table = React.forwardRef & PropsWithClassName>(\n ({ className, ...props }, ref) => {\n return (\n
\n \n \n );\n },\n);\nTable.displayName = \"Table\";\n\nexport const TableHeader = React.forwardRef<\n HTMLTableSectionElement,\n React.HTMLAttributes & PropsWithClassName\n>(({ className, ...props }, ref) => {\n return (\n \n );\n});\nTableHeader.displayName = \"TableHeader\";\n\nexport const TableRow = React.forwardRef<\n HTMLTableRowElement,\n React.HTMLAttributes & PropsWithClassName\n>(({ className, ...props }, ref) => {\n return ;\n});\nTableRow.displayName = \"TableRow\";\n\nexport const TableHead = React.forwardRef<\n HTMLTableCellElement,\n React.ThHTMLAttributes & PropsWithClassName\n>(({ hideOnSmallScreen, className, ...props }, ref) => {\n return
;\n});\nTableHead.displayName = \"TableHead\";\n\nexport const TableBody = React.forwardRef<\n HTMLTableSectionElement,\n React.HTMLAttributes & PropsWithClassName\n>(({ className, ...props }, ref) => {\n return (\n \n );\n});\nTableBody.displayName = \"TableBody\";\n\nexport const TableCell = React.forwardRef<\n HTMLTableCellElement,\n React.TdHTMLAttributes & PropsWithClassName\n>(({ hideOnSmallScreen, className, ...props }, ref) => {\n return ;\n});\nTableCell.displayName = \"TableCell\";\n","import { useListRunners, type PlainRunner } from \"@/queries/runner-queries\";\nimport { useEffect, useState } from \"react\";\nimport { RunnerKind } from \"gitpod-next-api/gitpod/v1/runner_pb\";\nimport { useAuthenticatedUser } from \"@/queries/user-queries\";\n\nexport function useAvailableRunners() {\n const { data: user } = useAuthenticatedUser();\n const {\n data: remoteRunners,\n isLoading: isLoadingRemoteRunners,\n isFetching: isFetchingRemoteRunners,\n isPending: isPendingRemoteRunners,\n } = useListRunners({ kind: RunnerKind.REMOTE });\n const {\n data: localRunners,\n isLoading: isLoadingLocalRunners,\n isFetching: isFetchingLocalRunners,\n isPending: isPendingLocalRunners,\n } = useListRunners({ creatorId: user?.id, kind: RunnerKind.LOCAL });\n\n const [availableRunners, setAvailableRunners] = useState([]);\n\n useEffect(() => {\n if (!user) {\n return;\n }\n const joined = [...(remoteRunners?.runners || []), ...(localRunners?.runners || [])];\n setAvailableRunners(joined);\n }, [remoteRunners?.runners, localRunners?.runners, user]);\n\n return {\n isLoading:\n isLoadingRemoteRunners ||\n isFetchingRemoteRunners ||\n isPendingRemoteRunners ||\n isLoadingLocalRunners ||\n isFetchingLocalRunners ||\n isPendingLocalRunners,\n availableRunners,\n };\n}\n","import { IconDot } from \"@/assets/icons/geist/IconDot\";\nimport { cn } from \"@/components/podkit/lib/cn\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/podkit/popover/Popover\";\nimport { type EffectiveState } from \"@/routes/environments/phase\";\nimport { EnvironmentPhase } from \"gitpod-next-api/gitpod/v1/environment_pb\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport { useMemo, type FC } from \"react\";\n\nexport const EnvironmentPhaseTag: FC<{\n state: EffectiveState;\n variant?: \"dot\" | \"label\";\n}> = ({ state, variant = \"label\" }) => {\n const { bgColor, fgColor, dotBgColor, pulse, name, warnings, failures, timeout } = useMemo(() => {\n if (state.state === \"OFFLINE\") {\n return {\n bgColor: \"bg-transparent\",\n dotBgColor: \"bg-content-tertiary\",\n fgColor: \"content-tertiary\",\n pulse: false,\n name: \"Offline\",\n ...state,\n };\n }\n if (state.failures && state.state !== EnvironmentPhase.DELETING) {\n return {\n bgColor: \"bg-surface-negative/10\",\n fgColor: \"content-negative\",\n dotBgColor: \"bg-content-negative\",\n pulse: false,\n name: \"Failed\",\n ...state,\n };\n }\n\n let bgColor = \"bg-surface-tertiary\";\n let dotBgColor = \"bg-content-tertiary\";\n let fgColor = \"content-tertiary\";\n let name = \"Unknown\";\n let pulse = false;\n\n switch (state.state) {\n case EnvironmentPhase.RUNNING:\n bgColor = \"bg-content-positive/10\";\n dotBgColor = \"bg-content-positive\";\n fgColor = \"text-content-positive\";\n name = \"Running\";\n pulse = false;\n if (state.warnings) {\n dotBgColor = \"bg-content-yield\";\n bgColor = \"bg-surface-yield\";\n fgColor = \"content-yield\";\n }\n break;\n case EnvironmentPhase.CREATING:\n bgColor = \"bg-content-yield/10\";\n dotBgColor = \"bg-content-yield\";\n fgColor = \"text-content-yield\";\n name = \"Creating...\";\n pulse = true;\n break;\n case EnvironmentPhase.STARTING:\n bgColor = \"bg-content-positive/4\";\n dotBgColor = \"bg-content-positive\";\n fgColor = \"text-content-positive\";\n name = \"Starting...\";\n pulse = true;\n break;\n case EnvironmentPhase.UPDATING:\n bgColor = \"bg-content-yield/10\";\n dotBgColor = \"bg-content-yield\";\n fgColor = \"text-content-yield\";\n name = \"Updating...\";\n pulse = true;\n break;\n case EnvironmentPhase.STOPPING:\n bgColor = \"bg-content-yield/10\";\n dotBgColor = \"bg-content-yield\";\n fgColor = \"text-content-yield\";\n name = \"Stopping...\";\n if (state.timeout) {\n name = `Auto-stopping...`;\n }\n pulse = true;\n break;\n case EnvironmentPhase.STOPPED:\n bgColor = \"bg-surface-tertiary\";\n dotBgColor = \"bg-content-tertiary\";\n fgColor = \"content-primary\";\n name = \"Stopped\";\n if (state.timeout) {\n name = `Auto-stopped`;\n }\n break;\n case EnvironmentPhase.DELETING:\n bgColor = \"bg-surface-negative/10\";\n dotBgColor = \"bg-content-negative\";\n fgColor = \"content-negative\";\n name = \"Deleting...\";\n pulse = true;\n break;\n }\n return {\n bgColor,\n fgColor,\n dotBgColor,\n pulse,\n name,\n ...state,\n };\n }, [state]);\n\n if (variant === \"dot\") {\n return (\n <>\n \n \n );\n }\n\n const problems =\n timeout || warnings || failures\n ? [...(timeout ? [timeout] : []), ...(failures || []), ...(warnings || [])]\n : undefined;\n\n return (\n \n \n
\n
\n
{name}
\n {problems && (\n \n \n \n \n \n
\n {problems\n .map((m) => m.split(\"\\n\"))\n .flat()\n .map((e, i) => (\n

{e}

\n ))}\n
\n
\n
\n )}\n \n );\n};\n","import { Button } from \"@/components/podkit/buttons/Button\";\nimport { IconArrowDown } from \"@/assets/icons/geist/IconArrowDown\";\nimport { DropdownMenuItem, DropdownMenuSeparator } from \"@/components/podkit/dropdown/DropDown\";\nimport { DropdownActions } from \"@/components/podkit/dropdown/DropDownActions\";\nimport { cn, type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport { LoadingState } from \"@/components/podkit/loading/LoadingState\";\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/podkit/modal/Modal\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/podkit/select/Select\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"@/components/podkit/tables/Table\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { getRepoUrl } from \"@/hooks/use-grouped-environments\";\nimport {\n useDeleteEnvironment,\n useListEnvironmentInventory,\n useStopEnvironment,\n type PlainEnvironment,\n type UseListEnvironmentInventoryParams,\n} from \"@/queries/environment-queries\";\nimport { useMembers, type PlainOrganizationMember } from \"@/queries/organization-queries\";\nimport { useListProjects, type PlainProject } from \"@/queries/project-queries\";\nimport { useListRunners, type PlainRunner } from \"@/queries/runner-queries\";\nimport { EnvironmentPhaseTag } from \"@/routes/environments/EnvironmentPhaseTag\";\nimport { effectiveState } from \"@/routes/environments/phase\";\nimport { formatError } from \"@/utils/errors\";\nimport { Timestamp } from \"@bufbuild/protobuf\";\nimport { EnvironmentPhase } from \"gitpod-next-api/gitpod/v1/environment_pb\";\nimport { useCallback, useEffect, useMemo, useState, type FC } from \"react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { useSearchParams } from \"react-router-dom\";\n\nexport const Inventory: FC = () => {\n const [searchParams] = useSearchParams();\n const runnerID = searchParams.get(\"runner\") || undefined;\n const creatorID = searchParams.get(\"creator\") || undefined;\n\n const { data: runnersData, isLoading: isLoadingRunners } = useListRunners({});\n const { data: membersData, isLoading: isLoadingMembers } = useMembers();\n const { data: projectsData, isLoading: isLoadingProjects } = useListProjects();\n const [filterValue, setFilterValue] = useState({ runnerID, creatorID });\n\n const handleFilterChange = useCallback((value: FilterValue) => {\n setFilterValue(value);\n }, []);\n\n const isLoading = isLoadingRunners || isLoadingMembers || isLoadingProjects;\n if (isLoading) {\n return ;\n }\n\n return (\n
\n
Inventory of running and stopped environments in your organization.
\n \n \n
\n );\n};\n\ntype FilterValue = UseListEnvironmentInventoryParams;\n\ntype FilterProps = {\n initialValue: FilterValue;\n runners: PlainRunner[];\n members: PlainOrganizationMember[];\n projects: PlainProject[];\n onFilterChange?: (value: FilterValue) => void;\n};\n\nconst allStatuses: Record = {\n Creating: EnvironmentPhase.CREATING,\n Starting: EnvironmentPhase.STARTING,\n Running: EnvironmentPhase.RUNNING,\n Updating: EnvironmentPhase.UPDATING,\n Stopping: EnvironmentPhase.STOPPING,\n Stopped: EnvironmentPhase.STOPPED,\n Deleting: EnvironmentPhase.DELETING,\n Deleted: EnvironmentPhase.DELETED,\n};\n\nconst Filter: FC = ({\n className,\n runners,\n members,\n projects,\n initialValue,\n onFilterChange,\n}) => {\n const [prevFilterValue, setPrevFilterValue] = useState(initialValue);\n\n const [selectedRunnerID, setSelectedRunnerID] = useState(initialValue.runnerID);\n const [selectedMemberID, setSelectedMemberID] = useState(initialValue.creatorID);\n const [selectedProjectID, setSelectedProjectID] = useState(initialValue.projectID);\n const [selectedStatus, setSelectedStatus] = useState();\n\n const debouncedFilterChanged = useDebouncedCallback((filter: FilterValue) => {\n if (!onFilterChange) {\n return;\n }\n onFilterChange(filter);\n }, 100);\n\n useEffect(() => {\n debouncedFilterChanged({ ...prevFilterValue });\n }, [prevFilterValue, debouncedFilterChanged]);\n\n useEffect(() => {\n setPrevFilterValue((prev) => {\n if (\n selectedRunnerID !== prev.runnerID ||\n selectedMemberID !== prev.creatorID ||\n selectedProjectID !== prev.projectID ||\n selectedStatus !== prev.status\n ) {\n return {\n ...prev,\n runnerID: selectedRunnerID,\n creatorID: selectedMemberID,\n projectID: selectedProjectID,\n status: selectedStatus ? allStatuses[selectedStatus] : undefined,\n };\n }\n return prev;\n });\n }, [selectedRunnerID, selectedMemberID, selectedProjectID, selectedStatus]);\n\n return (\n
\n [r.id, r.metadata?.name || r.id]))}\n onChange={setSelectedProjectID}\n />\n [r.userId, r.fullName]))}\n onChange={setSelectedMemberID}\n />\n [r.runnerId, r.name]))}\n onChange={setSelectedRunnerID}\n />\n [r, r]))}\n onChange={setSelectedStatus}\n />\n
\n );\n};\n\ntype FilterSelectProps = {\n initialValue?: string;\n items: Map;\n placeholder: string;\n onChange?: (value: string) => void;\n};\n\nconst FilterSelect: FC = ({ initialValue, placeholder, items, onChange }) => {\n const [value, setValue] = useState(initialValue);\n const onValueChange = useCallback(\n (value: string) => {\n const newValue = value === \"*\" ? \"\" : value;\n setValue(newValue);\n if (onChange) {\n onChange(newValue);\n }\n },\n [onChange],\n );\n\n return (\n \n );\n};\n\ntype InventoryTableProps = {\n filterValue: FilterValue;\n runners: PlainRunner[];\n members: PlainOrganizationMember[];\n projects: PlainProject[];\n};\n\nconst InventoryTable: FC = ({\n filterValue,\n className,\n members,\n runners,\n projects,\n}) => {\n const { data, hasNextPage, fetchNextPage } = useListEnvironmentInventory({ ...filterValue });\n const environments = data?.pages.flatMap((page) => page.environments) || [];\n\n const projectsMap = useMemo(() => {\n const map = new Map();\n for (const p of projects) {\n map.set(p.id, p);\n }\n return map;\n }, [projects]);\n const membersMap = useMemo(() => {\n const map = new Map();\n for (const m of members) {\n map.set(m.userId, m);\n }\n return map;\n }, [members]);\n const runnersMap = useMemo(() => {\n const map = new Map();\n for (const r of runners) {\n map.set(r.runnerId, r);\n }\n return map;\n }, [runners]);\n\n const isEmpty = environments.length === 0;\n if (isEmpty) {\n const anyFilters = filterValue.runnerID || filterValue.creatorID || filterValue.projectID || filterValue.status;\n\n if (anyFilters) {\n return (\n
\n No environments matching the filters.\n
\n );\n } else {\n return (\n
\n
\n
It's quiet here
\n Currently, there are no environments in your organization.\n
\n
\n );\n }\n }\n\n return (\n
\n
\n \n \n \n Project\n Member\n Runner\n Created\n Status\n \n \n \n \n {environments.map((environment, i) => (\n \n ))}\n \n
\n
\n
\n {hasNextPage && (\n \n )}\n
\n
\n );\n};\n\ntype InventoryItemProps = {\n environment: PlainEnvironment;\n member?: PlainOrganizationMember;\n runner?: PlainRunner;\n project?: PlainProject;\n};\n\nconst InventoryItem: FC = ({ environment, runner, member, project }) => {\n const [showModal, setShowModal] = useState<\"stop\" | \"delete\" | undefined>(undefined);\n\n const createdTime = useMemo(\n () =>\n environment.metadata?.createdAt\n ? new Timestamp(environment.metadata?.createdAt).toDate().toLocaleDateString()\n : \"\",\n [environment.metadata?.createdAt],\n );\n\n const state = useMemo(() => {\n if (environment && runner) {\n return effectiveState(environment, runner);\n }\n return;\n }, [environment, runner]);\n\n const { toast } = useToast();\n\n const stopEnvironment = useStopEnvironment();\n const handleStop = useCallback(() => {\n stopEnvironment.mutate(environment.id, {\n onError: (e) => {\n toast({\n title: \"Failed to stop environment\",\n description: formatError(e),\n });\n },\n });\n }, [stopEnvironment, toast, environment.id]);\n\n const deleteEnvironment = useDeleteEnvironment();\n const handleDelete = useCallback(() => {\n deleteEnvironment.mutate(\n { environmentId: environment.id, force: false },\n {\n onError: (e) => {\n toast({\n title: \"Failed to delete environment\",\n description: formatError(e),\n });\n },\n },\n );\n }, [deleteEnvironment, environment.id, toast]);\n\n const projectOrRepo = useMemo(() => {\n if (project?.metadata?.name) {\n return project?.metadata?.name;\n }\n return getRepoUrl(environment)?.repoUrl?.replace(\"https://github.com/\", \"\") || \"loading...\";\n }, [environment, project?.metadata?.name]);\n\n const handleCopyId = useCallback(async () => {\n await navigator.clipboard.writeText(environment.id);\n toast({\n title: `Environment ID copied to clipboard`,\n description: environment.id,\n });\n }, [toast, environment.id]);\n\n return (\n <>\n \n \n {projectOrRepo}\n \n \n
\n \n {member?.fullName}\n
\n
\n \n {runner?.name}\n \n \n {createdTime}\n \n \n {state && (\n <>\n
\n \n
\n
\n \n
\n \n )}\n
\n \n
\n \n Copy ID\n \n setShowModal(\"stop\")}\n >\n Stop\n \n setShowModal(\"delete\")} className=\"text-red-500\">\n Delete\n \n \n
\n
\n
\n {showModal === \"stop\" && member && runner && (\n {\n setShowModal(undefined);\n handleStop();\n }}\n onClose={() => {\n setShowModal(undefined);\n }}\n />\n )}\n {showModal === \"delete\" && member && runner && (\n {\n setShowModal(undefined);\n handleDelete();\n }}\n onClose={() => {\n setShowModal(undefined);\n }}\n />\n )}\n \n );\n};\n\nconst StopEnvironmentModal: FC<\n {\n onContinue: () => void;\n onClose: () => void;\n } & InventoryBannerProps\n> = (p) => {\n return (\n \n \n \n Stop environment\n \n\n \n This will stop the user's environment. They won't lose any changes.\n \n
\n Tip: You may want to let {p.member.fullName} know that you've stopped their environment.\n
\n
\n\n \n \n \n \n {\n e.preventDefault();\n p.onContinue();\n }}\n >\n Stop\n \n \n
\n
\n );\n};\n\nconst DeleteEnvironmentModal: FC<\n {\n onContinue: () => void;\n onClose: () => void;\n } & InventoryBannerProps\n> = (p) => {\n return (\n \n \n \n Delete environment\n \n\n \n This will delete the user's environment. They may lose any uncommitted changes.\n \n
\n Tip: You may want to let {p.member.fullName} know that you've deleted their environment.\n
\n
\n\n \n \n \n \n {\n e.preventDefault();\n p.onContinue();\n }}\n >\n Delete\n \n \n
\n
\n );\n};\n\ntype InventoryBannerProps = {\n environment: PlainEnvironment;\n member: PlainOrganizationMember;\n runner: PlainRunner;\n project?: PlainProject;\n} & PropsWithClassName;\n\nconst InventoryBanner: FC = (p) => {\n const projectOrRepo = useMemo(() => {\n if (p.project?.metadata?.name) {\n return p.project?.metadata?.name;\n }\n return getRepoUrl(p.environment)?.repoUrl?.replace(\"https://github.com/\", \"\") || \"loading...\";\n }, [p.environment, p.project?.metadata?.name]);\n return (\n \n
\n {projectOrRepo}\n {p.runner.name}\n
\n
\n \n {p.member?.fullName}\n
\n \n );\n};\n","import { NotAuthorized } from \"@/components/NotAuthorized\";\nimport { LoadingState } from \"@/components/podkit/loading/LoadingState\";\nimport { useDocumentTitle } from \"@/hooks/use-document-title\";\nimport { useMembership } from \"@/hooks/use-membership\";\nimport { OrganizationRole } from \"gitpod-next-api/gitpod/v1/organization_pb\";\nimport type { FC } from \"react\";\nimport { Inventory } from \"@/routes/environment-inventory/Inventory\";\n\nexport const EnvironmentInventoryPage: FC = () => {\n useDocumentTitle(\"Environments\");\n\n const { membership, isPending: isLoadingMembership } = useMembership();\n\n if (isLoadingMembership) {\n return ;\n }\n\n if (!membership) {\n return <>;\n }\n\n if (membership.userRole !== OrganizationRole.ADMIN) {\n return ;\n }\n\n return ;\n};\n","import { useDocumentTitle } from \"@/hooks/use-document-title\";\nimport type React from \"react\";\nimport { useCallback, useMemo, useState, type FC } from \"react\";\nimport {\n useDeleteHostAuthenticationToken,\n useListHostAuthenticationTokens,\n} from \"@/queries/runner-configuration-queries\";\nimport { ErrorMessage } from \"@/components/ErrorMessage\";\nimport type { PlainMessage } from \"@bufbuild/protobuf\";\nimport { HostAuthenticationTokenSource } from \"gitpod-next-api/gitpod/v1/runner_configuration_pb\";\nimport { type HostAuthenticationToken } from \"gitpod-next-api/gitpod/v1/runner_configuration_pb\";\nimport { GitHubIcon } from \"@/assets/icons/github\";\nimport { cn } from \"@/components/podkit/lib/cn\";\nimport { useListRunners } from \"@/queries/runner-queries\";\nimport { type Runner, RunnerKind } from \"gitpod-next-api/gitpod/v1/runner_pb\";\nimport { Dialog } from \"@radix-ui/react-dialog\";\nimport {\n DialogBody,\n DialogClose,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/podkit/modal/Modal\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { formatError } from \"@/utils/errors\";\nimport { LoadingState } from \"@/components/podkit/loading/LoadingState\";\nimport { useRunnerEnvironments } from \"@/queries/environment-queries\";\nimport { CheckIcon, Loader2 } from \"lucide-react\";\nimport { IconWarning } from \"@/assets/icons/geist/IconWarning\";\nimport { Button } from \"@/components/flexkit/Button\";\n\ntype RunnerToken = {\n runner: PlainMessage;\n token: PlainMessage;\n};\n\nexport const GitAuthenticationsPage: FC = () => {\n useDocumentTitle(\"Git Authentications\");\n const {\n data: tokensData,\n error: tokensError,\n isLoading: isLoadingTokens,\n isPending: isPendingTokens,\n } = useListHostAuthenticationTokens();\n const {\n data: runnersData,\n isLoading: isLoadingRunners,\n isPending: isPendingRunners,\n } = useListRunners({ kind: RunnerKind.REMOTE });\n\n const tokens: RunnerToken[] = useMemo(\n () => runnerTokens(tokensData?.tokens || [], runnersData?.runners || []),\n [tokensData, runnersData],\n );\n\n const isLoading = isLoadingTokens || isPendingTokens || isLoadingRunners || isPendingRunners;\n\n if (isLoading) {\n return ;\n }\n\n return (\n
\n \n {!tokensError && (\n
\n {tokens.length === 0 && There are no git authentications.}\n {tokens.map((rt) => (\n \n ))}\n
\n )}\n
\n );\n};\n\nconst GitHostAuthenticationToken: FC<{\n token: RunnerToken;\n}> = ({ token }) => {\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n return (\n \n {token.runner.name}\n
\n \n {token.token.host}\n \n
\n setShowDeleteModal(true)}\n data-testid={`git-authentications-delete-token-${token.token.id}`}\n data-track-label=\"true\"\n >\n Remove\n \n {showDeleteModal && (\n setShowDeleteModal(false)} />\n )}\n \n );\n};\n\nconst HostAuthenticationTokenDeleteModal: FC<{\n token: RunnerToken;\n onClose: () => void;\n}> = ({ token, onClose }) => {\n const { toast } = useToast();\n const deleteToken = useDeleteHostAuthenticationToken();\n const listEnvironments = useRunnerEnvironments(token.token.runnerId);\n\n const onOpenChange = (open: boolean) => {\n if (!open) {\n onClose();\n }\n };\n\n const handleDeleteToken = useCallback(() => {\n deleteToken.mutate(\n { tokenId: token.token.id },\n {\n onSuccess: () => {\n toast({ title: \"Authentication removed\" });\n onClose();\n },\n onError: (err) => {\n toast({ title: \"Failed to remove authentication\", description: formatError(err) });\n },\n },\n );\n }, [deleteToken, onClose, token, toast]);\n\n return (\n \n \n \n Remove authentication\n \n \n \n Remove your access to {token.token.host} for{\" \"}\n {token.runner.name} runner?\n \n
\n \n
\n
\n \n \n \n \n \n Yes, Remove\n \n \n \n
\n );\n};\n\nconst AffectedEnvironmentsText: FC<{\n token: RunnerToken;\n isLoading: boolean;\n environmentCount: number;\n error: Error | null;\n}> = ({ token, isLoading, environmentCount, error }) => {\n let icon: React.ReactNode;\n let content: string;\n\n if (isLoading) {\n icon = ;\n content = \"Verifying affected environments...\";\n } else if (error) {\n icon = ;\n content = `There was an error while verifying affected environments. Proceed with caution. Any existing environments will be unable to communicate with ${token.token.host} until you re-authenticate.`;\n } else if (environmentCount == 0) {\n icon = ;\n content = \"Safe to remove.\";\n } else {\n icon = ;\n content = `${environmentCount} environment${environmentCount > 1 ? \"s\" : \"\"} will be unable to communicate with ${token.token.host} until you re-authenticate.`;\n }\n\n return (\n
\n
{icon}
\n {content}\n
\n );\n};\n\nconst HostAuthenticationTokenSourceView: FC<{ source: HostAuthenticationTokenSource }> = ({ source }) => {\n let text = \"Unknown\";\n switch (source) {\n case HostAuthenticationTokenSource.OAUTH:\n text = \"OAuth\";\n break;\n case HostAuthenticationTokenSource.PAT:\n text = \"Personal Access Token\";\n break;\n }\n\n return (\n
\n {text}\n
\n );\n};\n\nfunction runnerTokens(tokens: PlainMessage[], runners: PlainMessage[]): RunnerToken[] {\n const byId = Object.fromEntries(runners.map((runner) => [runner.runnerId, runner]));\n\n const values: RunnerToken[] = [];\n for (const token of tokens) {\n const runner = byId[token.runnerId] || undefined;\n if (runner) {\n values.push({\n token,\n runner,\n });\n }\n }\n return values;\n}\n","import { cn } from \"@/components/podkit/lib/cn\";\nimport * as tags from \"react-tag-input-component\";\nimport \"./TagsInput.css\";\n\nexport type TagsInputProps = {\n className?: string;\n tagsClassName?: string;\n} & tags.TagsInputProps;\n\nexport const TagsInput: React.FC = ({ className, tagsClassName, ...props }) => {\n return (\n \n );\n};\nTagsInput.displayName = \"TagsInput\";\n","import { useId, type FC, useState, useMemo, useCallback } from \"react\";\nimport { Card } from \"@/components/podkit/Card\";\nimport { Button } from \"@/components/podkit/buttons/Button\";\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogBody,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/podkit/modal/Modal\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { type PlainOrganization, useDeleteOrganization } from \"@/queries/organization-queries\";\nimport { OrgIcon } from \"@/components/OrgIcon\";\nimport { Heading4, Subheading } from \"@/components/podkit/typography/Headings\";\nimport { Label } from \"@/components/podkit/forms/Label\";\nimport { Input } from \"@/components/podkit/forms/Input\";\nimport { LoadingButton } from \"@/components/podkit/buttons/LoadingButton\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { formatError } from \"@/utils/errors\";\nimport { useMembership } from \"@/hooks/use-membership\";\n\ntype DeleteOrganizationModalProps = {\n org: PlainOrganization;\n onContinue: () => void;\n onClose: () => void;\n};\n\nexport const DeleteOrganizationModal: FC = (p: DeleteOrganizationModalProps) => {\n const { toast } = useToast();\n const confirmationID = useId();\n const [confirmationValue, setConfirmationValue] = useState(\"\");\n\n const deleteOrganization = useDeleteOrganization();\n\n const onOpenChange = (open: boolean) => {\n if (!open) {\n p.onClose();\n }\n };\n\n const confirmed = useMemo(() => confirmationValue === p.org.name, [confirmationValue, p.org.name]);\n\n const handleDeleteOrganization = useCallback(() => {\n deleteOrganization.mutate(undefined, {\n onSuccess: () => {\n toast({ title: \"Organization deleted\" });\n p.onContinue();\n },\n onError: (err) => {\n toast({ title: \"Couldn't delete organization\", description: formatError(err) });\n },\n });\n }, [p, deleteOrganization, toast]);\n\n return (\n \n \n \n Delete organization\n \n\n \n \n \n Are you sure you want to permanently delete{\" \"}\n {p.org.name}?\n \n\n
    \n
  1. \n All environments will be deleted and cannot be restored later.\n
  2. \n
  3. \n All members will be loose access tp this organization and environments.\n
  4. \n
\n\n \n setConfirmationValue(e.target.value)}\n />\n
\n\n \n \n \n \n \n Yes, Delete Organization\n \n \n
\n
\n );\n};\n\nconst OrganizationCard: FC<{ org: PlainOrganization }> = (p) => {\n const cns = \"inline-flex w-full items-center justify-start gap-2 border-0 rounded-xl p-2 bg-surface-secondary\";\n const { membership } = useMembership();\n if (!membership) {\n return ;\n }\n return (\n \n \n
\n {p.org.name}\n {membership.organizationMemberCount} members\n
\n
\n );\n};\n","import { Button } from \"@/components/flexkit/Button\";\nimport { Input } from \"@/components/podkit/forms/Input\";\nimport { Label } from \"@/components/podkit/forms/Label\";\nimport { TagsInput } from \"@/components/podkit/forms/TagsInput\";\nimport { LoadingState } from \"@/components/podkit/loading/LoadingState\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { Heading3, Subheading } from \"@/components/podkit/typography/Headings\";\nimport { useOrganization, useUpdateOrganization, type PlainOrganization } from \"@/queries/organization-queries\";\nimport { DeleteOrganizationModal } from \"@/routes/manage-organization/DeleteOrganizationModal\";\nimport { formatError } from \"@/utils/errors\";\nimport { useCallback, useEffect, useId, useState, type FC, type FormEvent } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\n\nexport const OrganizationSettings: FC = () => {\n const { data: organization, isLoading } = useOrganization();\n\n if (!organization || isLoading) {\n return ;\n }\n\n return (\n
\n \n \n \n
\n );\n};\n\nconst OrganizationDetailsCard: FC = () => {\n const { toast } = useToast();\n\n const { data: orgData } = useOrganization();\n\n const inputNameID = useId();\n const [organizationName, setOrganizationName] = useState();\n const updateOrganization = useUpdateOrganization();\n\n useEffect(() => {\n setOrganizationName(orgData?.name);\n }, [orgData]);\n\n const handleUpdateName = useCallback(\n (evt: FormEvent) => {\n if (!orgData) {\n return;\n }\n evt.preventDefault();\n\n if (!organizationName) {\n toast({ title: \"Organization name cannot be empty\" });\n return;\n }\n\n updateOrganization.mutate(\n { name: organizationName },\n {\n onSuccess: () => {\n toast({ title: \"Updated display name\" });\n },\n onError: (err) => {\n toast({ title: \"Failed to update display name\", description: formatError(err) });\n },\n },\n );\n },\n [orgData, organizationName, updateOrganization, toast],\n );\n\n return (\n <>\n
\n \n setOrganizationName(e.target.value)}\n />\n\n \n \n \n );\n};\n\nconst OrganizationAllowedEmailDomainsCard: FC = () => {\n const { toast } = useToast();\n\n const { data: orgData } = useOrganization();\n\n const [allowedEmailDomains, setAllowedEmailDomains] = useState([]);\n const updateOrganization = useUpdateOrganization();\n\n useEffect(() => {\n if (!orgData) {\n return;\n }\n setAllowedEmailDomains(orgData?.inviteDomains?.domains || []);\n }, [orgData]);\n\n const handleUpdateInviteDomains = useCallback(\n (evt: FormEvent) => {\n if (!orgData) {\n return;\n }\n evt.preventDefault();\n\n updateOrganization.mutate(\n { inviteDomains: allowedEmailDomains },\n {\n onSuccess: ({ organization }) => {\n setAllowedEmailDomains(organization.inviteDomains?.domains || []);\n toast({\n title: \"Updated allowed email domains\",\n });\n },\n onSettled: (_, err) => {\n if (err) {\n setAllowedEmailDomains(orgData.inviteDomains?.domains || []);\n toast({\n title: \"Failed to update organization's allowed email domains\",\n description: formatError(err),\n });\n }\n },\n },\n );\n },\n [allowedEmailDomains, updateOrganization, orgData, toast],\n );\n\n return (\n
\n
\n Allowed email domains\n Enter an email domain of an existing member of this organization.\n
\n\n
\n \n\n \n \n
\n );\n};\n\ntype OrganizationDeleteCardProps = {\n org: PlainOrganization;\n};\n\nconst OrganizationDeleteCard: FC = ({ org }) => {\n const [showModal, setShowModal] = useState(false);\n const navigate = useNavigate();\n\n const onDeletion = useCallback(() => {\n // Move us away from the organization settings page\n navigate(\"/\", { replace: true });\n }, [navigate]);\n return (\n
\n
\n Delete organization\n This will delete this organization, including members and all their resources.\n
\n\n \n\n {showModal && (\n setShowModal(false)} />\n )}\n
\n );\n};\n","import { NotAuthorized } from \"@/components/NotAuthorized\";\nimport { LoadingState } from \"@/components/podkit/loading/LoadingState\";\nimport { useDocumentTitle } from \"@/hooks/use-document-title\";\nimport { useOrganization } from \"@/queries/organization-queries\";\nimport { useMembership } from \"@/hooks/use-membership\";\nimport { OrganizationSettings } from \"@/routes/manage-organization/OrganizationSettings\";\nimport { OrganizationRole } from \"gitpod-next-api/gitpod/v1/organization_pb\";\nimport type { FC } from \"react\";\n\nexport const ManageOrganizationPage: FC = () => {\n useDocumentTitle(\"Manage Organization\");\n\n const { data: organization, isLoading: isLoadingOrganization } = useOrganization();\n const { membership, isPending: isLoadingMembership } = useMembership();\n\n if (!organization && isLoadingOrganization) {\n return ;\n }\n\n if (!membership && isLoadingMembership) {\n return ;\n }\n\n if (membership?.userRole !== OrganizationRole.ADMIN) {\n return ;\n }\n\n return (\n <>\n \n \n );\n};\n","export const JOIN_ORGANIZATION_PATH = \"/join-organization\";\n\nexport const joinOrganizationURL = (inviteId: string) => {\n return `${window.location.origin}/join-organization/${inviteId}`;\n};\n","import type { Size } from \"@/assets/icons/geist/Size\";\nimport { type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport type { FC } from \"react\";\n\nexport const IconPlus: FC<{ size: Size } & PropsWithClassName> = ({ size, className }) => {\n switch (size) {\n case \"sm\":\n return (\n \n \n \n );\n case \"base\":\n return (\n \n \n \n );\n case \"lg\":\n return (\n \n \n \n );\n }\n};\n","import { NotAuthorized } from \"@/components/NotAuthorized\";\nimport { Input } from \"@/components/podkit/forms/Input\";\nimport { LoadingState } from \"@/components/podkit/loading/LoadingState\";\nimport { toast } from \"@/components/podkit/toasts/use-toast\";\nimport { Heading4, Subheading } from \"@/components/podkit/typography/Headings\";\nimport { useDocumentTitle } from \"@/hooks/use-document-title\";\nimport { useTemporaryState } from \"@/hooks/use-temporary-value\";\nimport { useCreateOrganizationInvite, useOrganization, useOrganizationInvite } from \"@/queries/organization-queries\";\nimport { useMembership } from \"@/hooks/use-membership\";\nimport { joinOrganizationURL } from \"@/routes/join-organization/join-organization\";\nimport { formatError } from \"@/utils/errors\";\nimport { OrganizationRole } from \"gitpod-next-api/gitpod/v1/organization_pb\";\nimport { Check, Copy } from \"lucide-react\";\nimport { useCallback, useMemo, type FC } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport { Button } from \"@/components/flexkit/Button\";\nimport { IconPlus } from \"@/assets/icons/geist/IconPlus\";\nimport { Text } from \"@/components/podkit/typography/Text\";\n\nexport const InviteMembersPage: FC = () => {\n useDocumentTitle(\"Invite Members\");\n\n const { data: organization, isLoading: isLoadingOrganization } = useOrganization();\n const { membership, isPending: isLoadingMembership } = useMembership();\n\n if (!organization && isLoadingOrganization) {\n return ;\n }\n\n if (!membership && isLoadingMembership) {\n return ;\n }\n\n if (membership?.userRole !== OrganizationRole.ADMIN) {\n return (\n \n );\n }\n\n return (\n
\n \n \n If you would like to invite people by email domain, you can set that up in{\" \"}\n \n Manage Organization\n \n .\n \n
\n );\n};\n\nexport const InviteMembers: FC = () => {\n const { data: invite } = useOrganizationInvite();\n const createOrganizationInvite = useCreateOrganizationInvite();\n const [copied, setCopied] = useTemporaryState(false, 2000);\n\n const inviteURL = useMemo(() => {\n return joinOrganizationURL(invite?.inviteId || \"\");\n }, [invite?.inviteId]);\n\n const handleCopyToClipboard = useCallback(() => {\n if (!invite?.inviteId) {\n return;\n }\n\n void navigator.clipboard.writeText(inviteURL).then(() => {\n setCopied(true);\n });\n }, [setCopied, invite?.inviteId, inviteURL]);\n\n return (\n
\n
\n Invite link\n Share this link with others you’d like to join your organization.\n
\n\n
\n \n
\n\n
\n (copied ? : )}\n data-track-label=\"true\"\n >\n Copy Link\n \n {\n createOrganizationInvite.mutate(void 0, {\n onSuccess: () => {\n toast({ title: `Invite link reset` });\n },\n onError: (error) => {\n toast({ title: `Failed to reset invite link`, description: formatError(error) });\n },\n });\n }}\n >\n Reset Invite Link\n \n
\n
\n );\n};\n","/**\n * Copyright (c) 2023 Gitpod GmbH. All rights reserved.\n * Licensed under the GNU Affero General Public License (AGPL).\n * See License.AGPL.txt in the project root for license information.\n */\n\nimport { type PropsWithClassName, cn } from \"@/components/podkit/lib/cn\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { Circle } from \"lucide-react\";\nimport React from \"react\";\n\nexport const RadioGroupItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & PropsWithClassName\n>(({ className, ...props }, ref) => {\n return (\n \n \n \n \n \n \n \n );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport type RadioListItem = {\n radio: React.ReactElement;\n label: React.ReactNode;\n hint?: React.ReactNode;\n};\n\nexport const RadioGroup = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & PropsWithClassName\n>(({ className, ...props }, ref) => {\n return ;\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n","import { Button } from \"@/components/flexkit/Button\";\nimport { Card } from \"@/components/podkit/Card\";\nimport { DropdownMenuItem } from \"@/components/podkit/dropdown/DropDown\";\nimport { DropdownActions } from \"@/components/podkit/dropdown/DropDownActions\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/podkit/forms/RadioListField\";\nimport { cn, type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport { LoadingState } from \"@/components/podkit/loading/LoadingState\";\nimport {\n Dialog,\n DialogBody,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/podkit/modal/Modal\";\nimport { Table, TableBody, TableCell, TableHeader, TableRow } from \"@/components/podkit/tables/Table\";\nimport { toast } from \"@/components/podkit/toasts/use-toast\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport {\n roleName,\n useLeaveOrganization,\n useSetRole,\n type PlainOrganizationMember,\n} from \"@/queries/organization-queries\";\nimport { useSuspendUser } from \"@/queries/user-queries\";\nimport { formatError } from \"@/utils/errors\";\nimport { Timestamp, type PlainMessage } from \"@bufbuild/protobuf\";\nimport { DialogClose } from \"@radix-ui/react-dialog\";\nimport type { AccountMembership } from \"gitpod-next-api/gitpod/v1/account_pb\";\nimport { OrganizationRole, type OrganizationMember } from \"gitpod-next-api/gitpod/v1/organization_pb\";\nimport type { PaginationResponse } from \"gitpod-next-api/gitpod/v1/pagination_pb\";\nimport type { User } from \"gitpod-next-api/gitpod/v1/user_pb\";\nimport { UserStatus } from \"gitpod-next-api/gitpod/v1/user_pb\";\nimport { useCallback, useMemo, useState, type FC } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\n\ntype MembersListProps = {\n user?: PlainMessage;\n membership?: PlainMessage;\n data: {\n members: PlainMessage[];\n pagination: PaginationResponse | undefined;\n };\n onInviteMembers?: () => void;\n};\n\nexport const MembersList: FC = ({ user, membership, data, onInviteMembers }) => {\n const isAdmin = membership?.userRole === OrganizationRole.ADMIN;\n\n if (!data.members || !user || !membership) {\n return ;\n }\n\n return (\n <>\n {isAdmin && (\n
\n
\n \n Invite Members\n \n
\n )}\n\n \n \n \n Name\n Date joined\n Role\n \n \n \n \n {data.members.map((member) => (\n \n ))}\n \n
\n \n );\n};\n\ntype MemberItemProps = {\n user: PlainMessage;\n membership: PlainMessage;\n member: PlainOrganizationMember;\n};\n\nconst MemberItem: FC = ({ user, membership, member }: MemberItemProps) => {\n const navigate = useNavigate();\n\n const leaveOrganization = useLeaveOrganization();\n const suspendUser = useSuspendUser();\n const setRole = useSetRole();\n\n const [showModal, setShowModal] = useState<\"suspend\" | \"change-role\" | \"leave\" | undefined>(undefined);\n\n const isModalShown = showModal !== undefined;\n\n const isMemberCurrentUser = user.id === member.userId;\n\n const isAdmin = membership.userRole == OrganizationRole.ADMIN;\n\n // TODO(gpl) display suspended/left differently?\n const inactive: boolean = member.status !== UserStatus.ACTIVE;\n\n const handleCopyId = useCallback(async () => {\n await navigator.clipboard.writeText(member.userId);\n toast({\n title: `Member ID copied to clipboard`,\n description: member.userId,\n });\n }, [member.userId]);\n\n const memberSince = useMemo(\n () => new Timestamp(member.memberSince).toDate().toLocaleDateString(),\n [member.memberSince],\n );\n\n const handleViewEnvironments = useCallback(() => {\n navigate(`/settings/environments?creator=${member.userId}`);\n }, [navigate, member.userId]);\n\n return (\n <>\n \n \n
\n \n
\n \n {member.fullName}\n \n {member.email}\n
\n
\n
\n \n {memberSince}\n \n \n {roleName(member.role)}\n \n \n
\n \n {isAdmin && (\n \n View Environments\n \n )}\n Copy ID\n {isAdmin && (\n setShowModal(\"change-role\")}>\n Change Role\n \n )}\n {isMemberCurrentUser && (\n setShowModal(\"leave\")}>\n Leave\n \n )}\n {isAdmin && !isMemberCurrentUser && (\n setShowModal(\"suspend\")}\n className=\"text-red-500\"\n >\n Remove\n \n )}\n \n
\n
\n {showModal === \"change-role\" && (\n {\n setShowModal(undefined);\n setRole.mutate(\n { userID: member.userId, role: role },\n {\n onSuccess: () => {\n toast({\n title: `Changed role for user ${member.fullName} to ${roleName(role)}`,\n });\n },\n onError: (e) => {\n toast({ title: `Failed to change role`, description: formatError(e) });\n },\n },\n );\n }}\n onClose={() => {\n setShowModal(undefined);\n }}\n />\n )}\n {showModal === \"suspend\" && (\n {\n setShowModal(undefined);\n suspendUser.mutate(\n { userId: member.userId, suspended: true },\n {\n onSuccess: () => {\n toast({ title: `User suspended` });\n },\n onError: (e) => {\n toast({ title: `Failed to suspend user`, description: formatError(e) });\n },\n },\n );\n }}\n onClose={() => {\n setShowModal(undefined);\n }}\n />\n )}\n {showModal === \"leave\" && (\n {\n setShowModal(undefined);\n leaveOrganization.mutate(\n { userId: member.userId },\n {\n onSuccess: () => {\n // TODO(at) check if this really works as intended\n navigate(\"/\", { replace: true });\n toast({ title: `You've left the organization` });\n },\n onError: (e) => {\n toast({\n title: `Failed to leave the organization`,\n description: formatError(e),\n });\n },\n },\n );\n }}\n onClose={() => {\n setShowModal(undefined);\n }}\n />\n )}\n
\n \n );\n};\n\nconst SuspendMemberModal: FC<{\n member: PlainOrganizationMember;\n onContinue: () => void;\n onClose: () => void;\n}> = (p) => {\n return (\n \n \n \n Suspend member\n \n\n Are you sure you want to suspend this member from the organization?\n\n \n\n \n \n \n \n {\n e.preventDefault();\n p.onContinue();\n }}\n >\n Yes, Suspend\n \n \n \n \n );\n};\n\nconst MemberCard: FC<{ member: PlainOrganizationMember } & PropsWithClassName> = (p) => {\n return (\n \n \n
\n {p.member.fullName}\n {p.member.email}\n
\n \n );\n};\n\nconst ChangeRoleModal: FC<{\n member: PlainOrganizationMember;\n onContinue: (role: OrganizationRole) => void;\n onClose: () => void;\n}> = (p) => {\n const [selectedRole, setSelectedRole] = useState(roleToString(p.member.role));\n\n return (\n \n \n \n Change role\n \n\n \n \n\n {\n setSelectedRole(roleToString(roleFromString(val)));\n }}\n >\n
\n \n \n
\n\n
\n \n \n
\n \n
\n\n \n \n \n \n {\n e.preventDefault();\n p.onContinue(roleFromString(selectedRole));\n }}\n >\n Continue\n \n \n
\n
\n );\n};\n\nconst LeaveOrganizationModal: FC<{\n onContinue: () => void;\n onClose: () => void;\n}> = (p) => {\n return (\n \n \n \n Leave organization\n \n\n \n Are you sure you want to leave this organization? You will have to be re-invited to come back.\n \n\n \n \n \n \n {\n e.preventDefault();\n p.onContinue();\n }}\n >\n Yes, Leave\n \n \n \n \n );\n};\n\nfunction roleToString(role: OrganizationRole): \"admin\" | \"member\" | \"unspecified\" {\n switch (role) {\n case OrganizationRole.ADMIN:\n return \"admin\";\n case OrganizationRole.MEMBER:\n return \"member\";\n case OrganizationRole.UNSPECIFIED:\n return \"unspecified\";\n }\n}\n\nfunction roleFromString(role: string): OrganizationRole {\n switch (role) {\n case \"admin\":\n return OrganizationRole.ADMIN;\n case \"member\":\n return OrganizationRole.MEMBER;\n default:\n return OrganizationRole.UNSPECIFIED;\n }\n}\n","import { useDocumentTitle } from \"@/hooks/use-document-title\";\nimport { useCallback, type FC } from \"react\";\nimport { MembersList } from \"@/routes/members/MembersList\";\nimport { useMembers } from \"@/queries/organization-queries\";\nimport { useMembership } from \"@/hooks/use-membership\";\nimport { useAuthenticatedUser } from \"@/queries/user-queries\";\nimport { useNavigate } from \"react-router-dom\";\n\nexport const MembersPage: FC = () => {\n useDocumentTitle(\"Members\");\n const { data: members } = useMembers();\n const { data: user } = useAuthenticatedUser();\n\n const { membership } = useMembership();\n\n const navigate = useNavigate();\n\n const onInviteMembers = useCallback(() => {\n navigate(\"invite\", { replace: true });\n }, [navigate]);\n\n return (\n \n );\n};\n","import { useGitpodAPI } from \"@/hooks/use-gitpod-api\";\nimport { defaultRetry, defaultThrowOnError } from \"@/queries/errors\";\nimport { keyWithPrincipal } from \"@/queries/principal-key\";\nimport { useAuthenticatedUser } from \"@/queries/user-queries\";\nimport { toPlainMessage, type PlainMessage } from \"@bufbuild/protobuf\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport type { PaginationResponse } from \"gitpod-next-api/gitpod/v1/pagination_pb\";\nimport {\n ListPersonalAccessTokensRequest,\n ListPersonalAccessTokensRequest_Filter,\n type PersonalAccessToken,\n} from \"gitpod-next-api/gitpod/v1/user_pb\";\n\nexport type PlainPersonalAccessToken = PlainMessage;\n\nexport function toPlainPersonalAccessToken(pat: PersonalAccessToken): PlainPersonalAccessToken {\n return toPlainMessage(pat);\n}\n\nexport const personalAccessTokensQueryKey = {\n list: (filter: Record) => keyWithPrincipal([\"pats\", \"list\", filter]),\n get: (patId?: string) => keyWithPrincipal([\"pats\", { patId }]),\n};\n\nexport type UseListPersonalAccessTokensParams = {\n userId: string;\n};\n\nexport const useListPersonalAccessTokens = ({ userId }: UseListPersonalAccessTokensParams) => {\n const api = useGitpodAPI();\n const { data: user } = useAuthenticatedUser();\n\n const query = useQuery({\n queryKey: personalAccessTokensQueryKey.list({ userId }),\n queryFn: async (): Promise => {\n if (!user) {\n throw new Error(\"User not authenticated\");\n }\n\n const { personalAccessTokens, pagination } = await api.userService.listPersonalAccessTokens(\n new ListPersonalAccessTokensRequest({\n filter: new ListPersonalAccessTokensRequest_Filter({\n userIds: [userId],\n }),\n }),\n );\n\n return {\n pats: personalAccessTokens.map(toPlainPersonalAccessToken),\n pagination,\n };\n },\n throwOnError: defaultThrowOnError,\n retry: defaultRetry,\n enabled: !!user,\n staleTime: 1_000 * 10, // 10 seconds\n gcTime: 1_000 * 60 * 60 * 24, // 24 hours\n });\n return { ...query, personalAccessTokens: query.data?.pats };\n};\n\ntype CachedPersonalAccessTokenList = {\n pats: PlainPersonalAccessToken[];\n pagination: PaginationResponse | undefined;\n};\n\nexport const useCreatePersonalAccessToken = () => {\n const client = useQueryClient();\n const api = useGitpodAPI();\n const { data: user } = useAuthenticatedUser();\n\n return useMutation({\n mutationFn: async ({ description, validForDays }: { description: string; validForDays: number }) => {\n if (!user) {\n throw new Error(\"User not authenticated\");\n }\n\n const { token } = await api.userService.createPersonalAccessToken({\n description,\n validFor: { seconds: BigInt(validForDays * 60 * 60 * 24) },\n userId: user.id,\n });\n\n if (!token) {\n throw new Error(\"Error creating runner\");\n }\n\n return {\n token,\n };\n },\n onSuccess: async () => {\n const prefixOfKeys = personalAccessTokensQueryKey.list({}).slice(0, -1);\n await client.invalidateQueries({ queryKey: prefixOfKeys });\n },\n });\n};\n\nexport const useDeletePersonalAccessToken = () => {\n const client = useQueryClient();\n const api = useGitpodAPI();\n const { data: user } = useAuthenticatedUser();\n\n return useMutation({\n mutationFn: async ({ personalAccessTokenId }: { personalAccessTokenId: string }) => {\n if (!user) {\n throw new Error(\"User not authenticated\");\n }\n\n return await api.userService.deletePersonalAccessToken({\n personalAccessTokenId,\n });\n },\n onSuccess: async () => {\n const prefixOfAllKeys = personalAccessTokensQueryKey.list({}).slice(0, -1);\n await client.invalidateQueries({ queryKey: prefixOfAllKeys });\n },\n });\n};\n","import { timeAgo } from \"@/format/time\";\nimport type { PlainPersonalAccessToken } from \"@/queries/personal-access-tokens-queries\";\nimport { Timestamp } from \"@bufbuild/protobuf\";\n\nconst timeAgoFormat = new Intl.RelativeTimeFormat(\"en\", {\n style: \"long\",\n});\nexport const getValidityDuration = (pat: PlainPersonalAccessToken) => {\n if (!pat.expiresAt) {\n return \"\";\n }\n const date = new Timestamp(pat.expiresAt).toDate();\n const relative = timeAgo(date, timeAgoFormat);\n let prefix = \"Expires\";\n if (date.getTime() < Date.now()) {\n prefix = \"Expired\";\n }\n return (\n \n {prefix} {relative}\n \n );\n};\n\nexport const getCreationTime = (pat: PlainPersonalAccessToken) => {\n if (!pat.createdAt) {\n return \"\";\n }\n const date = new Timestamp(pat.createdAt).toDate();\n const relative = timeAgo(date, timeAgoFormat);\n return {relative};\n};\n","import { Text } from \"@/components/podkit/typography/Text\";\nimport type { PlainPersonalAccessToken } from \"@/queries/personal-access-tokens-queries\";\nimport { getValidityDuration } from \"@/routes/personal-access-tokens/time-format\";\nimport type { FC, ReactNode } from \"react\";\n\nexport const PersonalAccessTokenCard: FC<{ pat: PlainPersonalAccessToken; children?: ReactNode }> = ({\n pat,\n children,\n}) => {\n return (\n
\n
\n {pat.description}\n {getValidityDuration(pat)}\n
\n {children}\n
\n );\n};\n","import { useCallback, useId, useState, type FC, type FormEvent } from \"react\";\nimport {\n Dialog,\n DialogHeader,\n DialogContent,\n DialogFooter,\n DialogClose,\n DialogBody,\n} from \"@/components/podkit/modal/Modal\";\n\nimport { Heading2 } from \"@/components/podkit/typography/Headings\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { Label } from \"@/components/podkit/forms/Label\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { Input } from \"@/components/podkit/forms/Input\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/podkit/select/Select\";\nimport { useCreatePersonalAccessToken, type PlainPersonalAccessToken } from \"@/queries/personal-access-tokens-queries\";\nimport { formatError } from \"@/utils/errors\";\nimport { Button } from \"@/components/flexkit/Button\";\nimport { CopyableTextArea } from \"@/components/podkit/forms/CopyableTextArea\";\nimport { PersonalAccessTokenCard } from \"@/routes/personal-access-tokens/PersonalAccessTokenCard\";\n\ntype Props = {\n onClose: () => void;\n};\n\nexport const NewPersonalAccessTokenModal: FC = ({ onClose }) => {\n const handleOpenChange = useCallback(\n (nextOpen: boolean) => {\n if (!nextOpen) {\n onClose();\n }\n },\n [onClose],\n );\n const [token, setToken] = useState();\n const [pat, setPat] = useState();\n\n return (\n \n \n \n New personal access token\n \n\n {!token && (\n {\n setPat(pat);\n setToken(tkn);\n }}\n />\n )}\n {token && pat && }\n \n \n );\n};\n\ntype NewPersonalAccessTokenFormProps = {\n onSuccess?: (pat: PlainPersonalAccessToken, token: string) => void;\n onClose?: () => void;\n};\nconst NewPersonalAccessTokenForm: FC = ({ onSuccess }) => {\n const { toast } = useToast();\n\n const [description, setDescription] = useState();\n const [validForDays, setValidForDays] = useState(\"30 days\");\n const inputDescriptionID = useId();\n const inputValidForID = useId();\n const createPAT = useCreatePersonalAccessToken();\n\n const handleSubmit = useCallback(\n async (event: FormEvent) => {\n event.preventDefault();\n if (!description || !validForDays) {\n return;\n }\n\n try {\n const validForDaysNumber = parseInt(validForDays.substring(0, validForDays.length - \" days\".length));\n const { token } = await createPAT.mutateAsync({\n description: description,\n validForDays: validForDaysNumber,\n });\n\n // close on success only\n onSuccess?.(\n // We fake the PAT structure here because the API doesn't return it\n {\n description,\n id: \"unknown\",\n userId: \"unknown\",\n expiresAt: {\n seconds: BigInt(Math.floor(Date.now() / 1000) + validForDaysNumber * 24 * 60 * 60),\n nanos: 0,\n },\n },\n token,\n );\n } catch (error) {\n toast({\n title: \"Failed to create personal access token.\",\n description: formatError(error),\n });\n }\n },\n [description, validForDays, onSuccess, toast, createPAT],\n );\n\n return (\n <>\n \n
\n
\n
\n
\n \n setDescription(e.target.value)}\n />\n
\n
\n \n setValidForDays(e)}\n >\n \n {validForDays}\n \n \n \n
\n
30 days
\n
\n
\n \n
\n
60 days
\n
\n
\n \n
\n
90 days
\n
\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n \n \n \n \n handleSubmit(e)}\n type=\"submit\"\n size=\"md\"\n variant=\"primary\"\n data-track-label=\"true\"\n >\n Create\n \n \n \n );\n};\n\ntype NewPersonalAccessTokenResultProps = {\n pat: PlainPersonalAccessToken;\n token: string;\n};\nconst NewPersonalAccessTokenResult: FC = ({ pat, token }) => {\n return (\n <>\n \n
\n \n Be sure to copy your personal access token now, as it won't be viewable again.\n \n \n \n \n
\n
\n \n \n \n \n \n \n );\n};\n","import { type FC, useCallback } from \"react\";\nimport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/podkit/modal/Modal\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { formatError } from \"@/utils/errors\";\nimport { useDeletePersonalAccessToken, type PlainPersonalAccessToken } from \"@/queries/personal-access-tokens-queries\";\nimport { PersonalAccessTokenCard } from \"@/routes/personal-access-tokens/PersonalAccessTokenCard\";\nimport { Button } from \"@/components/flexkit/Button\";\n\ntype DeletePersonalAccessTokenModalProps = {\n pat: PlainPersonalAccessToken;\n onClose: () => void;\n};\n\nexport const DeletePersonalAccessTokenModal: FC = ({\n pat,\n onClose,\n}: DeletePersonalAccessTokenModalProps) => {\n const { toast } = useToast();\n\n const deletePersonalAccessToken = useDeletePersonalAccessToken();\n\n const onOpenChange = (open: boolean) => {\n if (!open) {\n onClose();\n }\n };\n\n const handleDeletePersonalAccessToken = useCallback(() => {\n deletePersonalAccessToken.mutate(\n {\n personalAccessTokenId: pat.id,\n },\n {\n onSuccess: () => {\n toast({ title: \"Personal Access Token deleted\" });\n onClose();\n },\n onError: (err) => {\n toast({ title: \"Failed to delete personal access token\", description: formatError(err) });\n },\n },\n );\n }, [deletePersonalAccessToken, onClose, pat, toast]);\n\n return (\n \n \n \n Delete Personal Access Token\n \n\n \n
\n \n Once this token is removed, applications or scripts will lose access to the Gitpod API. This\n action cannot be undone.\n \n \n
\n
\n\n \n \n \n \n \n Yes, Delete\n \n \n
\n
\n );\n};\n","import { useMemo, useState, type FC } from \"react\";\nimport { LoadingState } from \"@/components/podkit/loading/LoadingState\";\nimport { useAuthenticatedUser } from \"@/queries/user-queries\";\nimport { useListPersonalAccessTokens, type PlainPersonalAccessToken } from \"@/queries/personal-access-tokens-queries\";\nimport { NewPersonalAccessTokenModal } from \"@/routes/personal-access-tokens/NewPersonalAccessTokenModal\";\nimport { useMembers, type PlainOrganizationMember } from \"@/queries/organization-queries\";\nimport { Button } from \"@/components/flexkit/Button\";\nimport { DeletePersonalAccessTokenModal } from \"@/routes/personal-access-tokens/DeletePersonalAccessTokenModal\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { Heading2 } from \"@/components/podkit/typography/Headings\";\nimport { getCreationTime, getValidityDuration } from \"@/routes/personal-access-tokens/time-format\";\n\nexport const PersonalAccessTokensList: FC = () => {\n const { data: user, isLoading: isLoadingUser } = useAuthenticatedUser();\n const useListPersonalAccessTokensFilter = { userId: user?.id || \"\" };\n const {\n data: patsData,\n isLoading: isLoadingPATs,\n isPending: isPendingPATs,\n } = useListPersonalAccessTokens(useListPersonalAccessTokensFilter);\n\n const { data: membersData, isLoading: isLoadingMembers, isPending: isPendingMembers } = useMembers();\n\n const isLoading = isLoadingPATs || isLoadingMembers || isPendingPATs || isPendingMembers || isLoadingUser;\n\n const patsToShow = useMemo(() => {\n if (!membersData || !patsData || !user) {\n return [];\n }\n\n const currentMember = membersData.members.find((m) => m.userId === user.id);\n\n return patsData.pats.map((pat) => {\n return {\n pat,\n creator: membersData.members.find((m) => m.userId === pat.creator?.id),\n currentMember,\n };\n });\n }, [membersData, patsData, user]);\n\n const [showNewPATModal, setShowNewPATModal] = useState(false);\n\n if (isLoading) {\n return ;\n }\n\n const hasPATs = patsToShow.length > 0;\n\n return (\n
\n

\n

\n \n Personal access tokens (PATs) are secure credentials that allow API access to Gitpod without\n manually logging in. They enable integration with external tools and workflow automation, while\n providing users the ability to revoke access when needed.\n \n {hasPATs && (\n setShowNewPATModal(true)}\n size={\"md\"}\n variant={\"secondary\"}\n data-track-label=\"true\"\n >\n New Token\n \n )}\n
\n

\n
\n {patsToShow?.map(({ pat, creator, currentMember }) => (\n \n ))}\n {!hasPATs && (\n
\n
\n No Personal Access Tokens\n
Create a new token to integrate with other systems.
\n
\n setShowNewPATModal(true)}\n size={\"md\"}\n variant={\"primary\"}\n data-track-label=\"true\"\n >\n New Token\n \n
\n )}\n {showNewPATModal && setShowNewPATModal(false)} />}\n
\n
\n );\n};\n\nconst PersonalAccessTokenRow: FC<{\n pat: PlainPersonalAccessToken;\n creator?: PlainOrganizationMember;\n currentMember?: PlainOrganizationMember;\n}> = ({ pat, creator, currentMember }) => {\n const [showDeletionModal, setShowDeletionModal] = useState(false);\n\n return (\n <>\n
\n
\n {pat.description}\n \n by {currentMember?.userId === creator?.userId ? \"You\" : creator?.fullName || \"Unknown\"}\n {\" · \"}\n {getCreationTime(pat)}\n \n
\n
\n
{getValidityDuration(pat)}
\n
\n
\n setShowDeletionModal(true)}\n data-track-label=\"true\"\n >\n Remove\n \n
\n
\n {showDeletionModal && (\n setShowDeletionModal(false)} />\n )}\n \n );\n};\n","import { useDocumentTitle } from \"@/hooks/use-document-title\";\nimport { type FC } from \"react\";\nimport { PersonalAccessTokensList } from \"@/routes/personal-access-tokens/PersonalAccessTokensList\";\n\nexport const PersonalAccessTokensPage: FC = () => {\n useDocumentTitle(\"Personal Access Tokens\");\n\n return ;\n};\n","export const RunnerConfigurationKeys = {\n AWSInstanceType: \"instanceType\",\n DiskSizeGB: \"diskSizeGB\",\n SpotInstanceEnabled: \"spot\",\n};\n\nexport const RunnerAdditionalFieldsKeys = {\n AWSAccountID: \"awsAccountID\",\n StackURL: \"awsCloudFormationStackURL\",\n StackName: \"awsCloudFormationStackName\",\n Region: \"region\",\n};\n","import type { Size } from \"@/assets/icons/geist/Size\";\nimport { type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport type { FC } from \"react\";\n\nexport const IconInfoFilled: FC<{ size: Size } & PropsWithClassName> = ({ size, className }) => {\n switch (size) {\n case \"sm\":\n return (\n \n \n \n \n \n \n \n \n \n \n );\n case \"base\":\n return (\n \n \n \n \n \n \n \n \n \n \n );\n case \"lg\":\n return (\n \n \n \n \n \n \n \n \n \n \n );\n }\n};\n","import type { Size } from \"@/assets/icons/geist/Size\";\nimport { type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport type { FC } from \"react\";\n\nexport const IconLock: FC<{ size: Size } & PropsWithClassName> = ({ size, className }) => {\n switch (size) {\n case \"sm\":\n return (\n \n \n \n );\n case \"base\":\n return (\n \n \n \n );\n case \"lg\":\n return (\n \n \n \n );\n }\n};\n","import { IconCheckCircle } from \"@/assets/icons/geist/IconCheckCircle\";\nimport { IconInfoFilled } from \"@/assets/icons/geist/IconInfoFilled\";\nimport { IconLock } from \"@/assets/icons/geist/IconLock\";\nimport { cn } from \"@/components/podkit/lib/cn\";\nimport { SkeletonText } from \"@/components/podkit/loading/Skeleton\";\nimport { useMemo, type FC, type PropsWithChildren } from \"react\";\n\nexport type SectionStatus = \"loading\" | \"required\" | \"optional\" | \"locked\" | \"initializing\" | \"complete\";\n\nexport const RunnerDetailsSection: FC<\n PropsWithChildren<{\n status: SectionStatus;\n }>\n> = ({ children, status }) => {\n return (\n
\n
\n \n
\n \n \n
\n
\n
\n
{children}
\n
\n );\n};\n\nconst SectionStatusIcon: FC<{ status: SectionStatus }> = ({ status }) => {\n switch (status) {\n case \"required\":\n return ;\n case \"locked\":\n case \"initializing\":\n return ;\n case \"optional\":\n return null;\n case \"complete\":\n return ;\n }\n};\n\nconst SectionStatusText: FC<{ status: SectionStatus }> = ({ status }) => {\n const { text, fgColor } = useMemo(() => {\n switch (status) {\n case \"loading\":\n return {\n text: \"Loading...\",\n fgColor: \"text-content-primary\",\n };\n case \"required\":\n return {\n text: \"Please complete this step\",\n fgColor: \"text-content-primary\",\n };\n case \"locked\":\n return {\n text: \"Complete the previous steps to unlock this\",\n fgColor: \"text-content-secondary\",\n };\n case \"initializing\":\n return {\n text: \"Waiting for runner to fully initialize\",\n fgColor: \"text-content-secondary\",\n };\n case \"optional\":\n return {\n text: \"Optional\",\n fgColor: \"text-content-primary\",\n };\n case \"complete\":\n return {\n text: \"Complete\",\n fgColor: \"text-content-primary\",\n };\n }\n }, [status]);\n return
{text}
;\n};\n","import { type PropsWithChildren, type ReactElement } from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nexport function Tooltip({\n children,\n content,\n delayDuration,\n}: PropsWithChildren & { content?: string | ReactElement; delayDuration?: number }) {\n return (\n \n \n {children}\n {content && (\n \n {content}\n \n )}\n \n \n );\n}\n","import { Button } from \"@/components/flexkit/Button\";\nimport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/podkit/modal/Modal\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { ExternalLink } from \"@/components/podkit/typography/Link\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { useRunnerEnvironments } from \"@/queries/environment-queries\";\nimport { useDeleteRunner, type PlainRunner } from \"@/queries/runner-queries\";\nimport { RunnerAdditionalFieldsKeys } from \"@/routes/runners/details/runner-configuration-keys\";\nimport { formatError } from \"@/utils/errors\";\nimport { useCallback, type FC } from \"react\";\n\ntype DeleteRunnerModalProps = {\n runner: PlainRunner;\n onClose: () => void;\n};\n\nexport const DeleteRunnerModal: FC = ({ runner, onClose }: DeleteRunnerModalProps) => {\n const { toast } = useToast();\n\n const deleteRunner = useDeleteRunner();\n const listEnvironments = useRunnerEnvironments(runner.runnerId);\n\n const onOpenChange = (open: boolean) => {\n if (!open) {\n onClose();\n }\n };\n\n const handleDeleteRunner = useCallback(() => {\n deleteRunner.mutate(\n {\n runnerId: runner.runnerId,\n force: false,\n },\n {\n onSuccess: () => {\n toast({ title: \"Runner scheduled for deletion\" });\n onClose();\n },\n onError: (err) => {\n toast({ title: \"Failed to delete runner\", description: formatError(err) });\n },\n },\n );\n }, [deleteRunner, onClose, runner, toast]);\n\n const environmentCount = listEnvironments.data?.environments.length ?? 0;\n const stackURL = runner.status?.additionalInfo.find(\n (info) => info.key === RunnerAdditionalFieldsKeys.StackURL,\n )?.value;\n\n return (\n \n \n \n Delete runner\n \n\n \n
\n {environmentCount > 0 ? (\n <>\n All environments associated with this runner will also be deleted.\n \n This will affect {environmentCount} environment{environmentCount > 1 ? \"s\" : \"\"}.{\" \"}\n \n \n ) : (\n There are no environments associated with this runner. \n )}\n Are you sure you'd like to delete it?\n {stackURL && (\n
\n Important:\n \n Remember to also delete the associated AWS CloudFormation stack manually after this\n operation.\n \n \n Stack URL:{\" \"}\n \n {stackURL}\n \n \n
\n )}\n
\n
\n\n \n \n \n \n \n Yes, Delete\n \n \n
\n
\n );\n};\n\nexport const ForceDeleteRunnerModal: FC = ({ runner, onClose }: DeleteRunnerModalProps) => {\n const { toast } = useToast();\n\n const deleteRunner = useDeleteRunner();\n const listEnvironments = useRunnerEnvironments(runner.runnerId);\n\n const onOpenChange = (open: boolean) => {\n if (!open) {\n onClose();\n }\n };\n\n const handleDeleteRunner = useCallback(() => {\n deleteRunner.mutate(\n { runnerId: runner.runnerId, force: true },\n {\n onSuccess: () => {\n toast({ title: \"Runner force deleted\" });\n onClose();\n },\n onError: (err) => {\n toast({ title: \"Failed to force delete runner\", description: formatError(err) });\n },\n },\n );\n }, [deleteRunner, onClose, runner, toast]);\n\n let withUndeletedText = `${listEnvironments.data?.environments.length} undeleted environments`;\n if (listEnvironments.data?.environments.length === 0) {\n withUndeletedText = \"no undeleted environments\";\n } else if (listEnvironments.data?.environments.length === 1) {\n withUndeletedText = \"1 undeleted environment\";\n }\n\n return (\n \n \n \n Warning: Irreversible Action\n \n\n \n \n You are about to permanently and forcibly delete{\" \"}\n "{runner.name}"{\" \"}\n {!listEnvironments.isLoading && (\n \n with {withUndeletedText}.{\" \"}\n \n )}\n However, please note:\n \n\n
    \n
  • \n Infrastructure and Files: This action does not\n guarantee the cleanup of any related infrastructure, files, or other dependencies which may\n continue to exist.\n
  • \n
  • \n Manual Cleanup Required: You may need to manually\n verify and clean up these elements.\n
  • \n
\n\n Are you sure you want to proceed with Force Delete?\n
\n\n \n \n \n \n \n Yes, Force Delete\n \n \n
\n
\n );\n};\n","import { Button } from \"@/components/flexkit/Button\";\nimport { Input } from \"@/components/podkit/forms/Input\";\nimport { Label } from \"@/components/podkit/forms/Label\";\nimport {\n Dialog,\n DialogBody,\n DialogClose,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/podkit/modal/Modal\";\nimport { useCallback, useId, useState, type FC, type FormEvent } from \"react\";\n\nimport { useUpdateRunner, type PlainRunner } from \"@/queries/runner-queries\";\n\ntype Props = {\n runner: PlainRunner;\n onClose: () => void;\n};\n\nexport const RenameRunnerModal: FC = ({ runner, onClose }) => {\n const updateRunner = useUpdateRunner();\n\n const [newName, setNewName] = useState();\n const name = newName || runner.name;\n\n const inputNameID = useId();\n\n const handleOpenChange = useCallback(\n (nextOpen: boolean) => {\n if (!nextOpen) {\n onClose();\n }\n },\n [onClose],\n );\n\n const handleSubmit = useCallback(\n async (event: FormEvent) => {\n event.preventDefault();\n if (!name) {\n return;\n }\n\n await updateRunner.mutateAsync({\n name,\n runnerId: runner.runnerId,\n });\n\n onClose();\n },\n [name, updateRunner, runner.runnerId, onClose],\n );\n\n return (\n \n \n \n Rename runner\n \n\n
\n \n
\n
\n \n setNewName(e.target.value)}\n />\n
\n
\n
\n\n \n \n \n \n \n Rename\n \n \n
\n
\n
\n );\n};\n","import { cn, type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport type { PlainRunner } from \"@/queries/runner-queries\";\nimport { RunnerKind } from \"gitpod-next-api/gitpod/v1/runner_pb\";\nimport { LaptopIcon, CloudIcon } from \"lucide-react\";\nimport { useMemo, type FC } from \"react\";\n\nexport const RunnerIcon: FC = ({ runner, className }) => {\n const isPersonal = useMemo(() => runner?.kind === RunnerKind.LOCAL, [runner?.kind]);\n\n const classes = cn(\"h-5 w-5 text-content-primary\", className);\n\n return isPersonal ? : ;\n};\n","export default \"__VITE_ASSET__D3D0WMWg__\"","import { IconPlus } from \"@/assets/icons/geist/IconPlus\";\nimport { Button } from \"@/components/flexkit/Button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/podkit/dropdown/DropDown\";\nimport { cn } from \"@/components/podkit/lib/cn\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { Tooltip } from \"@/components/Tooltip\";\nimport { timeAgo } from \"@/format/time\";\nimport type { PlainOrganizationMember } from \"@/queries/organization-queries\";\nimport type { PlainRunner } from \"@/queries/runner-queries\";\nimport { DeleteRunnerModal, ForceDeleteRunnerModal } from \"@/routes/runners/DeleteRunnerModal\";\nimport { RenameRunnerModal } from \"@/routes/runners/RenameRunnerModal\";\nimport { RunnerIcon } from \"@/routes/runners/RunnerIcon\";\nimport { Timestamp } from \"@bufbuild/protobuf\";\nimport { Principal } from \"gitpod-next-api/gitpod/v1/identity_pb\";\nimport { OrganizationRole } from \"gitpod-next-api/gitpod/v1/organization_pb\";\nimport { RunnerKind, RunnerPhase } from \"gitpod-next-api/gitpod/v1/runner_pb\";\nimport { MoreHorizontalIcon } from \"lucide-react\";\nimport { type FC, useCallback, useMemo, useState } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport awsLogo from \"@/assets/aws.svg\";\n\nexport const RunnerCard: FC<{\n runner: PlainRunner;\n creator?: PlainOrganizationMember;\n currentMember?: PlainOrganizationMember;\n}> = ({ runner, creator, currentMember }) => {\n const navigate = useNavigate();\n\n const [showRenameModal, setShowRenameModal] = useState(false);\n const [showDeletionModal, setShowDeletionModal] = useState(false);\n const [showForceDeleteModal, setShowForceDeleteModal] = useState(false);\n const { toast } = useToast();\n\n const handleViewDetails = useCallback(() => {\n navigate(`/settings/runners/${runner.runnerId}`);\n }, [navigate, runner.runnerId]);\n\n const handleCopyId = useCallback(async () => {\n await navigator.clipboard.writeText(runner.runnerId);\n toast({\n title: `Runner ID copied to clipboard`,\n description: runner.runnerId,\n });\n }, [toast, runner.runnerId]);\n\n const isCreator =\n runner.creator?.principal === Principal.USER\n ? runner.creator?.id === (currentMember?.userId || \"no-member\")\n : false;\n const isAdmin = currentMember?.role === OrganizationRole.ADMIN;\n const canEdit = isAdmin || isCreator;\n const canViewDetails = runner.kind === RunnerKind.REMOTE && canEdit;\n const showForceDelete = runner.spec?.desiredPhase === RunnerPhase.DELETED;\n\n const version = runner.status?.version || \"unknown\";\n\n const isRemoteRunner = runner.kind === RunnerKind.REMOTE;\n const isSelectable = isRemoteRunner;\n\n const handleViewEnvironments = useCallback(() => {\n navigate(`/settings/environments?runner=${runner.runnerId}`);\n }, [navigate, runner.runnerId]);\n\n const handleOnClick = useCallback(() => {\n if (!isSelectable) {\n return;\n }\n navigate(`/settings/runners/${runner.runnerId}`);\n }, [isSelectable, navigate, runner.runnerId]);\n\n return (\n <>\n \n
\n
\n
\n
\n \n
\n
\n \n \n \n \n \n {canViewDetails && (\n \n View Details\n \n )}\n {isAdmin && (\n \n View Environments\n \n )}\n Copy ID\n \n {canEdit && (\n setShowRenameModal(true)}>\n Rename\n \n )}\n {canEdit && (\n \n showForceDelete\n ? setShowForceDeleteModal(true)\n : setShowDeletionModal(true)\n }\n >\n {showForceDelete ? \"Force Delete\" : \"Delete\"}\n \n )}\n \n Version: {version}\n \n \n \n
\n
\n \n
{runner.name}
\n
\n
{toRunnerType(runner)}
\n
\n
\n
\n \n
\n
\n
Created {getCreationTime(runner)}
\n
\n by {currentMember?.userId === creator?.userId ? \"You\" : creator?.fullName || \"Unknown\"}\n
\n
\n
\n {showDeletionModal && setShowDeletionModal(false)} />}\n {showForceDeleteModal && (\n setShowForceDeleteModal(false)} />\n )}\n {showRenameModal && setShowRenameModal(false)} />}\n \n );\n};\n\nconst toRunnerType = (runner: PlainRunner) => {\n switch (runner.kind) {\n case RunnerKind.LOCAL:\n return \"Your computer\";\n case RunnerKind.REMOTE:\n return \"Amazon EC2\";\n default:\n return \"Unknown\";\n }\n};\n\nconst getCreationTime = (runner: PlainRunner) => {\n if (!runner.createdAt) {\n return \"\";\n }\n const date = new Timestamp(runner.createdAt).toDate();\n const relative = timeAgo(date);\n return {relative};\n};\n\nexport const AddNewRunnerCard: FC<{ setShowNewRunnerModal: (show: boolean) => void }> = ({ setShowNewRunnerModal }) => {\n return (\n
\n setShowNewRunnerModal(true)}\n >\n \n
Setup a runner
\n \n \n
\n );\n};\n\nexport const RunnerPhaseTag: FC<{ runner: PlainRunner }> = ({ runner }) => {\n const { fgColor, bgColor, pulse, name, tooltip } = useMemo(() => {\n const actual = runner.status?.phase;\n const message = runner.status?.message || \"\";\n const desired = runner.spec?.desiredPhase;\n\n switch (desired) {\n case RunnerPhase.DELETED:\n return {\n bgColor: \"bg-surface-negative/10\",\n fgColor: \"content-negative\",\n name: \"Pending deletion\",\n pulse: false,\n };\n }\n\n switch (actual) {\n case RunnerPhase.ACTIVE:\n return {\n bgColor: \"bg-content-positive/10\",\n fgColor: \"text-content-positive\",\n name: \"Online\",\n pulse: false,\n };\n case RunnerPhase.DEGRADED:\n return {\n bgColor: \"bg-surface-negative/10\",\n fgColor: \"content-negative\",\n name: \"Active (Degraded)\",\n tooltip: message,\n pulse: false,\n };\n case RunnerPhase.CREATED:\n return {\n bgColor: \"bg-surface-primary\",\n fgColor: \"text-content-orange\",\n name: \"Waiting to connect\",\n pulse: false,\n };\n case RunnerPhase.DELETING:\n return {\n bgColor: \"bg-surface-negative/10\",\n fgColor: \"content-negative\",\n name: \"Pending deletion\",\n pulse: false,\n };\n case RunnerPhase.INACTIVE:\n return {\n bgColor: \"bg-surface-negative/10\",\n fgColor: \"content-negative\",\n name: \"Offline\",\n pulse: false,\n };\n case RunnerPhase.DELETED:\n return {\n bgColor: \"bg-surface-primary\",\n fgColor: \"text-content-secondary\",\n name: \"Deleted\",\n pulse: false,\n };\n default:\n return {\n fgColor: \"\",\n bgColor: \"\",\n name: \"Unknonwn\",\n pulse: false,\n };\n }\n }, [runner]);\n\n return (\n \n \n
\n
{name}
\n
\n
\n \n );\n};\n","import type { PlainRunner } from \"@/queries/runner-queries\";\n\nconst templateURL = \"https://gitpod-flex-releases.s3.amazonaws.com/ec2/stable/gitpod-ec2-runner.json\";\n\nexport function createRunnerSetupURL(\n runner: PlainRunner,\n accessToken: string,\n region: string,\n cpURL = `${window.location.origin}/api`,\n) {\n return `https://${region}.console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/create/review?templateURL=${templateURL}&stackName=${stackName(runner.name)}¶m_ExchangeToken=${accessToken}¶m_Endpoint=${cpURL}¶m_RunnerID=${runner.runnerId}`;\n}\n\nexport function createRunnerSetupText(\n runner: PlainRunner,\n accessToken: string,\n region: string,\n cpURL = `${window.location.origin}/api`,\n) {\n return `\n Configuration details for ${runner?.name} runner\n\n Region:\n ${region}\n\n CloudFormation template URL:\n ${templateURL}\n\n Stack Name:\n ${stackName(runner.name)}\n\n Runner Token:\n ${accessToken}\n\n Runner ID:\n ${runner.runnerId}\n\n Endpoint:\n ${cpURL}\n `\n .split(\"\\n\")\n .map((line) => line.trimStart())\n .join(\"\\n\");\n}\n\nexport function stackName(runnerName: string) {\n // Stack name can include letters (A–Z and a–z), numbers (0–9) and dashes (-).\n const name = runnerName.replace(/\\s+/g, \"-\").replace(/[^a-zA-Z0-9\\\\-]/g, \"\");\n return `Gitpod-${name}`;\n}\n","import { IconExternalLink } from \"@/assets/icons/geist/IconExternalLink\";\nimport { ErrorMessage } from \"@/components/ErrorMessage\";\nimport { Button } from \"@/components/flexkit/Button\";\nimport { cn, type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport { SkeletonBlock, SkeletonText } from \"@/components/podkit/loading/Skeleton\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { ExternalLink } from \"@/components/podkit/typography/Link\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { useCreateRunnerAccessToken } from \"@/queries/runner-queries\";\nimport { RunnerAdditionalFieldsKeys } from \"@/routes/runners/details/runner-configuration-keys\";\nimport { RunnerDetailsSection, type SectionStatus } from \"@/routes/runners/details/RunnerDetailsSection\";\nimport { RunnerPhaseTag } from \"@/routes/runners/RunnerCard\";\nimport { createRunnerSetupText, createRunnerSetupURL } from \"@/routes/runners/RunnerSetupURL\";\nimport { formatError } from \"@/utils/errors\";\nimport type { PlainMessage } from \"@bufbuild/protobuf\";\nimport { RunnerKind, type Runner } from \"gitpod-next-api/gitpod/v1/runner_pb\";\nimport { useCallback, type FC, type ReactNode } from \"react\";\n\nexport const CloudFormationStack: FC<{ runner: PlainMessage | undefined; hasStack: boolean }> = ({\n runner,\n hasStack,\n}) => {\n if (runner && runner.kind !== RunnerKind.REMOTE) {\n return null;\n }\n\n let status: SectionStatus = \"loading\";\n if (runner) {\n status = hasStack ? \"complete\" : \"required\";\n }\n\n return (\n \n
\n
\n \n AWS CloudFormation\n \n {!hasStack && (\n \n We’ll help you setup a new runner on your AWS EC2 infrastructure. The link below will\n take you to AWS CloudFormation with a set of pre-populated values to quickly get everything\n working.{\" \"}\n \n Read the docs.\n \n \n )}\n
\n \n
\n
\n );\n};\n\nexport const CloudFormationStackContent: FC<\n { runner: PlainMessage | undefined; hasStack: boolean } & PropsWithClassName\n> = ({ runner, hasStack, className }) => {\n const { toast } = useToast();\n const createRunnerToken = useCreateRunnerAccessToken();\n\n const onCopyDetails = useCallback(async () => {\n if (!runner) {\n return;\n }\n try {\n const region = runner.spec?.configuration?.region;\n const { accessToken } = await createRunnerToken.mutateAsync(runner.runnerId);\n await navigator.clipboard.writeText(createRunnerSetupText(runner, accessToken, region || \"\"));\n toast({\n title: \"Message copied to clipboard\",\n });\n } catch (error) {\n toast({\n title: \"Failed to copy details to your clipboard\",\n description: formatError(error),\n });\n }\n }, [toast, createRunnerToken, runner]);\n return (\n \n {runner && (\n
\n
\n {hasStack ? (\n \n ) : (\n \n )}\n
\n {!hasStack && (\n
\n Does someone else manage your AWS account?\n \n Copy the details and share them.\n \n
\n )}\n
\n )}\n
\n );\n};\n\nconst CloudFormationStackCreate: FC<{ runner: PlainMessage }> = ({ runner }) => {\n const createRunnerToken = useCreateRunnerAccessToken();\n const { toast } = useToast();\n const onClick = useCallback(async () => {\n const region = runner.spec?.configuration?.region;\n const { accessToken } = await createRunnerToken.mutateAsync(runner.runnerId);\n const setupURL = createRunnerSetupURL(runner, accessToken, region || \"\");\n window.open(setupURL, \"awsWindow\", \"popup\");\n toast({\n title: \"Opened AWS CloudFormation Stack URL\",\n description: (\n \n If the window was blocked by your browser, you can use{\" \"}\n this link instead.\n \n ),\n });\n }, [runner, createRunnerToken, toast]);\n return (\n
\n
\n \n Once you've executed the CloudFormation template in AWS, please return to this page.\n \n \n Gitpod will register your runner after the stack has been created. This will take 2-3 minutes.\n \n
\n {runner && }\n \n Open AWS CloudFormation\n \n \n
\n );\n};\n\nconst CloudFormationStackDetails: FC<{ runner: PlainMessage }> = ({ runner }) => {\n const dict = Object.fromEntries(runner.status?.additionalInfo.map((info) => [info.key, info.value]) || []);\n\n const accountID = dict[RunnerAdditionalFieldsKeys.AWSAccountID] || \"\";\n const url = dict[RunnerAdditionalFieldsKeys.StackURL] || \"\";\n const name = dict[RunnerAdditionalFieldsKeys.StackName] || \"\";\n const region = dict[RunnerAdditionalFieldsKeys.Region] || \"\";\n\n return (\n
\n {runner && (\n \n \n \n )}\n {accountID && {accountID}}\n {name && {name}}\n {url && (\n \n \n {url}\n \n \n )}\n {runner.name}\n {region && {region}}\n
\n );\n};\n\nconst Info: FC<{ label: string; children: ReactNode }> = ({ label, children }) => (\n
\n {label}: \n {children}\n
\n);\n","import type { Size } from \"@/assets/icons/geist/Size\";\nimport { type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport type { FC } from \"react\";\n\nexport const IconChip: FC<{ size: Size } & PropsWithClassName> = ({ size, className }) => {\n switch (size) {\n case \"sm\":\n return null;\n case \"base\":\n return null;\n case \"lg\":\n return (\n \n \n \n );\n }\n};\n","import type { Size } from \"@/assets/icons/geist/Size\";\nimport { type PropsWithClassName } from \"@/components/podkit/lib/cn\";\nimport type { FC } from \"react\";\n\nexport const IconPlusSquare: FC<{ size: Size } & PropsWithClassName> = ({ size, className }) => {\n switch (size) {\n case \"sm\":\n return null;\n case \"base\":\n return null;\n case \"lg\":\n return (\n \n \n \n );\n }\n};\n","import { cn } from \"@/components/podkit/lib/cn\";\nimport { toast } from \"@/components/podkit/toasts/use-toast\";\nimport { useSetEnvironmentClassEnabled } from \"@/queries/runner-configuration-queries\";\nimport { formatError } from \"@/utils/errors\";\nimport type { EnvironmentClass } from \"gitpod-next-api/gitpod/v1/runner_configuration_pb\";\nimport { useCallback, type FC } from \"react\";\n\nexport const EnvironmentClassToggle: FC<{environmentClass: EnvironmentClass}> = ({environmentClass}) => {\n const setEnvironmentClassEnabled = useSetEnvironmentClassEnabled(environmentClass);\n\n const handleClick = useCallback(async () => {\n try {\n if (environmentClass.enabled) {\n await setEnvironmentClassEnabled.mutateAsync({ enabled: false });\n } else {\n await setEnvironmentClassEnabled.mutateAsync({ enabled: true });\n }\n } catch (error) {\n toast({\n title: `Failed to ${environmentClass.enabled ? \"disable\" : \"enable\"} environment class`,\n description: formatError(error),\n });\n }\n }, [setEnvironmentClassEnabled, environmentClass.enabled]);\n\n return (\n \n
\n
\n
\n \n );\n};\n","/**\n * Copyright (c) 2023 Gitpod GmbH. All rights reserved.\n * Licensed under the GNU Affero General Public License (AGPL).\n * See License.AGPL.txt in the project root for license information.\n */\n\nimport React from \"react\";\nimport * as ReactCheckbox from \"@radix-ui/react-checkbox\";\nimport { type PropsWithClassName, cn } from \"@/components/podkit/lib/cn\";\nimport { CheckIcon } from \"lucide-react\";\n\ntype CheckboxProps = React.ComponentPropsWithoutRef & PropsWithClassName;\n\nexport type CheckedState = ReactCheckbox.CheckedState;\n\nexport const Checkbox = React.forwardRef, CheckboxProps>(\n ({ className, ...props }, ref) => {\n return (\n \n \n \n \n \n \n \n );\n },\n);\nCheckbox.displayName = ReactCheckbox.Root.displayName;\n","import { type ReactNode, useCallback, useId } from \"react\";\nimport { cn } from \"@/components/podkit/lib/cn\";\nimport { Checkbox, type CheckedState } from \"@/components/podkit/checkbox/Checkbox\";\nimport { InputFieldHint } from \"@/components/podkit/forms/InputFieldHint\";\nimport { InputField } from \"@/components/podkit/forms/InputField\";\n\ntype Props = {\n id?: string;\n value?: string;\n checked?: CheckedState;\n disabled?: boolean;\n label: ReactNode;\n hint?: ReactNode;\n error?: ReactNode;\n className?: string;\n onChange?: (checked: boolean) => void;\n};\nexport const CheckboxInputField = ({\n id,\n value,\n label,\n hint,\n error,\n checked,\n disabled = false,\n className,\n onChange,\n}: Props) => {\n const maybeId = useId();\n const elementId = id ?? maybeId;\n\n const handleChange = useCallback(\n (state: CheckedState) => {\n onChange?.(state === true);\n },\n [onChange],\n );\n\n return (\n // Intentionally not passing label and hint to InputField because we want to render them differently for checkboxes.\n \n
\n \n \n );\n};\n","import { Button } from \"@/components/podkit/buttons/Button\";\nimport { Text } from \"@/components/podkit/typography/Text\";\nimport { DialogBody, DialogClose, DialogFooter, DialogHeader, DialogTitle } from \"@/components/podkit/modal/Modal\";\nimport {\n type EnvironmentClass,\n type EnvironmentClassValidationResult,\n type FieldValue,\n type RunnerConfigurationSchema_Field,\n} from \"gitpod-next-api/gitpod/v1/runner_configuration_pb\";\nimport { useCallback, useMemo, useState, type FC, type FormEvent } from \"react\";\nimport { LoadingButton } from \"@/components/podkit/buttons/LoadingButton\";\nimport { useSetEnvironmentClassEnabled, useUpdateEnvironmentClass } from \"@/queries/runner-configuration-queries\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { formatError } from \"@/utils/errors\";\nimport { InputField } from \"@/components/podkit/forms/InputField\";\nimport { Input } from \"@/components/podkit/forms/Input\";\n\nexport const EnvironmentClassEditModal: FC<{\n environmentClass: EnvironmentClass;\n schema: RunnerConfigurationSchema_Field[];\n onClose: () => void;\n}> = ({ environmentClass, schema, onClose }) => {\n const { toast } = useToast();\n const updateEnvironmentClass = useUpdateEnvironmentClass(environmentClass);\n const setEnvironmentClassEnabled = useSetEnvironmentClassEnabled(environmentClass);\n\n const [validationErrors, setValidationErrors] = useState();\n\n const handleSubmit = useCallback(\n async (values: { displayName: string; description: string }) => {\n try {\n const validationResult = await updateEnvironmentClass.mutateAsync({\n displayName: values.displayName,\n description: values.description,\n });\n setValidationErrors(validationResult);\n if (!validationResult?.valid) {\n return;\n }\n\n toast({\n title: \"Environment class updated\",\n description: \"The environment class has been updated successfully.\",\n });\n onClose();\n } catch (error) {\n toast({\n title: \"Failed to update environment class\",\n description: formatError(error),\n });\n }\n },\n [onClose, toast, updateEnvironmentClass],\n );\n\n return (\n <>\n \n Edit environment class\n \n\n \n \n \n\n \n
\n\n
\n \n \n Cancel\n \n \n \n Save\n \n
\n
\n
\n \n );\n};\n\nconst Configuration: FC<{\n configuration: FieldValue[];\n schema: RunnerConfigurationSchema_Field[];\n}> = ({ configuration, schema }) => {\n const fields = useMemo(() => Object.fromEntries(schema.map((field) => [field.id, field])), [schema]);\n return (\n
\n {configuration.map((field) => {\n let displayValue = field.value;\n if (field.key === \"spot\") {\n displayValue = field.value === \"true\" ? \"Yes\" : \"No\";\n }\n return (\n
\n {fields[field.key]?.name || field.key}:\n {displayValue}\n
\n );\n })}\n
\n );\n};\n\nexport const DisplayForm: FC<{\n formId: string;\n schema: RunnerConfigurationSchema_Field[];\n configuration: FieldValue[];\n validationErrors: EnvironmentClassValidationResult | undefined;\n initialValues: { displayName: string; description: string };\n onSubmit: (values: { displayName: string; description: string; configuration: FieldValue[] }) => void;\n}> = ({ formId, initialValues, schema, configuration, validationErrors, onSubmit }) => {\n const [name, setName] = useState(initialValues.displayName);\n const [description, setDescription] = useState(initialValues.description);\n\n const handleSubmit = useCallback(\n (event: FormEvent) => {\n event.preventDefault();\n onSubmit({\n description: description,\n displayName: name,\n configuration,\n });\n },\n [configuration, onSubmit, name, description],\n );\n\n return (\n
\n \n\n \n {\n setName(value.target.value);\n }}\n />\n \n\n \n {\n setDescription(value.target.value);\n }}\n />\n \n \n );\n};\n","import { Button } from \"@/components/podkit/buttons/Button\";\nimport { DialogBody, DialogClose, DialogFooter, DialogHeader, DialogTitle } from \"@/components/podkit/modal/Modal\";\nimport {\n EnvironmentClass,\n FieldValue,\n type EnvironmentClassValidationResult,\n type RunnerConfigurationSchema_BoolField,\n type RunnerConfigurationSchema_EnumField,\n type RunnerConfigurationSchema_Field,\n type RunnerConfigurationSchema_IntField,\n} from \"gitpod-next-api/gitpod/v1/runner_configuration_pb\";\nimport { startTransition, useCallback, useMemo, useState, type FC, type FormEvent } from \"react\";\nimport { LoadingButton } from \"@/components/podkit/buttons/LoadingButton\";\nimport { CheckboxInputField } from \"@/components/podkit/checkbox/CheckboxInputField\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport {\n SelectContentCombobox,\n SelectItemCombobox,\n SelectTrigger,\n SelectValue,\n} from \"@/components/podkit/select/Select\";\nimport { InputField } from \"@/components/podkit/forms/InputField\";\nimport { useCreateEnvironmentClass, useValidateEnvironmentClass } from \"@/queries/runner-configuration-queries\";\nimport { useToast } from \"@/components/podkit/toasts/use-toast\";\nimport { formatError } from \"@/utils/errors\";\nimport { RunnerConfigurationKeys } from \"@/routes/runners/details/runner-configuration-keys\";\nimport { DisplayForm } from \"@/routes/runners/details/EnvironmentClassEditModal\";\nimport { Input } from \"@/components/podkit/forms/Input\";\nimport { ComboboxProvider } from \"@ariakit/react\";\n\ntype State = { page: \"configuration\" } | { page: \"display\"; configuration: FieldValue[] };\n\nexport const EnvironmentClassAddModal: FC<{\n runnerId: string;\n schema: RunnerConfigurationSchema_Field[];\n onClose: () => void;\n}> = ({ runnerId, schema, onClose }) => {\n const { toast } = useToast();\n\n const validateEnvironmentClass = useValidateEnvironmentClass();\n const createEnvironmentClass = useCreateEnvironmentClass(runnerId);\n const isLoading = validateEnvironmentClass.isPending || createEnvironmentClass.isPending;\n\n const [validationErrors, setValidationErrors] = useState();\n const [state, setState] = useState({ page: \"configuration\" });\n\n const handleConfigurationSubmit = useCallback(\n async (configuration: FieldValue[]) => {\n const validationResult = await validateEnvironmentClass.mutateAsync(\n new EnvironmentClass({\n runnerId,\n displayName: defaultDisplayValue(configuration),\n description: defaultDescription(configuration),\n configuration,\n }),\n );\n setValidationErrors(validationResult);\n if (!validationResult?.valid) {\n return;\n }\n setState({ page: \"display\", configuration });\n },\n [runnerId, validateEnvironmentClass, setValidationErrors],\n );\n\n const handleSave = useCallback(\n async (values: { displayName: string; description: string; configuration: FieldValue[] }) => {\n const validationResult = await validateEnvironmentClass.mutateAsync(\n new EnvironmentClass({\n runnerId,\n displayName: values.displayName,\n description: values.description,\n configuration: values.configuration,\n }),\n );\n setValidationErrors(validationResult);\n if (!validationResult?.valid) {\n return;\n }\n\n try {\n await createEnvironmentClass.mutateAsync({\n configuration: values.configuration,\n description: values.description,\n displayName: values.displayName,\n });\n onClose();\n toast({\n title: \"Created environment class\",\n description: \"The environment class has been successfully created.\",\n });\n } catch (e) {\n toast({\n title: \"Failed to create environment class\",\n description: formatError(e),\n });\n }\n },\n [runnerId, onClose, toast, validateEnvironmentClass, setValidationErrors, createEnvironmentClass],\n );\n\n return (\n <>\n \n Add environment class\n \n\n \n {state.page === \"configuration\" && (\n \n )}\n {state.page === \"display\" && (\n \n )}\n \n\n \n \n \n \n \n {state.page === \"configuration\" ? \"Next\" : \"Create\"}\n \n \n \n );\n};\n\nconst ConfigurationForm: FC<{\n schema: RunnerConfigurationSchema_Field[];\n validationErrors: EnvironmentClassValidationResult | undefined;\n onSubmit: (cfg: FieldValue[]) => void;\n}> = ({ schema, validationErrors, onSubmit }) => {\n const [values, setValues] = useState>(\n Object.fromEntries(\n schema.map((field) => {\n const v = new FieldValue({\n key: field.id,\n value: encodeDefaultFieldValue(field),\n });\n return [field.id, v];\n }),\n ),\n );\n\n const handleSubmit = useCallback(\n (event: FormEvent) => {\n event.preventDefault();\n onSubmit(Object.values(values));\n },\n [onSubmit, values],\n );\n\n return (\n \n {schema.map((field) => {\n const validationError = validationErrors?.configurationErrors.find((e) => e.key === field.id)?.error;\n switch (field.type.case) {\n case \"enum\": {\n return (\n \n setValues((old) => ({\n ...old,\n [field.id]: new FieldValue({ key: field.id, value }),\n }))\n }\n />\n );\n }\n case \"int\": {\n return (\n \n setValues((old) => ({\n ...old,\n [field.id]: new FieldValue({\n key: field.id,\n value: encodeFieldValueInt(value),\n }),\n }))\n }\n />\n );\n }\n case \"bool\": {\n return (\n \n setValues((old) => ({\n ...old,\n [field.id]: new FieldValue({\n key: field.id,\n value: encodeFieldValueBool(value),\n }),\n }))\n }\n />\n );\n }\n }\n })}\n \n );\n};\n\nconst SchemaInputFieldEnum: FC<{\n field: RunnerConfigurationSchema_Field;\n fieldType: RunnerConfigurationSchema_EnumField;\n validationError?: string;\n onChange: (value: string) => void;\n}> = ({ field, fieldType, validationError, onChange }) => {\n const [selection, setSelection] = useState(fieldType.default !== \"\" ? fieldType.default : undefined);\n\n const updateValue = useCallback(\n (value: string) => {\n setSelection(value);\n onChange(value);\n },\n [setSelection, onChange],\n );\n const [open, setOpen] = useState(false);\n const [searchValue, setSearchValue] = useState(\"\");\n\n const matches = useMemo(() => {\n if (!searchValue) return fieldType.values.slice(0, 10);\n\n const words = searchValue.split(\" \");\n const matches = fieldType.values\n .filter((v) => {\n const label = v.toLowerCase();\n return words.reduce((acc, word) => acc && label.includes(word), true);\n })\n .slice(0, 200);\n\n // Radix Select does not work if we don't render the selected item, so we\n // make sure to include it in the list of matches.\n if (selection && !matches.includes(selection)) {\n matches.push(selection);\n }\n\n return matches;\n }, [fieldType, selection, searchValue]);\n\n return (\n