From e9685ce71a401decc98bf0b032f35185be4672a4 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 2 Jan 2023 09:09:32 +0000 Subject: [PATCH] add build page --- .../components/build/ActionStateProvider.tsx | 53 +++++++++ frontend/src/components/build/Actions.tsx | 74 ++++++++++++ frontend/src/components/build/Build.tsx | 42 ++++++- frontend/src/components/build/Header.tsx | 74 ++++++++++++ frontend/src/components/build/Updates.tsx | 60 ++++++++++ frontend/src/components/build/tabs/Owners.tsx | 111 ++++++++++++++++++ .../src/components/build/tabs/Provider.tsx | 107 +++++++++++++++++ frontend/src/components/build/tabs/Tabs.tsx | 49 ++++++++ .../build/tabs/build-config/BuildConfig.tsx | 46 ++++++++ .../build/tabs/build-config/CliBuild.tsx | 44 +++++++ .../build/tabs/build-config/Docker.tsx | 77 ++++++++++++ .../components/build/tabs/git-config/Git.tsx | 71 +++++++++++ .../build/tabs/git-config/GitConfig.tsx | 69 +++++++++++ .../build/tabs/git-config/OnClone.tsx | 63 ++++++++++ frontend/src/util/helpers.ts | 4 + 15 files changed, 942 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/build/ActionStateProvider.tsx create mode 100644 frontend/src/components/build/Actions.tsx create mode 100644 frontend/src/components/build/Header.tsx create mode 100644 frontend/src/components/build/Updates.tsx create mode 100644 frontend/src/components/build/tabs/Owners.tsx create mode 100644 frontend/src/components/build/tabs/Provider.tsx create mode 100644 frontend/src/components/build/tabs/Tabs.tsx create mode 100644 frontend/src/components/build/tabs/build-config/BuildConfig.tsx create mode 100644 frontend/src/components/build/tabs/build-config/CliBuild.tsx create mode 100644 frontend/src/components/build/tabs/build-config/Docker.tsx create mode 100644 frontend/src/components/build/tabs/git-config/Git.tsx create mode 100644 frontend/src/components/build/tabs/git-config/GitConfig.tsx create mode 100644 frontend/src/components/build/tabs/git-config/OnClone.tsx diff --git a/frontend/src/components/build/ActionStateProvider.tsx b/frontend/src/components/build/ActionStateProvider.tsx new file mode 100644 index 000000000..84f970d5a --- /dev/null +++ b/frontend/src/components/build/ActionStateProvider.tsx @@ -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(); + +export const ActionStateProvider: ParentComponent<{}> = (p) => { + const { ws } = useAppState(); + const params = useParams(); + const [actions, setActions] = createStore({ + 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 ( + {p.children} + ); +} + +export function useActionStates() { + return useContext(context) as State; +} \ No newline at end of file diff --git a/frontend/src/components/build/Actions.tsx b/frontend/src/components/build/Actions.tsx new file mode 100644 index 000000000..a28f6851b --- /dev/null +++ b/frontend/src/components/build/Actions.tsx @@ -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 ( + + +

actions

+ + build{" "} + + + + } + > + { + client.build(params.id); + }} + > + + + + + + reclone{" "} + + + + } + > + { + client.reclone_build(params.id) + }} + > + + + + +
+
+ ); +}; + +export default Actions; diff --git a/frontend/src/components/build/Build.tsx b/frontend/src/components/build/Build.tsx index edef235bc..0bdd7127c 100644 --- a/frontend/src/components/build/Build.tsx +++ b/frontend/src/components/build/Build.tsx @@ -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
; + 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 ( + }> + + + {/* left / actions */} + +
+ + + + + + {/* right / tabs */} + + + + + ); }; export default Build; diff --git a/frontend/src/components/build/Header.tsx b/frontend/src/components/build/Header.tsx new file mode 100644 index 000000000..3fc2d6da9 --- /dev/null +++ b/frontend/src/components/build/Header.tsx @@ -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 ( + <> + { + if (isMobile() && userCanUpdate()) toggleShowUpdates(); + }} + > + +

{build().name}

+
build
+
+ + { + client.delete_build(params.id); + }} + color="red" + > + + + + + + updates{" "} + + + +
+ + + + + ); +}; + +export default Header; diff --git a/frontend/src/components/build/Updates.tsx b/frontend/src/components/build/Updates.tsx new file mode 100644 index 000000000..b246c3032 --- /dev/null +++ b/frontend/src/components/build/Updates.tsx @@ -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 ( + 0 + } + > + +

updates

