-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce org-level GITPOD_IMAGE_AUTH (#20538)
* [db, protocol] Introduce DBOrgEnvVar * [server, spicedb] Introduce and integrate org env vars into internal services * [server, public-api] Added API for org-level environment variables * [dashboard] Add UI for setting/removing GITPOD_IMAGE_AUTH to "Organization Settings" * [db, server] Fix DB queries, mapping to image-build args and fixed tests * [dashboard] Review comment "icon spacing" Co-authored-by: Filip Troníček <[email protected]> * [dashboard] Review comment superfluous key Co-authored-by: Filip Troníček <[email protected]> * [dashboard] more spacing Co-authored-by: Filip Troníček <[email protected]> * [dashboard] Copyright year Co-authored-by: Filip Troníček <[email protected]> * [public-api] Add converter test case --------- Co-authored-by: Filip Troníček <[email protected]>
- Loading branch information
1 parent
e7dbf43
commit ad4b7a8
Showing
33 changed files
with
9,211 additions
and
289 deletions.
There are no files selected for viewing
108 changes: 108 additions & 0 deletions
108
components/dashboard/src/data/organizations/org-envvar-queries.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
/** | ||
* Copyright (c) 2025 Gitpod GmbH. All rights reserved. | ||
* Licensed under the GNU Affero General Public License (AGPL). | ||
* See License.AGPL.txt in the project root for license information. | ||
*/ | ||
|
||
import { OrganizationEnvironmentVariable } from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; | ||
import { envVarClient } from "../../service/public-api"; | ||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||
|
||
const getListOrgEnvVarQueryKey = (orgId: string) => { | ||
const key: any[] = ["organization", orgId, "envvar", "list"]; | ||
|
||
return key; | ||
}; | ||
|
||
const getOrgEnvVarQueryKey = (orgId: string, variableId: string) => { | ||
const key: any[] = ["organization", orgId, "envvar", { variableId }]; | ||
|
||
return key; | ||
}; | ||
|
||
export const useListOrganizationEnvironmentVariables = (orgId: string) => { | ||
return useQuery<OrganizationEnvironmentVariable[]>(getListOrgEnvVarQueryKey(orgId), { | ||
queryFn: async () => { | ||
const { environmentVariables } = await envVarClient.listOrganizationEnvironmentVariables({ | ||
organizationId: orgId, | ||
}); | ||
|
||
return environmentVariables; | ||
}, | ||
cacheTime: 1000 * 60 * 60 * 24, // one day | ||
}); | ||
}; | ||
|
||
type DeleteEnvironmentVariableArgs = { | ||
variableId: string; | ||
organizationId: string; | ||
}; | ||
export const useDeleteOrganizationEnvironmentVariable = () => { | ||
const queryClient = useQueryClient(); | ||
|
||
return useMutation<void, Error, DeleteEnvironmentVariableArgs>({ | ||
mutationFn: async ({ variableId }) => { | ||
void (await envVarClient.deleteOrganizationEnvironmentVariable({ | ||
environmentVariableId: variableId, | ||
})); | ||
}, | ||
onSuccess: (_, { organizationId, variableId }) => { | ||
queryClient.invalidateQueries({ queryKey: getListOrgEnvVarQueryKey(organizationId) }); | ||
queryClient.invalidateQueries({ queryKey: getOrgEnvVarQueryKey(organizationId, variableId) }); | ||
}, | ||
}); | ||
}; | ||
|
||
type CreateEnvironmentVariableArgs = { | ||
organizationId: string; | ||
name: string; | ||
value: string; | ||
}; | ||
export const useCreateOrganizationEnvironmentVariable = () => { | ||
const queryClient = useQueryClient(); | ||
|
||
return useMutation<OrganizationEnvironmentVariable, Error, CreateEnvironmentVariableArgs>({ | ||
mutationFn: async ({ organizationId, name, value }) => { | ||
const { environmentVariable } = await envVarClient.createOrganizationEnvironmentVariable({ | ||
organizationId, | ||
name, | ||
value, | ||
}); | ||
if (!environmentVariable) { | ||
throw new Error("Failed to create environment variable"); | ||
} | ||
|
||
return environmentVariable; | ||
}, | ||
onSuccess: (_, { organizationId }) => { | ||
queryClient.invalidateQueries({ queryKey: getListOrgEnvVarQueryKey(organizationId) }); | ||
}, | ||
}); | ||
}; | ||
|
||
type UpdateEnvironmentVariableArgs = CreateEnvironmentVariableArgs & { | ||
variableId: string; | ||
}; | ||
export const useUpdateOrganizationEnvironmentVariable = () => { | ||
const queryClient = useQueryClient(); | ||
|
||
return useMutation<OrganizationEnvironmentVariable, Error, UpdateEnvironmentVariableArgs>({ | ||
mutationFn: async ({ variableId, name, value, organizationId }: UpdateEnvironmentVariableArgs) => { | ||
const { environmentVariable } = await envVarClient.updateOrganizationEnvironmentVariable({ | ||
environmentVariableId: variableId, | ||
organizationId, | ||
name, | ||
value, | ||
}); | ||
if (!environmentVariable) { | ||
throw new Error("Failed to update environment variable"); | ||
} | ||
|
||
return environmentVariable; | ||
}, | ||
onSuccess: (_, { organizationId, variableId }) => { | ||
queryClient.invalidateQueries({ queryKey: getListOrgEnvVarQueryKey(organizationId) }); | ||
queryClient.invalidateQueries({ queryKey: getOrgEnvVarQueryKey(organizationId, variableId) }); | ||
}, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 163 additions & 0 deletions
163
components/dashboard/src/teams/variables/NamedOrganizationEnvvarItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
/** | ||
* Copyright (c) 2025 Gitpod GmbH. All rights reserved. | ||
* Licensed under the GNU Affero General Public License (AGPL). | ||
* See License.AGPL.txt in the project root for license information. | ||
*/ | ||
|
||
import { OrganizationEnvironmentVariable } from "@gitpod/public-api/lib/gitpod/v1/envvar_pb"; | ||
import { useCallback, useState } from "react"; | ||
import { OrganizationRemoveEnvvarModal } from "./OrganizationRemoveEnvvarModal"; | ||
import { InputField } from "../../components/forms/InputField"; | ||
import { ReactComponent as Stack } from "../../icons/Repository.svg"; | ||
import { Button } from "@podkit/buttons/Button"; | ||
import { useCreateOrganizationEnvironmentVariable } from "../../data/organizations/org-envvar-queries"; | ||
import Modal, { ModalBody, ModalFooter, ModalFooterAlert, ModalHeader } from "../../components/Modal"; | ||
import { TextInputField } from "../../components/forms/TextInputField"; | ||
import { useToast } from "../../components/toasts/Toasts"; | ||
import { LoadingButton } from "@podkit/buttons/LoadingButton"; | ||
|
||
type Props = { | ||
disabled?: boolean; | ||
organizationId: string; | ||
name: string; | ||
variable: OrganizationEnvironmentVariable | undefined; | ||
}; | ||
export const NamedOrganizationEnvvarItem = ({ disabled, organizationId, name, variable }: Props) => { | ||
const [showRemoveModal, setShowRemoveModal] = useState<boolean>(false); | ||
const [showAddModal, setShowAddModal] = useState<boolean>(false); | ||
|
||
const value = variable ? "*****" : "not set"; | ||
|
||
return ( | ||
<> | ||
{variable && showRemoveModal && ( | ||
<OrganizationRemoveEnvvarModal | ||
variable={variable} | ||
organizationId={organizationId} | ||
onClose={() => setShowRemoveModal(false)} | ||
/> | ||
)} | ||
|
||
{showAddModal && ( | ||
<AddOrgEnvironmentVariableModal | ||
organizationId={organizationId} | ||
staticName={name} | ||
onClose={() => setShowAddModal(false)} | ||
/> | ||
)} | ||
|
||
<InputField disabled={disabled} className="w-full max-w-lg"> | ||
<div className="flex flex-col bg-gray-50 dark:bg-gray-800 p-3 rounded-lg"> | ||
<div className="flex items-center justify-between"> | ||
<div className="flex-1 flex items-center overflow-hidden h-8 gap-2" title={value}> | ||
<span className="w-5 h-5"> | ||
<Stack /> | ||
</span> | ||
<span className="truncate font-medium text-gray-700 dark:text-gray-200">{name}</span> | ||
</div> | ||
{!disabled && !variable && ( | ||
<Button variant="link" onClick={() => setShowAddModal(true)}> | ||
Add | ||
</Button> | ||
)} | ||
{!disabled && variable && ( | ||
<Button variant="link" onClick={() => setShowRemoveModal(true)}> | ||
Delete | ||
</Button> | ||
)} | ||
</div> | ||
<div className="mx-7 text-gray-400 dark:text-gray-500 truncate"> | ||
<>{value}</> | ||
{disabled && ( | ||
<> | ||
· Requires <span className="font-medium">Owner</span> permissions to | ||
change | ||
</> | ||
)} | ||
</div> | ||
</div> | ||
</InputField> | ||
</> | ||
); | ||
}; | ||
|
||
type AddOrgEnvironmentVariableModalProps = { | ||
organizationId: string; | ||
staticName?: string; | ||
onClose: () => void; | ||
}; | ||
export const AddOrgEnvironmentVariableModal = ({ | ||
organizationId, | ||
staticName, | ||
onClose, | ||
}: AddOrgEnvironmentVariableModalProps) => { | ||
const { toast } = useToast(); | ||
|
||
const [name, setName] = useState(staticName || ""); | ||
const [value, setValue] = useState(""); | ||
const createVariable = useCreateOrganizationEnvironmentVariable(); | ||
|
||
const addVariable = useCallback(() => { | ||
createVariable.mutateAsync( | ||
{ | ||
organizationId, | ||
name, | ||
value, | ||
}, | ||
{ | ||
onSuccess: () => { | ||
toast("Variable added"); | ||
onClose(); | ||
}, | ||
}, | ||
); | ||
}, [createVariable, organizationId, name, value, onClose, toast]); | ||
|
||
return ( | ||
<Modal visible onClose={onClose} onSubmit={addVariable}> | ||
<ModalHeader>Add a variable</ModalHeader> | ||
<ModalBody> | ||
<div className="mt-8"> | ||
<TextInputField | ||
disabled={staticName !== undefined} | ||
label="Name" | ||
autoComplete={"off"} | ||
className="w-full" | ||
value={name} | ||
placeholder="Variable name" | ||
onChange={(name) => setName(name)} | ||
autoFocus | ||
required | ||
/> | ||
</div> | ||
<div className="mt-4"> | ||
<TextInputField | ||
label="Value" | ||
autoComplete={"off"} | ||
className="w-full" | ||
value={value} | ||
placeholder="Variable value" | ||
onChange={(value) => setValue(value)} | ||
required | ||
/> | ||
</div> | ||
</ModalBody> | ||
<ModalFooter | ||
alert={ | ||
createVariable.isError ? ( | ||
<ModalFooterAlert type="danger"> | ||
{String(createVariable.error).replace(/Error: Request \w+ failed with message: /, "")} | ||
</ModalFooterAlert> | ||
) : null | ||
} | ||
> | ||
<Button variant="secondary" onClick={onClose}> | ||
Cancel | ||
</Button> | ||
<LoadingButton type="submit" loading={createVariable.isLoading}> | ||
Add Variable | ||
</LoadingButton> | ||
</ModalFooter> | ||
</Modal> | ||
); | ||
}; |
Oops, something went wrong.