add build page

This commit is contained in:
mbecker20
2023-01-02 09:09:32 +00:00
parent bdf4e9d6c6
commit e9685ce71a
15 changed files with 942 additions and 2 deletions
@@ -0,0 +1,53 @@
import { useParams } from "@solidjs/router";
import { createContext, createEffect, onCleanup, ParentComponent, useContext } from "solid-js";
import { createStore } from "solid-js/store";
import { client } from "../..";
import { useAppState } from "../../state/StateProvider";
import { BuildActionState, Operation, UpdateStatus } from "../../types";
type State = {
} & BuildActionState;
const context = createContext<State>();
export const ActionStateProvider: ParentComponent<{}> = (p) => {
const { ws } = useAppState();
const params = useParams();
const [actions, setActions] = createStore<BuildActionState>({
building: false,
recloning: false,
updating: false,
});
createEffect(() => {
client.get_build_action_state(params.id).then(setActions);
});
onCleanup(
ws.subscribe([Operation.BuildBuild], (update) => {
if (update.target.id === params.id) {
setActions("building", update.status !== UpdateStatus.Complete);
}
})
);
onCleanup(
ws.subscribe([Operation.RecloneBuild], (update) => {
if (update.target.id === params.id) {
setActions("recloning", update.status !== UpdateStatus.Complete);
}
})
);
// onCleanup(
// ws.subscribe([DELETE_BUILD], ({ complete, buildID }) => {
// if (buildID === selected.id()) {
// setActions("deleting", !complete);
// }
// })
// );
return (
<context.Provider value={actions}>{p.children}</context.Provider>
);
}
export function useActionStates() {
return useContext(context) as State;
}
+74
View File
@@ -0,0 +1,74 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import ConfirmButton from "../shared/ConfirmButton";
import Icon from "../shared/Icon";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import Loading from "../shared/loading/Loading";
import { useActionStates } from "./ActionStateProvider";
import { client } from "../..";
import { combineClasses, getId } from "../../util/helpers";
import { useParams } from "@solidjs/router";
import { PermissionLevel } from "../../types";
const Actions: Component<{}> = (p) => {
const { user } = useUser();
const params = useParams() as { id: string };
const { builds, ws } = useAppState();
const build = () => builds.get(params.id)!;
const actions = useActionStates();
const userCanExecute = () =>
user().admin ||
build().permissions[getId(user())] === PermissionLevel.Execute ||
build().permissions[getId(user())] === PermissionLevel.Execute;
return (
<Show when={userCanExecute()}>
<Grid class={combineClasses("card shadow")}>
<h1>actions</h1>
<Flex class={combineClasses("action shadow")}>
build{" "}
<Show
when={!actions.building}
fallback={
<button class="green">
<Loading type="spinner" />
</button>
}
>
<ConfirmButton
color="green"
onConfirm={() => {
client.build(params.id);
}}
>
<Icon type="build" />
</ConfirmButton>
</Show>
</Flex>
<Flex class={combineClasses("action shadow")}>
reclone{" "}
<Show
when={!actions.recloning}
fallback={
<button class="orange">
<Loading type="spinner" />
</button>
}
>
<ConfirmButton
color="orange"
onConfirm={() => {
client.reclone_build(params.id)
}}
>
<Icon type="reset" />
</ConfirmButton>
</Show>
</Flex>
</Grid>
</Show>
);
};
export default Actions;
+40 -2
View File
@@ -1,7 +1,45 @@
import { Component } from "solid-js";
import { useParams } from "@solidjs/router";
import { Component, Show } from "solid-js";
import { useAppDimensions } from "../../state/DimensionProvider";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import { PermissionLevel } from "../../types";
import { combineClasses, getId } from "../../util/helpers";
import NotFound from "../NotFound";
import Grid from "../shared/layout/Grid";
import Actions from "./Actions";
import { ActionStateProvider } from "./ActionStateProvider";
import Header from "./Header";
import BuildTabs from "./tabs/Tabs";
import Updates from "./Updates";
const Build: Component<{}> = (p) => {
return <div></div>;
const { builds } = useAppState();
const params = useParams();
const build = () => builds.get(params.id)!;
const { isSemiMobile } = useAppDimensions();
const { user } = useUser();
const userCanUpdate = () =>
user().admin ||
build().permissions[getId(user())] === PermissionLevel.Update;
return (
<Show when={build()} fallback={<NotFound type="build" />}>
<ActionStateProvider>
<Grid class={combineClasses("content")}>
{/* left / actions */}
<Grid class="left-content">
<Header />
<Actions />
<Show when={!isSemiMobile() && userCanUpdate()}>
<Updates />
</Show>
</Grid>
{/* right / tabs */}
<BuildTabs />
</Grid>
</ActionStateProvider>
</Show>
);
};
export default Build;
+74
View File
@@ -0,0 +1,74 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../state/StateProvider";
import { useUser } from "../../state/UserProvider";
import ConfirmButton from "../shared/ConfirmButton";
import Icon from "../shared/Icon";
import Flex from "../shared/layout/Flex";
import Grid from "../shared/layout/Grid";
import { useActionStates } from "./ActionStateProvider";
import { combineClasses, getId } from "../../util/helpers";
import { useAppDimensions } from "../../state/DimensionProvider";
import Updates from "./Updates";
import { useLocalStorageToggle } from "../../util/hooks";
import { useParams } from "@solidjs/router";
import { PermissionLevel } from "../../types";
import { client } from "../..";
const Header: Component<{}> = (p) => {
const { builds, ws } = useAppState();
const params = useParams();
const build = () => builds.get(params.id)!;
const actions = useActionStates();
const { user } = useUser();
const { isMobile } = useAppDimensions();
const [showUpdates, toggleShowUpdates] =
useLocalStorageToggle("show-updates");
const userCanUpdate = () =>
user().admin ||
build().permissions[getId(user())] === PermissionLevel.Update;
return (
<>
<Flex
class={combineClasses("card shadow")}
justifyContent="space-between"
alignItems="center"
style={{
position: "relative",
cursor: isMobile() && userCanUpdate() ? "pointer" : undefined,
}}
onClick={() => {
if (isMobile() && userCanUpdate()) toggleShowUpdates();
}}
>
<Grid gap="0.1rem">
<h1>{build().name}</h1>
<div style={{ opacity: 0.8 }}>build</div>
</Grid>
<Show when={userCanUpdate()}>
<ConfirmButton
onConfirm={() => {
client.delete_build(params.id);
}}
color="red"
>
<Icon type="trash" />
</ConfirmButton>
</Show>
<Show when={isMobile() && userCanUpdate()}>
<Flex gap="0.5rem" alignItems="center" class="show-updates-indicator">
updates{" "}
<Icon
type={showUpdates() ? "chevron-up" : "chevron-down"}
width="0.9rem"
/>
</Flex>
</Show>
</Flex>
<Show when={isMobile() && userCanUpdate() && showUpdates()}>
<Updates />
</Show>
</>
);
};
export default Header;
+60
View File
@@ -0,0 +1,60 @@
import { Component, createSignal, For, onCleanup, Show } from "solid-js";
import { useArray } from "../../state/hooks";
import { useAppState } from "../../state/StateProvider";
import Update from "../update/Update";
import Grid from "../shared/layout/Grid";
import { combineClasses } from "../../util/helpers";
import { client } from "../..";
import { useParams } from "@solidjs/router";
const Updates: Component<{}> = (p) => {
const { ws } = useAppState();
const params = useParams();
const selectedUpdates = useArray(() =>
client.list_updates(0, { type: "Build", id: params.id })
);
const unsub = ws.subscribe([], (update) => {
if (update.target.id === params.id) {
selectedUpdates.add(update);
}
});
onCleanup(unsub);
const [noMoreUpdates, setNoMore] = createSignal(false);
const loadMore = async () => {
const offset = selectedUpdates.collection()?.length;
if (offset) {
const updates = await client.list_updates(offset, {
type: "Build",
id: params.id,
});
selectedUpdates.addManyToEnd(updates);
if (updates.length !== 10) {
setNoMore(true);
}
}
};
return (
<Show
when={
selectedUpdates.loaded() &&
(selectedUpdates.collection()?.length || 0) > 0
}
>
<Grid class={combineClasses("card shadow")}>
<h1>updates</h1>
<Grid class="updates-container scroller">
<For each={selectedUpdates.collection()}>
{(update) => <Update update={update} />}
</For>
<Show when={!noMoreUpdates()}>
<button class="grey" style={{ width: "100%" }} onClick={loadMore}>
load more
</button>
</Show>
</Grid>
</Grid>
</Show>
);
};
export default Updates;
@@ -0,0 +1,111 @@
import { Component, createEffect, createSignal, For, Show } from "solid-js";
import { pushNotification } from "../../..";
import { useUser } from "../../../state/UserProvider";
import { User } from "../../../types";
import { combineClasses } from "../../../util/helpers";
import ConfirmButton from "../../shared/ConfirmButton";
import Input from "../../shared/Input";
import Flex from "../../shared/layout/Flex";
import Grid from "../../shared/layout/Grid";
import Menu from "../../shared/menu/Menu";
import { useConfig } from "./Provider";
const Owners: Component<{}> = (p) => {
// const { build } = useConfig();
// const { user } = useUser();
// const [userSearch, setUserSearch] = createSignal("");
// const [users, setUsers] = createSignal<User[]>([]);
// createEffect(() => {
// if (userSearch().length > 0) {
// getUsers(userSearch(), true).then((users) => {
// setUsers(users.filter((user) => !build.owners.includes(user.username)));
// });
// } else {
// setUsers([]);
// }
// });
// return (
// <Show when={build.loaded}>
// <Grid class="config">
// <Grid class="config-items scroller" style={{ height: "100%" }}>
// <Grid
// class={combineClasses("config-item shadow", themeClass())}
// gap="0.5rem"
// >
// <Menu
// show={userSearch() ? true : false}
// close={() => setUserSearch("")}
// target={
// <Input
// placeholder="add user"
// value={userSearch()}
// onEdit={setUserSearch}
// style={{ width: "12rem" }}
// />
// }
// content={
// <>
// <For each={users()}>
// {(user) => (
// <ConfirmButton
// color="grey"
// style={{
// width: "100%",
// "justify-content": "flex-start",
// }}
// onConfirm={async () => {
// await addOwnerToBuild(build._id!, user.username);
// pushNotification("good", "owner added to build");
// setUserSearch("");
// }}
// confirm="add user"
// >
// {user.username}
// </ConfirmButton>
// )}
// </For>
// <Show when={users().length === 0}>no matching users</Show>
// </>
// }
// menuStyle={{ width: "12rem" }}
// />
// <For each={build.owners}>
// {(owner) => (
// <Flex
// alignItems="center"
// justifyContent="space-between"
// class={combineClasses("grey-no-hover", themeClass())}
// style={{
// padding: "0.5rem",
// }}
// >
// <div class="big-text">
// {owner}
// {owner === username() && " ( you )"}
// </div>
// <Show when={permissions() > 1}>
// <ConfirmButton
// color="red"
// onConfirm={async () => {
// await removeOwnerFromBuild(build._id!, owner);
// pushNotification(
// "good",
// "user removed from collaborators"
// );
// }}
// >
// remove
// </ConfirmButton>
// </Show>
// </Flex>
// )}
// </For>
// </Grid>
// </Grid>
// </Grid>
// </Show>
// );
return <div></div>
};
export default Owners;
@@ -0,0 +1,107 @@
import { useParams } from "@solidjs/router";
import {
createContext,
createEffect,
onCleanup,
ParentComponent,
useContext,
} from "solid-js";
import { createStore, SetStoreFunction } from "solid-js/store";
import { client } from "../../..";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import { Build, Operation, PermissionLevel } from "../../../types";
import { getId } from "../../../util/helpers";
type ConfigBuild = Build & {
loaded: boolean;
updated: boolean;
saving: boolean;
};
type State = {
build: ConfigBuild;
setBuild: SetStoreFunction<ConfigBuild>;
reset: () => void;
save: () => void;
userCanUpdate: () => boolean;
};
const context = createContext<State>();
export const ConfigProvider: ParentComponent<{}> = (p) => {
const { ws, builds } = useAppState();
const params = useParams();
const { user } = useUser();
const [build, set] = createStore({
...builds.get(params.id)!,
loaded: false,
updated: false,
saving: false,
});
const setBuild: SetStoreFunction<ConfigBuild> = (...args: any) => {
// @ts-ignore
set(...args);
set("updated", true);
};
const load = () => {
// console.log("load build");
client.get_build(params.id).then((build) => {
set({
...build,
repo: build.repo,
branch: build.branch,
on_clone: build.on_clone,
pre_build: build.pre_build,
docker_build_args: build.docker_build_args,
docker_account: build.docker_account,
github_account: build.github_account,
loaded: true,
updated: false,
saving: false,
});
});
};
createEffect(load);
const save = () => {
setBuild("saving", true);
client.update_build(build)
};
onCleanup(
ws.subscribe([Operation.UpdateBuild], (update) => {
if (update.target.id === params.id) {
load();
}
})
);
onCleanup(
ws.subscribe(
[Operation.ModifyUserPermissions],
async (update) => {
if (update.target.id === params.id) {
const build = await client.get_build(params.id);
set("permissions", build.permissions);
}
}
)
);
const userCanUpdate = () => user().admin || build.permissions[getId(user())] === PermissionLevel.Update;
const state = {
build,
setBuild,
reset: load,
save,
userCanUpdate,
};
return <context.Provider value={state}>{p.children}</context.Provider>;
};
export function useConfig() {
return useContext(context) as State;
}
@@ -0,0 +1,49 @@
import { useParams } from "@solidjs/router";
import { Component, Show } from "solid-js";
import { useAppState } from "../../../state/StateProvider";
import { useUser } from "../../../state/UserProvider";
import { PermissionLevel } from "../../../types";
import { getId } from "../../../util/helpers";
import Tabs, { Tab } from "../../shared/tabs/Tabs";
import BuildConfig from "./build-config/BuildConfig";
import GitConfig from "./git-config/GitConfig";
import Owners from "./Owners";
import { ConfigProvider } from "./Provider";
const BuildTabs: Component<{}> = (p) => {
const { builds } = useAppState();
const params = useParams();
const { user } = useUser();
const build = () => builds.get(params.id)!;
const userCanUpdate = () =>
user().admin ||
build().permissions[getId(user())] === PermissionLevel.Update;
return (
<Show when={build()}>
<ConfigProvider>
<Tabs
containerClass="card shadow"
tabs={
[
{
title: "repo",
element: () => <GitConfig />,
},
{
title: "build",
element: () => <BuildConfig />,
},
userCanUpdate() && {
title: "collaborators",
element: () => <Owners />,
},
].filter((e) => e) as Tab[]
}
localStorageKey="build-tab"
/>
</ConfigProvider>
</Show>
);
};
export default BuildTabs;
@@ -0,0 +1,46 @@
import { Component, Show } from "solid-js";
import ConfirmButton from "../../../shared/ConfirmButton";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import CliBuild from "./CliBuild";
import Docker from "./Docker";
import { useConfig } from "../Provider";
import Loading from "../../../shared/loading/Loading";
const BuildConfig: Component<{}> = (p) => {
const { build, reset, save, userCanUpdate } = useConfig();
return (
<Show when={build.loaded}>
<Grid class="config">
<Grid class="config-items scroller">
<Docker />
<CliBuild />
</Grid>
<Show when={userCanUpdate() && build.updated}>
<Show
when={!build.saving}
fallback={
<button class="green">
updating <Loading type="spinner" />
</button>
}
>
<Flex style={{ "place-self": "center", padding: "1rem" }}>
<button onClick={reset}>
reset
<Icon type="reset" />
</button>
<ConfirmButton onConfirm={save} color="green">
save
<Icon type="floppy-disk" />
</ConfirmButton>
</Flex>
</Show>
</Show>
</Grid>
</Show>
);
};
export default BuildConfig;
@@ -0,0 +1,44 @@
import { Component } from "solid-js";
import { combineClasses } from "../../../../util/helpers";
import Input from "../../../shared/Input";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
const CliBuild: Component<{}> = (p) => {
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>pre build</h1>
{/* <div style={{ opacity: 0.7 }}>build with a custom command</div> */}
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>build path: </h2>
<Input
placeholder="from root of repo"
value={build.pre_build?.path || (userCanUpdate() ? "" : "/")}
onEdit={(path) => setBuild("pre_build", { path })}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>command: </h2>
<Input
placeholder="ie. yarn build"
value={build.pre_build?.command || ""}
onEdit={(command) => setBuild("pre_build", { command })}
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>
);
};
export default CliBuild;
@@ -0,0 +1,77 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import { client } from "../../../..";
import { useAppState } from "../../../../state/StateProvider";
import { combineClasses } from "../../../../util/helpers";
import Input from "../../../shared/Input";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Selector from "../../../shared/menu/Selector";
import { useConfig } from "../Provider";
const Docker: Component<{}> = (p) => {
const { build, setBuild, userCanUpdate } = useConfig();
const [dockerAccounts, setDockerAccounts] = createSignal<string[]>();
createEffect(() => {
client.get_server_docker_accounts(build.server_id).then(setDockerAccounts);
});
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>docker build</h1> {/* checkbox here? */}
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>build path: </h2>
<Input
placeholder="from root of repo"
value={build.docker_build_args?.build_path || ""}
onEdit={(build_path) => setBuild("docker_build_args", { build_path })}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>dockerfile path: </h2>
<Input
placeholder="from root of build path"
value={
build.docker_build_args?.dockerfile_path ||
(userCanUpdate() ? "" : "Dockerfile")
}
onEdit={(dockerfile_path) =>
setBuild("docker_build_args", { dockerfile_path })
}
disabled={!userCanUpdate()}
/>
</Flex>
<Show when={dockerAccounts() && dockerAccounts()!.length > 0}>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>dockerhub account: </h2>
<Selector
targetClass="blue"
selected={build.docker_account || "none"}
items={["none", ...dockerAccounts()!]}
onSelect={(account) => {
setBuild(
"docker_account",
account === "none" ? undefined : account
);
}}
position="bottom right"
disabled={!userCanUpdate()}
/>
</Flex>
</Show>
</Grid>
);
};
export default Docker;
@@ -0,0 +1,71 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../shared/layout/Flex";
import Input from "../../../shared/Input";
import Selector from "../../../shared/menu/Selector";
import { combineClasses } from "../../../../util/helpers";
import { client } from "../../../..";
const Git: Component<{}> = (p) => {
const { build, setBuild, userCanUpdate } = useConfig();
const [githubAccounts, setGithubAccounts] = createSignal<string[]>();
createEffect(() => {
client.get_server_github_accounts(build.server_id).then(setGithubAccounts)
});
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>github config</h1>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>repo: </h2>
<Input
placeholder="ie. solidjs/solid"
value={build.repo || ""}
onEdit={(value) => setBuild("repo", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>branch: </h2>
<Input
placeholder="defaults to main"
value={build.branch || (userCanUpdate() ? "" : "main")}
onEdit={(value) => setBuild("branch", value)}
disabled={!userCanUpdate()}
/>
</Flex>
<Show when={githubAccounts() && githubAccounts()!.length > 0}>
<Flex
justifyContent={userCanUpdate() ? "space-between" : undefined}
alignItems="center"
style={{ "flex-wrap": "wrap" }}
>
<h2>github account: </h2>
<Selector
targetClass="blue"
selected={build.github_account || "none"}
items={["none", ...githubAccounts()!]}
onSelect={(account) => {
setBuild(
"github_account",
account === "none" ? undefined : account
);
}}
position="bottom right"
disabled={!userCanUpdate()}
/>
</Flex>
</Show>
</Grid>
);
};
export default Git;
@@ -0,0 +1,69 @@
import { Component, Show } from "solid-js";
import { pushNotification, URL } from "../../../..";
import { combineClasses, copyToClipboard } from "../../../../util/helpers";
import ConfirmButton from "../../../shared/ConfirmButton";
import Icon from "../../../shared/Icon";
import Flex from "../../../shared/layout/Flex";
import Grid from "../../../shared/layout/Grid";
import Loading from "../../../shared/loading/Loading";
import { useConfig } from "../Provider";
import Git from "./Git";
import OnClone from "./OnClone";
const GitConfig: Component<{}> = (p) => {
const { build, reset, save, userCanUpdate } = useConfig();
const listenerUrl = () => `${URL}/api/listener/build/${build._id}`;
return (
<Show when={build.loaded}>
<Grid class="config">
<Grid class="config-items scroller">
<Git />
<OnClone />
<Show when={userCanUpdate()}>
<Grid class={combineClasses("config-item shadow")}>
<h1>webhook url</h1>
<Flex justifyContent="space-between" alignItems="center">
<div class="ellipsis" style={{ width: "250px" }}>
{listenerUrl()}
</div>
<ConfirmButton
color="blue"
onFirstClick={() => {
copyToClipboard(listenerUrl());
pushNotification("good", "copied url to clipboard");
}}
confirm={<Icon type="check" />}
>
<Icon type="clipboard" />
</ConfirmButton>
</Flex>
</Grid>
</Show>
</Grid>
<Show when={userCanUpdate() && build.updated}>
<Show
when={!build.saving}
fallback={
<button class="green">
updating <Loading type="spinner" />
</button>
}
>
<Flex style={{ "place-self": "center", padding: "1rem" }}>
<button onClick={reset}>
reset
<Icon type="reset" />
</button>
<ConfirmButton onConfirm={save} color="green">
save
<Icon type="floppy-disk" />
</ConfirmButton>
</Flex>
</Show>
</Show>
</Grid>
</Show>
);
};
export default GitConfig;
@@ -0,0 +1,63 @@
import { Component } from "solid-js";
import { combineClasses } from "../../../../util/helpers";
import Input from "../../../shared/Input";
import Grid from "../../../shared/layout/Grid";
import { useConfig } from "../Provider";
import Flex from "../../../shared/layout/Flex";
const OnClone: Component = () => {
const { build, setBuild, userCanUpdate } = useConfig();
return (
<Grid class={combineClasses("config-item shadow")}>
<h1>on clone</h1>
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
style={{ "flex-wrap": "wrap" }}
>
<h2>path:</h2>
<Input
placeholder="relative to repo"
value={build.on_clone?.path || ""}
onEdit={(path) => {
if (
path.length === 0 &&
(!build.on_clone ||
!build.on_clone.command ||
build.on_clone.command.length === 0)
) {
setBuild("on_clone", undefined);
}
setBuild("on_clone", { path });
}}
disabled={!userCanUpdate()}
/>
</Flex>
<Flex
alignItems="center"
justifyContent={userCanUpdate() ? "space-between" : undefined}
style={{ "flex-wrap": "wrap" }}
>
<h2>command:</h2>
<Input
placeholder="command"
value={build.on_clone?.command || ""}
onEdit={(command) => {
if (
command.length === 0 &&
(!build.on_clone ||
!build.on_clone.path ||
build.on_clone.path.length === 0)
) {
setBuild("on_clone", undefined);
}
setBuild("on_clone", { command });
}}
disabled={!userCanUpdate()}
/>
</Flex>
</Grid>
);
};
export default OnClone;
+4
View File
@@ -109,4 +109,8 @@ export function serverStatusClass(
} else if (status === ServerStatus.NotOk) {
return "exited"
}
}
export function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}