+ + + {(update) => } + + + + + +
+
+ ); +}; + +export default Updates; diff --git a/frontend/src/components/build/tabs/Owners.tsx b/frontend/src/components/build/tabs/Owners.tsx new file mode 100644 index 000000000..7e8d538c1 --- /dev/null +++ b/frontend/src/components/build/tabs/Owners.tsx @@ -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([]); + // createEffect(() => { + // if (userSearch().length > 0) { + // getUsers(userSearch(), true).then((users) => { + // setUsers(users.filter((user) => !build.owners.includes(user.username))); + // }); + // } else { + // setUsers([]); + // } + // }); + // return ( + // + // + // + // + // setUserSearch("")} + // target={ + // + // } + // content={ + // <> + // + // {(user) => ( + // { + // await addOwnerToBuild(build._id!, user.username); + // pushNotification("good", "owner added to build"); + // setUserSearch(""); + // }} + // confirm="add user" + // > + // {user.username} + // + // )} + // + // no matching users + // + // } + // menuStyle={{ width: "12rem" }} + // /> + // + // {(owner) => ( + // + //
+ // {owner} + // {owner === username() && " ( you )"} + //
+ // 1}> + // { + // await removeOwnerFromBuild(build._id!, owner); + // pushNotification( + // "good", + // "user removed from collaborators" + // ); + // }} + // > + // remove + // + // + //
+ // )} + //
+ // + // + // + // + // ); + return
+}; + +export default Owners; diff --git a/frontend/src/components/build/tabs/Provider.tsx b/frontend/src/components/build/tabs/Provider.tsx new file mode 100644 index 000000000..d433e6076 --- /dev/null +++ b/frontend/src/components/build/tabs/Provider.tsx @@ -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; + reset: () => void; + save: () => void; + userCanUpdate: () => boolean; +}; + +const context = createContext(); + +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 = (...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 {p.children}; +}; + +export function useConfig() { + return useContext(context) as State; +} diff --git a/frontend/src/components/build/tabs/Tabs.tsx b/frontend/src/components/build/tabs/Tabs.tsx new file mode 100644 index 000000000..cd41ca478 --- /dev/null +++ b/frontend/src/components/build/tabs/Tabs.tsx @@ -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 ( + + + , + }, + { + title: "build", + element: () => , + }, + userCanUpdate() && { + title: "collaborators", + element: () => , + }, + ].filter((e) => e) as Tab[] + } + localStorageKey="build-tab" + /> + + + ); +}; + +export default BuildTabs; diff --git a/frontend/src/components/build/tabs/build-config/BuildConfig.tsx b/frontend/src/components/build/tabs/build-config/BuildConfig.tsx new file mode 100644 index 000000000..0669a0486 --- /dev/null +++ b/frontend/src/components/build/tabs/build-config/BuildConfig.tsx @@ -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 ( + + + + + + + + + updating + + } + > + + + + save + + + + + + + + ); +}; + +export default BuildConfig; diff --git a/frontend/src/components/build/tabs/build-config/CliBuild.tsx b/frontend/src/components/build/tabs/build-config/CliBuild.tsx new file mode 100644 index 000000000..492ced524 --- /dev/null +++ b/frontend/src/components/build/tabs/build-config/CliBuild.tsx @@ -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 ( + +

pre build

+ {/*
build with a custom command
*/} + +

build path:

+ setBuild("pre_build", { path })} + disabled={!userCanUpdate()} + /> +
+ +

command:

+ setBuild("pre_build", { command })} + disabled={!userCanUpdate()} + /> +
+
+ ); +}; + +export default CliBuild; diff --git a/frontend/src/components/build/tabs/build-config/Docker.tsx b/frontend/src/components/build/tabs/build-config/Docker.tsx new file mode 100644 index 000000000..e2705869b --- /dev/null +++ b/frontend/src/components/build/tabs/build-config/Docker.tsx @@ -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(); + createEffect(() => { + client.get_server_docker_accounts(build.server_id).then(setDockerAccounts); + }); + return ( + +

docker build

{/* checkbox here? */} + +

build path:

+ setBuild("docker_build_args", { build_path })} + disabled={!userCanUpdate()} + /> +
+ +

dockerfile path:

+ + setBuild("docker_build_args", { dockerfile_path }) + } + disabled={!userCanUpdate()} + /> +
+ 0}> + +

dockerhub account:

+ { + setBuild( + "docker_account", + account === "none" ? undefined : account + ); + }} + position="bottom right" + disabled={!userCanUpdate()} + /> +
+
+
+ ); +}; + +export default Docker; diff --git a/frontend/src/components/build/tabs/git-config/Git.tsx b/frontend/src/components/build/tabs/git-config/Git.tsx new file mode 100644 index 000000000..bb4b546b4 --- /dev/null +++ b/frontend/src/components/build/tabs/git-config/Git.tsx @@ -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(); + createEffect(() => { + client.get_server_github_accounts(build.server_id).then(setGithubAccounts) + }); + return ( + +

github config

+ +

repo:

+ setBuild("repo", value)} + disabled={!userCanUpdate()} + /> +
+ +

branch:

+ setBuild("branch", value)} + disabled={!userCanUpdate()} + /> +
+ 0}> + +

github account:

+ { + setBuild( + "github_account", + account === "none" ? undefined : account + ); + }} + position="bottom right" + disabled={!userCanUpdate()} + /> +
+
+
+ ); +}; + +export default Git; diff --git a/frontend/src/components/build/tabs/git-config/GitConfig.tsx b/frontend/src/components/build/tabs/git-config/GitConfig.tsx new file mode 100644 index 000000000..eb59c4f31 --- /dev/null +++ b/frontend/src/components/build/tabs/git-config/GitConfig.tsx @@ -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 ( + + + + + + + +

webhook url

+ +
+ {listenerUrl()} +
+ { + copyToClipboard(listenerUrl()); + pushNotification("good", "copied url to clipboard"); + }} + confirm={} + > + + +
+
+
+
+ + + updating + + } + > + + + + save + + + + + +
+
+ ); +}; + +export default GitConfig; diff --git a/frontend/src/components/build/tabs/git-config/OnClone.tsx b/frontend/src/components/build/tabs/git-config/OnClone.tsx new file mode 100644 index 000000000..4cab7401e --- /dev/null +++ b/frontend/src/components/build/tabs/git-config/OnClone.tsx @@ -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 ( + +

on clone

+ +

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()} + /> +
+ +

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()} + /> +
+
+ ); +}; + +export default OnClone; diff --git a/frontend/src/util/helpers.ts b/frontend/src/util/helpers.ts index 4d90d1a11..c4cf427e1 100644 --- a/frontend/src/util/helpers.ts +++ b/frontend/src/util/helpers.ts @@ -109,4 +109,8 @@ export function serverStatusClass( } else if (status === ServerStatus.NotOk) { return "exited" } +} + +export function copyToClipboard(text: string) { + navigator.clipboard.writeText(text); } \ No newline at end of file