Files
komodo/frontend/src/components/resources/build/config.tsx
Maxwell Becker d05c81864e 1.16.3 (#150)
* refactor listener api implementation for Gitlab integration

* version 1.16.3

* builder delete id link cleanup

* refactor and add "__ALL__" branch to avoid branch filtering

* frontend config the webhook url

* action webhook config

* clean up webhook url copy

* add __ALL__ branch switch for Actions / Procedures
2024-10-24 16:03:00 -07:00

477 lines
17 KiB
TypeScript

import { Config } from "@components/config";
import {
AccountSelectorConfig,
AddExtraArgMenu,
ConfigInput,
ConfigItem,
ImageRegistryConfig,
InputList,
ProviderSelectorConfig,
SystemCommand,
WebhookBuilder,
} from "@components/config/util";
import {
getWebhookIntegration,
useInvalidate,
useLocalStorage,
useRead,
useWebhookIdOrName,
useWebhookIntegrations,
useWrite,
} from "@lib/hooks";
import { Types } from "komodo_client";
import { Button } from "@ui/button";
import { Ban, CirclePlus, PlusCircle } from "lucide-react";
import { ReactNode } from "react";
import { CopyWebhook, ResourceLink, ResourceSelector } from "../common";
import { useToast } from "@ui/use-toast";
import { text_color_class_by_intention } from "@lib/color";
import { ConfirmButton } from "@components/util";
import { Link } from "react-router-dom";
import { SecretsSearch } from "@components/config/env_vars";
import { MonacoEditor } from "@components/monaco";
export const BuildConfig = ({
id,
titleOther,
}: {
id: string;
titleOther: ReactNode;
}) => {
const perms = useRead("GetPermissionLevel", {
target: { type: "Build", id },
}).data;
const build = useRead("GetBuild", { build: id }).data;
const config = build?.config;
const name = build?.name;
const webhook = useRead("GetBuildWebhookEnabled", { build: id }).data;
const global_disabled =
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
const [update, set] = useLocalStorage<Partial<Types.BuildConfig>>(
`build-${id}-update-v1`,
{}
);
const { mutateAsync } = useWrite("UpdateBuild");
const { integrations } = useWebhookIntegrations();
const [id_or_name] = useWebhookIdOrName();
if (!config) return null;
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
const git_provider = update.git_provider ?? config.git_provider;
const webhook_integration = getWebhookIntegration(integrations, git_provider);
return (
<Config
titleOther={titleOther}
disabled={disabled}
config={config}
update={update}
set={set}
onSave={async () => {
await mutateAsync({ id, config: update });
}}
components={{
"": [
{
label: "Builder",
labelHidden: true,
components: {
builder_id: (builder_id, set) => {
return (
<ConfigItem
label={
builder_id ? (
<div className="flex gap-3 text-lg">
Builder:
<ResourceLink type="Builder" id={builder_id} />
</div>
) : (
"Select Builder"
)
}
description="Select the Builder to build with."
>
<ResourceSelector
type="Builder"
selected={builder_id}
onSelect={(builder_id) => set({ builder_id })}
disabled={disabled}
align="start"
/>
</ConfigItem>
);
},
},
},
{
label: "Version",
components: {
version: (_version, set) => {
const version =
typeof _version === "object"
? `${_version.major}.${_version.minor}.${_version.patch}`
: _version;
return (
<ConfigInput
className="text-lg w-[200px]"
label="Version"
description="Version the image with major.minor.patch. It can be interpolated using [[$VERSION]]."
placeholder="0.0.0"
value={version}
onChange={(version) => set({ version: version as any })}
disabled={disabled}
boldLabel
/>
);
},
auto_increment_version: {
description:
"Automatically increment the patch number on every build.",
},
},
},
{
label: "Source",
components: {
git_provider: (provider, set) => {
const https = update.git_https ?? config.git_https;
return (
<ProviderSelectorConfig
account_type="git"
selected={provider}
disabled={disabled}
onSelect={(git_provider) => set({ git_provider })}
https={https}
onHttpsSwitch={() => set({ git_https: !https })}
/>
);
},
git_account: (account, set) => (
<AccountSelectorConfig
id={update.builder_id ?? config.builder_id ?? undefined}
type="Builder"
account_type="git"
provider={update.git_provider ?? config.git_provider}
selected={account}
onSelect={(git_account) => set({ git_account })}
disabled={disabled}
placeholder="None"
/>
),
repo: {
placeholder: "Enter repo",
description:
"The repo path on the provider. {namespace}/{repo_name}",
},
branch: {
placeholder: "Enter branch",
description: "Select a custom branch, or default to 'main'.",
},
commit: {
label: "Commit Hash",
placeholder: "Input commit hash",
description:
"Optional. Switch to a specific commit hash after cloning the branch.",
},
},
},
{
label: "Build",
labelHidden: true,
components: {
build_path: {
placeholder: ".",
description:
"The cwd to run 'docker build', relative to the root of the repo.",
},
dockerfile_path: {
placeholder: "Dockerfile",
description:
"The path to the dockerfile, relative to the build path.",
},
},
},
{
label: "Registry",
labelHidden: true,
components: {
image_registry: (registry, set) => (
<ImageRegistryConfig
registry={registry}
setRegistry={(image_registry) => set({ image_registry })}
resource_id={update.builder_id ?? config.builder_id}
disabled={disabled}
/>
),
},
},
{
label: "Links",
description: "Add quick links in the resource header",
contentHidden: ((update.links ?? config.links)?.length ?? 0) === 0,
actions: !disabled && (
<Button
variant="secondary"
onClick={() =>
set((update) => ({
...update,
links: [...(update.links ?? config.links ?? []), ""],
}))
}
className="flex items-center gap-2 w-[200px]"
>
<PlusCircle className="w-4 h-4" />
Add Link
</Button>
),
components: {
links: (values, set) => (
<InputList
field="links"
values={values ?? []}
set={set}
disabled={disabled}
placeholder="Input link"
/>
),
},
},
],
advanced: [
{
label: "Tagging",
components: {
image_name: {
description: "Push the image under a different name",
placeholder: "Custom image name",
},
image_tag: {
description: "Postfix the image version with a custom tag.",
placeholder: "Custom image tag",
},
},
},
{
label: "Pre Build",
description:
"Execute a shell command before running docker build. The 'path' is relative to the root of the repo.",
components: {
pre_build: (value, set) => (
<SystemCommand
value={value}
set={(value) => set({ pre_build: value })}
disabled={disabled}
/>
),
},
},
{
label: "Build Args",
description:
"Pass build args to 'docker build'. These can be used in the Dockerfile via ARG, and are visible in the final image.",
labelExtra: !disabled && <SecretsSearch />,
components: {
build_args: (env, set) => (
<MonacoEditor
value={env || " # VARIABLE = value\n"}
onValueChange={(build_args) => set({ build_args })}
language="key_value"
readOnly={disabled}
/>
),
},
},
{
label: "Secret Args",
description: (
<div className="flex flex-row flex-wrap gap-2">
<div>
Pass secrets to 'docker build'. These values remain hidden in
the final image by using docker secret mounts.
</div>
<Link
to="https://docs.rs/komodo_client/latest/komodo_client/entities/build/struct.BuildConfig.html#structfield.secret_args"
target="_blank"
className="text-primary"
>
See docker docs.
</Link>
</div>
),
labelExtra: !disabled && <SecretsSearch />,
components: {
secret_args: (env, set) => (
<MonacoEditor
value={env || " # VARIABLE = value\n"}
onValueChange={(secret_args) => set({ secret_args })}
language="key_value"
readOnly={disabled}
/>
),
},
},
{
label: "Extra Args",
labelHidden: true,
components: {
extra_args: (value, set) => (
<ConfigItem
label="Extra Args"
boldLabel
description={
<div className="flex flex-row flex-wrap gap-2">
<div>Pass extra arguments to 'docker build'.</div>
<Link
to="https://docs.docker.com/reference/cli/docker/buildx/build/"
target="_blank"
className="text-primary"
>
See docker docs.
</Link>
</div>
}
>
{!disabled && (
<AddExtraArgMenu
type="Build"
onSelect={(suggestion) =>
set({
extra_args: [
...(update.extra_args ?? config.extra_args ?? []),
suggestion,
],
})
}
disabled={disabled}
/>
)}
<InputList
field="extra_args"
values={value ?? []}
set={set}
disabled={disabled}
placeholder="--extra-arg=value"
/>
</ConfigItem>
),
},
},
{
label: "Labels",
description: "Attach --labels to image.",
components: {
labels: (labels, set) => (
<MonacoEditor
value={labels || " # your.docker.label: value\n"}
language="key_value"
onValueChange={(labels) => set({ labels })}
readOnly={disabled}
/>
),
},
},
{
label: "Webhook",
description: `Configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
components: {
["Guard" as any]: () => {
if (update.branch ?? config.branch) {
return null;
}
return (
<ConfigItem label="Configure Branch">
<div>Must configure Branch before webhooks will work.</div>
</ConfigItem>
);
},
["Builder" as any]: () => (
<WebhookBuilder git_provider={git_provider} />
),
["build" as any]: () => (
<ConfigItem label="Webhook Url - Build">
<CopyWebhook
integration={webhook_integration}
path={`/build/${id_or_name === "Id" ? id : name}`}
/>
</ConfigItem>
),
webhook_enabled: webhook !== undefined && !webhook.managed,
webhook_secret: {
description:
"Provide a custom webhook secret for this resource, or use the global default.",
placeholder: "Input custom secret",
},
["managed" as any]: () => {
const inv = useInvalidate();
const { toast } = useToast();
const { mutate: createWebhook, isPending: createPending } =
useWrite("CreateBuildWebhook", {
onSuccess: () => {
toast({ title: "Webhook Created" });
inv(["GetBuildWebhookEnabled", { build: id }]);
},
});
const { mutate: deleteWebhook, isPending: deletePending } =
useWrite("DeleteBuildWebhook", {
onSuccess: () => {
toast({ title: "Webhook Deleted" });
inv(["GetBuildWebhookEnabled", { build: id }]);
},
});
if (!webhook || !webhook.managed) return;
return (
<ConfigItem label="Manage Webhook">
{webhook.enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention("Good")}
>
ENABLED
</div>
</div>
<ConfirmButton
title="Disable"
icon={<Ban className="w-4 h-4" />}
variant="destructive"
onClick={() => deleteWebhook({ build: id })}
loading={deletePending}
disabled={disabled || deletePending}
/>
</div>
)}
{!webhook.enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention(
"Critical"
)}
>
DISABLED
</div>
</div>
<ConfirmButton
title="Enable Build"
icon={<CirclePlus className="w-4 h-4" />}
onClick={() => createWebhook({ build: id })}
loading={createPending}
disabled={disabled || createPending}
/>
</div>
)}
</ConfigItem>
);
},
},
},
],
}}
/>
);
};