init from gitlab

This commit is contained in:
texm
2023-04-25 14:33:14 +08:00
parent 6a85a41ff0
commit c8202a5c82
281 changed files with 19861 additions and 1 deletions

View File

@@ -0,0 +1,61 @@
<script>
import { page } from "$app/stores";
import { getAppOverview } from "$lib/api";
import { useQuery, useQueryClient } from "@sveltestack/svelte-query";
import ContentPage from "$common/ContentPage.svelte";
import Sidebar from "$common/Sidebar.svelte";
import AppHeader from "./AppHeader.svelte";
const appName = $page.params.name;
let onSetupPage;
let currentPage;
$: if ($page.url) {
onSetupPage = $page.url.pathname.startsWith(`/app/${appName}/setup`);
currentPage = $page.url.pathname.substring(`/app/${appName}`.length + 1);
}
const pages = [
{ name: "Overview", path: "" },
{ name: "Builds", path: "builds" },
{ name: "Domains", path: "domains" },
{ name: "Environment", path: "environment" },
{ name: "Logs", path: "logs" },
{ name: "Network", path: "network" },
{ name: "Services", path: "services" },
// {"name": "Terminal", "path": "terminal"},
{ name: "Storage", path: "storage" },
{ name: "Settings", path: "settings" },
];
const queryClient = useQueryClient();
const appOverviewQueryKey = [{ appName }, "getAppOverview"];
const appOverview = useQuery(appOverviewQueryKey, () =>
getAppOverview(appName)
);
const statusChanged = () =>
queryClient.invalidateQueries(appOverviewQueryKey);
</script>
<ContentPage>
<div slot="sidebar" class:hidden={onSetupPage}>
<Sidebar {pages} prefix={"/app/" + $page.params.name} />
</div>
<div slot="header" class:hidden={onSetupPage} class="mb-2">
{#if $appOverview.isSuccess}
<AppHeader
on:statusChanged={statusChanged}
isDeployed={$appOverview.data["is_deployed"]}
isRunning={$appOverview.data["is_running"]}
isSetup={$appOverview.data["is_setup"]}
setupMethod={$appOverview.data["setup_method"]}
/>
{/if}
</div>
<div slot="content">
<slot />
</div>
</ContentPage>

View File

@@ -0,0 +1,74 @@
<script>
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { getAppOverview } from "$lib/api";
import { useQuery } from "@sveltestack/svelte-query";
import Icon from "$common/Icon.svelte";
import QueryDataWrapper from "$common/QueryDataWrapper.svelte";
import Code from "$common/Code.svelte";
import Cards from "$common/Cards.svelte";
import Card from "$common/Card.svelte";
import ProcessesOverview from "$components/processes/ProcessesOverview.svelte";
const appName = $page.params.name;
const appOverviewQueryKey = [{ appName }, "getAppOverview"];
const appOverview = useQuery(appOverviewQueryKey, () =>
getAppOverview(appName)
);
let gitPushLines = [];
let isSetup = false;
let setupMethod = "";
let isDeployed = false;
let isRunning = false;
let numProcesses = 0;
$: if ($appOverview.isSuccess && $appOverview.data) {
const data = $appOverview.data;
isSetup = data["is_setup"];
setupMethod = data["setup_method"];
isDeployed = data["is_deployed"];
isRunning = data["is_running"];
numProcesses = data["num_processes"];
let deployBranch = data["git_deploy_branch"];
gitPushLines = [
`git remote add dokku dokku@${$page.url.host}:${appName}`,
`git push dokku ${deployBranch}:master`,
];
}
</script>
<Cards>
<Card title={isDeployed ? "Processes" : "Almost there..."}>
<QueryDataWrapper query={appOverview} action="loading app overview">
{#if isDeployed}
<ProcessesOverview />
{:else}
<div class="hero p-6">
<div class="hero-content text-center">
<div class="max-w-md">
{#if isSetup && setupMethod === "git"}
<p class="py-2">Add the remote and push your code to deploy:</p>
<Code lines={gitPushLines} prefix=">" />
{:else}
<p class="py-6">Some additional setup required to deploy</p>
<button
class="btn btn-primary gap-2"
on:click={() => goto(`/app/${appName}/setup`)}
>
<Icon type="spanner" size="sm" />
setup app
</button>
{/if}
</div>
</div>
</div>
{/if}
</QueryDataWrapper>
</Card>
</Cards>

View File

@@ -0,0 +1,111 @@
<script>
import {
getAppDomainsReport,
restartApp,
rebuildApp,
startApp,
stopApp,
} from "$lib/api";
import { commandExecutionIds, executionIdDescriptions } from "$lib/stores";
import { page } from "$app/stores";
import {
useMutation,
useQuery,
useQueryClient,
} from "@sveltestack/svelte-query";
import { createEventDispatcher } from "svelte";
import AppHeaderIconButton from "$src/routes/app/[name]/AppHeaderIconButton.svelte";
import Icon from "$common/Icon.svelte";
export let isSetup;
export let setupMethod;
export let isRunning;
export let isDeployed;
const appName = $page.params.name;
const queryClient = useQueryClient();
const queryKey = [{ appName }, "getAppDomainsReport"];
const domainsReport = useQuery(queryKey, () => getAppDomainsReport(appName));
const dispatch = createEventDispatcher();
const onSuccess = () => {
dispatch("statusChanged");
};
const newMutation = (action, fn) => {
const mutationFn = async () => {
const id = await fn(appName);
$executionIdDescriptions[id] = `${action} ${appName}`;
return commandExecutionIds.addID(id);
};
return useMutation(mutationFn, { onSuccess });
};
const restartMutation = newMutation("Restart", restartApp);
const rebuildMutation = newMutation("Rebuild", rebuildApp);
const startMutation = newMutation("Start", startApp);
const stopMutation = newMutation("Stop", stopApp);
let loading;
$: loading =
$startMutation.isLoading ||
$restartMutation.isLoading ||
$rebuildMutation.isLoading ||
$stopMutation.isLoading;
let appDomain;
$: if ($domainsReport.data) {
appDomain = null;
let domains = $domainsReport.data["domains"];
if (domains && domains.length > 0) {
appDomain = domains[0];
}
}
</script>
<div class="w-full min-h-16 flex flex-row bg-base-200 p-4 items-center rounded-lg">
<div class="flex flex-row items-center">
{#if appDomain}
<a href="http://{appDomain}" class="link link-hover" target="_blank">
<div class="flex flex-row">
<span class="text-3xl mr-1">
{appName}
</span>
<Icon type="external-link" size="sm" />
</div>
</a>
{:else}
<span class="text-3xl align-center">{appName}</span>
{/if}
</div>
<div class="flex-grow" ></div>
<div class="flex md:flex-row flex-col gap-2">
{#if isSetup && isDeployed && isRunning}
<AppHeaderIconButton
action="rebuild"
{loading}
on:clicked={$rebuildMutation.mutate}
/>
{/if}
{#if isDeployed && !isRunning}
<AppHeaderIconButton
action="start"
{loading}
on:clicked={$startMutation.mutate}
/>
{:else if isRunning}
<AppHeaderIconButton
action="stop"
{loading}
on:clicked={$stopMutation.mutate}
/>
<AppHeaderIconButton
action="restart"
{loading}
on:clicked={$restartMutation.mutate}
/>
{/if}
</div>
</div>

View File

@@ -0,0 +1,21 @@
<script>
import Icon from "$common/Icon.svelte";
import { createEventDispatcher } from "svelte";
export let action;
export let loading;
const dispatch = createEventDispatcher();
</script>
<button
class="btn btn-outline md:btn-sm"
class:gap-2={!loading}
class:loading
on:click={() => dispatch("clicked")}
>
{#if !loading}
<Icon type={action} size="sm" />
{/if}
{action}
</button>

View File

@@ -0,0 +1,27 @@
<script>
import Cards from "$common/Cards.svelte";
import Card from "$common/Card.svelte";
import BuilderSelection from "./BuilderSelection.svelte";
import BuildDirectory from "./BuildDirectory.svelte";
import DeployChecks from "./DeployChecks.svelte";
import SetupConfig from "./SetupConfig.svelte";
</script>
<Cards>
<Card title="Config">
<div class=""><SetupConfig /></div>
</Card>
<Card title="Builder Selection">
<div class="max-w-xs"><BuilderSelection /></div>
</Card>
<Card title="Build Directory">
<div class="max-w-xs"><BuildDirectory /></div>
</Card>
<Card title="Zero-Downtime Deployment Checks">
<div class="max-w-xs"><DeployChecks /></div>
</Card>
</Cards>

View File

@@ -0,0 +1,88 @@
<script>
import {
getAppBuildDirectory,
setAppBuildDirectory,
clearAppBuildDirectory,
} from "$lib/api";
import { page } from "$app/stores";
import {
useMutation,
useQuery,
useQueryClient,
} from "@sveltestack/svelte-query";
import QueryDataWrapper from "$common/QueryDataWrapper.svelte";
import Error from "$common/Error.svelte";
const appName = $page.params.name;
const queryClient = useQueryClient();
const queryKey = [{ appName }, "getAppBuildDirectory"];
let buildDir = "";
let enableBuildDirectory = false;
const onBuildDirFetched = (data) => {
buildDir = data;
enableBuildDirectory = !!buildDir;
};
const getBuildDir = useQuery(queryKey, () => getAppBuildDirectory(appName), {
onSuccess: onBuildDirFetched,
});
const setDir = useMutation(
(newDir) => setAppBuildDirectory(appName, newDir),
{
onSuccess: (_, newDir) => queryClient.setQueryData(queryKey, newDir),
}
);
const clearDir = useMutation(() => clearAppBuildDirectory(appName), {
onSuccess: () => queryClient.setQueryData(queryKey, null),
});
const onEnableToggled = async (e) => {
enableBuildDirectory = e.target.checked;
if (!e.target.checked) $clearDir.mutate(appName);
};
</script>
<QueryDataWrapper query={getBuildDir} action="loading build directory">
<label class="label cursor-pointer">
<span class="label-text">Use custom directory</span>
<input
type="checkbox"
class="toggle"
disabled={$setDir.isLoading || $clearDir.isLoading}
checked={enableBuildDirectory}
on:change={onEnableToggled}
/>
</label>
{#if enableBuildDirectory}
<label class="input-group input-group-md">
<span class="w-auto">Directory</span>
<input
type="text"
class="input input-md input-bordered flex-grow"
disabled={$setDir.isLoading || $clearDir.isLoading}
bind:value={buildDir}
/>
</label>
<div class="mt-4">
<button
class="btn btn-primary"
class:loading={$setDir.isLoading || $clearDir.isLoading}
class:hidden={$getBuildDir.data === buildDir}
on:click={() => $setDir.mutate(buildDir)}
>
Save
</button>
</div>
{/if}
</QueryDataWrapper>
{#if $setDir.isError}
<Error action="setting build directory" error={$setDir.error} />
{/if}
{#if $clearDir.isError}
<Error action="clearing build directory" error={$clearDir.error} />
{/if}

View File

@@ -0,0 +1,49 @@
<script>
import { getSelectedBuilder, setSelectedBuilder } from "$lib/api";
import {
useMutation,
useQuery,
useQueryClient,
} from "@sveltestack/svelte-query";
import QueryDataWrapper from "$common/QueryDataWrapper.svelte";
import Error from "$common/Error.svelte";
import { page } from "$app/stores";
const appName = $page.params.name;
const queryClient = useQueryClient();
const queryKey = [{ appName }, "getAppBuilder"];
const selectedBuilder = useQuery(queryKey, () => getSelectedBuilder(appName));
const updateBuilder = useMutation(
(newBuilder) => setSelectedBuilder(appName, newBuilder),
{
onSuccess: (_, newBuilder) =>
queryClient.setQueryData(queryKey, newBuilder),
}
);
const builders = {
auto: "Auto Detect",
dockerfile: "Dockerfile",
herokuish: "Herokuish",
lambda: "Lambda",
// "pack": "Cloud Native Buildpacks",
null: "Null Builder",
};
</script>
<QueryDataWrapper query={selectedBuilder} action="loading selected builder">
<select
class="select w-full select-bordered"
value={$selectedBuilder.data}
on:change={(e) => $updateBuilder.mutate(e.target.value)}
>
{#each Object.keys(builders) as builder}
<option value={builder}>{builders[builder]}</option>
{/each}
</select>
</QueryDataWrapper>
{#if $updateBuilder.isError}
<Error action="changing selected builder" error={$updateBuilder.error} />
{/if}

View File

@@ -0,0 +1,60 @@
<script>
import { page } from "$app/stores";
import { useMutation, useQuery } from "@sveltestack/svelte-query";
import { getAppDeployChecksReport, setAppDeployChecksState } from "$lib/api";
import QueryDataWrapper from "$common/QueryDataWrapper.svelte";
const appName = $page.params.name;
let allDisabled = false;
let allSkipped = false;
const getReport = async () => {
const report = await getAppDeployChecksReport(appName);
allDisabled = report["all_disabled"];
allSkipped = report["all_skipped"];
return report;
};
const queryKey = [{ appName }, "getAppDeployChecks"];
const checksReport = useQuery(queryKey, getReport);
const stateMutation = useMutation((state) =>
setAppDeployChecksState(appName, state)
);
const updateSkipped = () => {
if (allDisabled) return;
$stateMutation.mutate(allSkipped ? "skipped" : "enabled");
};
const updateDisabled = () =>
$stateMutation.mutate(allDisabled ? "disabled" : "enabled");
</script>
<QueryDataWrapper query={checksReport} action="loading deploy checks state">
<label class="label mt-2" class:cursor-pointer={!allDisabled}>
<span class="label-text">Skip Deploy Checks</span>
<div
class="tooltip-warning"
class:tooltip={allDisabled}
data-tip="Checks are disabled"
>
<input
type="checkbox"
class="toggle"
disabled={allDisabled || $checksReport.isLoading}
bind:checked={allSkipped}
on:change={updateSkipped}
/>
</div>
</label>
<label class="label cursor-pointer mt-2">
<span class="label-text">Disable Deploy Checks</span>
<input
type="checkbox"
class="toggle"
disabled={$checksReport.isLoading}
bind:checked={allDisabled}
on:change={updateDisabled}
/>
</label>
</QueryDataWrapper>

View File

@@ -0,0 +1,62 @@
<script>
// TODO: delete this?
import { useMutation } from "@sveltestack/svelte-query";
import { setAppProcessDeployChecksState } from "$lib/api";
import { page } from "$app/stores";
import Error from "$common/Error.svelte";
export let process;
export let checks;
const appName = $page.params.name;
const stateMutation = useMutation((state) =>
setAppProcessDeployChecksState(appName, process, state)
);
const onToggleEnabled = async (e) =>
$stateMutation.mutate(e.target.checked ? "enabled" : "disabled");
const onToggleSkipped = async (e) =>
$stateMutation.mutate(e.target.checked ? "skipped" : "enabled");
const manualChecksTrigger = async () => {
alert("todo");
};
</script>
<span class="text-lg font-bold">{process}</span>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Enable Deployment Checks</span>
<input
type="checkbox"
class="toggle"
class:disabled={$stateMutation.isLoading}
on:change={onToggleEnabled}
checked={checks["enabled"]}
/>
</label>
</div>
{#if checks["enabled"]}
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Skip Deployment Checks</span>
<input
type="checkbox"
class="toggle"
class:disabled={$stateMutation.isLoading}
on:change={onToggleSkipped}
checked={checks["skipped"]}
/>
</label>
</div>
<button class="btn btn-sm" on:click={manualChecksTrigger}>Run Checks</button>
{/if}
{#if $stateMutation.isError}
<Error error={$stateMutation.error} />
{/if}

View File

@@ -0,0 +1,54 @@
<script>
import QueryDataWrapper from "$common/QueryDataWrapper.svelte";
import { page } from "$app/stores";
import { useQuery } from "@sveltestack/svelte-query";
import { getAppSetupConfig } from "$lib/api";
const appName = $page.params.name;
const queryKey = [{ appName }, "getAppSetupConfig"];
const query = useQuery(queryKey, () => getAppSetupConfig(appName));
let isSetup, method, deploymentBranch, repoUrl, repoGitRef, image;
$: if ($query.isFetched && $query.data) {
isSetup = $query.data["is_setup"];
method = $query.data["method"];
image = $query.data["image"];
deploymentBranch = $query.data["deployment_branch"];
repoUrl = $query.data["repo_url"];
repoGitRef = $query.data["repo_git_ref"];
}
</script>
<QueryDataWrapper {query} action="getting setup config">
{#if isSetup}
{#if method === "git"}
<span
>Building on git push to branch <strong>{deploymentBranch}</strong
></span
>
{:else if method === "sync-repo"}
<span
>Synced from remote git repo <strong>{repoUrl}</strong>
using git ref {repoGitRef}</span
>
{:else if method === "docker"}
<span>Built from image <strong>{image}</strong></span>
{/if}
<div class="">
<a href={`/app/${appName}/setup`}>
<button class="btn btn-primary"> Update Configuration </button>
</a>
</div>
{:else}
<div>
<span class="text-lg text-warning"
>App needs to be setup before it can be built</span
>
</div>
<a href={`/app/${appName}/setup`}>
<button class="btn btn-info"> Go to setup </button>
</a>
{/if}
</QueryDataWrapper>

View File

@@ -0,0 +1,5 @@
<script>
import Todo from "$common/Todo.svelte";
</script>
<Todo />

View File

@@ -0,0 +1,75 @@
<script>
import { page } from "$app/stores";
import {
useMutation,
useQuery,
useQueryClient,
} from "@sveltestack/svelte-query";
import { addAppDomain, getAppDomainsReport, removeAppDomain } from "$lib/api";
import QueryDataWrapper from "$common/QueryDataWrapper.svelte";
import Card from "$common/Card.svelte";
import Icon from "$common/Icon.svelte";
import AddDomainModal from "./AddDomainModal.svelte";
import DomainListItem from "./DomainListItem.svelte";
const appName = $page.params.name;
const queryClient = useQueryClient();
const queryKey = [{ appName }, "getAppDomainsReport"];
const domainsReport = useQuery(queryKey, () => getAppDomainsReport(appName));
let addDomainModalOpen = false;
const onSuccess = () => {
addDomainModalOpen = false;
queryClient.invalidateQueries(queryKey);
};
const addDomainMutation = useMutation(
(domain) => addAppDomain(appName, domain),
{ onSuccess }
);
const removeDomainMutation = useMutation(
(domain) => removeAppDomain(appName, domain),
{ onSuccess }
);
const addDomain = ({ detail }) => $addDomainMutation.mutate(detail.domain);
const removeDomain = ({ detail }) =>
$removeDomainMutation.mutate(detail.domain);
let domains = ["bleh.com"];
$: if ($domainsReport.isSuccess && $domainsReport.data) {
domains = $domainsReport.data["domains"] || [];
}
</script>
<QueryDataWrapper query={domainsReport} action="fetching domains">
<Card title="Domains">
{#if domains.length === 0}
<p>No domains configured</p>
{:else}
<div class="flex flex-col gap-3 w-60">
{#each domains as domain, i}
<DomainListItem {domain} on:removeDomain={removeDomain} />
{/each}
</div>
{/if}
<div slot="actions">
<button class="btn gap-3" on:click={() => (addDomainModalOpen = true)}>
Add Domain
<Icon type="add" />
</button>
</div>
</Card>
</QueryDataWrapper>
<AddDomainModal
loading={$addDomainMutation.isLoading}
bind:open={addDomainModalOpen}
on:addDomain={addDomain}
/>

View File

@@ -0,0 +1,39 @@
<script>
import Modal from "$common/Modal.svelte";
import { createEventDispatcher } from "svelte";
import { page } from "$app/stores";
export let open;
export let loading;
const appName = $page.params.name;
const dispatch = createEventDispatcher();
let domain;
const signalAddDomain = () => dispatch("addDomain", { domain });
</script>
<Modal
name="add-domain"
title="Add a new domain for {appName}"
bind:open
preventClose={loading}
>
<div class="form-control">
<label class="input-group">
<span>Domain</span>
<input
type="text"
placeholder="foo.example.com"
class="input input-bordered"
bind:value={domain}
/>
</label>
</div>
<div class="mt-4">
<button class="btn" class:loading on:click={signalAddDomain}>
Confirm
</button>
</div>
</Modal>

View File

@@ -0,0 +1,49 @@
<script>
import { page } from "$app/stores";
import { removeAppDomain } from "$lib/api";
import { createEventDispatcher } from "svelte";
import Icon from "$common/Icon.svelte";
import ConfirmationModal from "$common/ConfirmationModal.svelte";
import Alert from "$common/Alert.svelte";
export let domain = "";
export let loading = false;
let domainHref = domain.startsWith("http") ? domain : `https://${domain}`;
const dispatch = createEventDispatcher();
const signalRemoved = () => dispatch("removeDomain", { domain });
let confirmationModalOpen = false;
</script>
<div
class="flex flex-row items-center gap-2 w-auto shadow-lg bg-base-100 p-2 rounded-lg"
>
<div>
<a class="link link-hover" href={domainHref}
><span class="text-lg">{domain}</span></a
>
</div>
<div class="flex-grow" />
<div class="">
<button
class="btn bg-base-100 hover:btn-error btn-sm btn-outline gap-2 btn-circle"
class:loading
on:click={() => (confirmationModalOpen = true)}
>
{#if !loading}
<Icon type="delete" size="sm" />
{/if}
</button>
</div>
</div>
<ConfirmationModal
name="remove-domain-modal"
title="Removing Domain"
action="remove domain {domain}"
on:accepted={signalRemoved}
bind:open={confirmationModalOpen}
doingAction={loading}
/>

View File

@@ -0,0 +1,47 @@
<script>
import { page } from "$app/stores";
import { getAppConfig, setAppConfig } from "$lib/api";
import { useQuery, useMutation } from "@sveltestack/svelte-query";
import Error from "$common/Error.svelte";
import QueryDataWrapper from "$common/QueryDataWrapper.svelte";
import KVEditor from "$common/KVEditor.svelte";
import Card from "$common/Card.svelte";
const appName = $page.params.name;
const queryKey = [{ appName }, "getAppConfig"];
const varsQuery = useQuery(queryKey, () => getAppConfig(appName));
let stateDirty = false;
const setConfigMutation = useMutation(setAppConfig, {
onSuccess: () => {
stateDirty = false;
},
});
const saveVars = async ({ detail }) => {
let newConfig = {};
for (let [k, v] of detail) newConfig[k] = v;
$setConfigMutation.mutate({ appName, newConfig });
};
</script>
<QueryDataWrapper query={varsQuery} action="getting environment variables">
<Card title="Environment Variables">
<KVEditor
vars={$varsQuery.data}
saving={$setConfigMutation.isLoading}
showSaveButton={true}
neutralButtons={true}
bind:stateDirty
on:save={saveVars}
/>
</Card>
</QueryDataWrapper>
{#if $setConfigMutation.isError}
<Error
action="updating environment variables"
error={$setConfigMutation.error}
/>
{/if}

View File

@@ -0,0 +1,22 @@
<script>
import { page } from "$app/stores";
import { getAppLogs } from "$lib/api";
import QueryDataWrapper from "$common/QueryDataWrapper.svelte";
import Logs from "$common/Logs.svelte";
import { useQuery } from "@sveltestack/svelte-query";
import Card from "$common/Card.svelte";
const appName = $page.params.name;
const queryKey = [{ appName }, "getAppLogs"];
const logsQuery = useQuery(queryKey, () => getAppLogs(appName));
</script>
<QueryDataWrapper query={logsQuery} action="fetching logs">
<Card title="Logs">
{#if !$logsQuery.data || $logsQuery.data.length === 0}
<p>No logs available</p>
{:else}
<Logs logs={$logsQuery.data} />
{/if}
</Card>
</QueryDataWrapper>

View File

@@ -0,0 +1,58 @@
<script>
import {
getNetworksList,
getAppNetworksReport,
updateAppNetworks,
} from "$lib/api";
import { page } from "$app/stores";
import {
useMutation,
useQuery,
useQueryClient,
} from "@sveltestack/svelte-query";
import Loader from "$common/Loader.svelte";
import Error from "$common/Error.svelte";
import AppNetworks from "./AppNetworks.svelte";
const appName = $page.params.name;
const queryClient = useQueryClient();
const reportQueryKey = [{ appName }, "getAppNetworkReport"];
const networkReport = useQuery(reportQueryKey, () =>
getAppNetworksReport(appName)
);
const networkList = useQuery("getNetworkList", getNetworksList);
const updateNetworks = useMutation(
(config) => updateAppNetworks(appName, config),
{ onSuccess: () => queryClient.invalidateQueries(reportQueryKey) }
);
let isLoaded;
$: isLoaded = $networkReport.isSuccess && $networkList.isSuccess;
</script>
{#if $networkReport.isLoading || $networkList.isLoading}
<Loader />
{/if}
{#if $networkList.isError}
<Error action="fetching network info" error={$networkList.error} />
{/if}
{#if $networkReport.isError}
<Error action="fetching app network info" error={$networkReport.error} />
{/if}
{#if isLoaded}
<AppNetworks
report={$networkReport.data}
networks={$networkList.data}
loading={$updateNetworks.isLoading}
on:saved={({ detail }) => $updateNetworks.mutate(detail)}
/>
{/if}
{#if $updateNetworks.isError}
<Error action="updating app network details" error={$updateNetworks.error} />
{/if}

View File

@@ -0,0 +1,154 @@
<script>
import { createEventDispatcher } from "svelte";
import NetworkSelect from "./NetworkSelect.svelte";
import Card from "$common/Card.svelte";
import Cards from "$common/Cards.svelte";
export let loading = false;
export let report = {};
export let networks = [];
let reportAI, reportPC, reportPD;
let initial, postCreate, postDeploy;
let reportBAI, bindAllInterfaces;
let reportTLD, tld;
let enableInitial = false;
let enablePostCreate = false;
let enablePostDeploy = false;
const resetValues = () => {
reportAI = report["attach_initial"];
initial = reportAI;
enableInitial = !!initial;
reportPC = report["attach_post_create"];
postCreate = reportPC;
enablePostCreate = !!postCreate;
reportPD = report["attach_post_deploy"];
postDeploy = reportPD;
enablePostDeploy = !!postDeploy;
reportBAI = report["bind_all_interfaces"];
bindAllInterfaces = reportBAI === true;
reportTLD = report["tld"];
tld = reportTLD;
};
$: if (report) resetValues();
const getDirty = () => {
let pairs = [
[enableInitial ? initial : "", "attach_initial"],
[enablePostCreate ? postCreate : "", "attach_post_create"],
[enablePostDeploy ? postDeploy : "", "attach_post_deploy"],
[bindAllInterfaces, "bind_all_interfaces"],
[tld, "tld"],
];
return pairs.filter((pair) => pair[0] !== report[pair[1]]);
};
const dispatch = createEventDispatcher();
const tryUpdateNetwork = () => {
let dirty = getDirty();
let updateMap = {};
for (let i in dirty) {
let [val, key] = dirty[i];
updateMap[key] = val;
}
dispatch("saved", updateMap);
};
</script>
<Cards>
<Card title="Attached Networks">
<NetworkSelect
labelText="Initial"
bind:enable={enableInitial}
bind:selected={initial}
dirty={initial !== reportAI && enableInitial !== !!reportAI}
on:save={tryUpdateNetwork}
{networks}
{loading}
/>
<NetworkSelect
labelText="Post-Create"
bind:enable={enablePostCreate}
bind:selected={postCreate}
dirty={postCreate !== reportPC && enablePostCreate !== !!reportPC}
on:save={tryUpdateNetwork}
{networks}
{loading}
/>
<NetworkSelect
labelText="Post-Deploy"
bind:enable={enablePostDeploy}
bind:selected={postDeploy}
dirty={postDeploy !== reportPD && enablePostDeploy !== !!reportPD}
on:save={tryUpdateNetwork}
{networks}
{loading}
/>
<div class="divider max-w-xs" />
<div class="form-control w-full max-w-xs">
<label class="label cursor-pointer">
<span class="label-text">Bind All Interfaces</span>
<input
type="checkbox"
class="toggle"
bind:checked={bindAllInterfaces}
/>
</label>
<button
class="btn btn-primary"
on:click={tryUpdateNetwork}
class:hidden={bindAllInterfaces === reportBAI}
class:loading
>
Update
</button>
</div>
</Card>
<Card title="Custom TLD">
<div class="form-control w-full max-w-xs">
<label class="label">
<span class="label-text">Set Custom TLD</span>
</label>
<input
type="text"
class="input input-bordered w-full max-w-xs"
placeholder="svc.local"
bind:value={tld}
/>
</div>
<div slot="actions">
<button
class="btn btn-primary"
on:click={tryUpdateNetwork}
class:hidden={tld === reportTLD}
class:loading
>
Update
</button>
</div>
</Card>
{#if report["web_listeners"]}
<Card title="Web Listeners">
<p>{report["web_listeners"]}</p>
</Card>
{/if}
<!--div class="mt-2">
<button class="btn btn-primary" on:click={tryUpdateNetwork} class:loading>
Update
</button>
</div-->
</Cards>

View File

@@ -0,0 +1,40 @@
<script>
import { createEventDispatcher } from "svelte";
export let labelText;
export let enable;
export let selected = "";
export let networks = [];
export let loading;
export let dirty;
if (!selected) selected = "host";
const dispatch = createEventDispatcher();
</script>
<div class="form-control w-full max-w-xs">
<label class="label">
<span class="label-text">{labelText}</span>
<input type="checkbox" class="toggle" bind:checked={enable} />
</label>
{#if enable}
<select
bind:value={selected}
class="select select-bordered w-full max-w-xs"
>
{#each networks as network}
<option value={network}>{network}</option>
{/each}
</select>
{/if}
<button
class="btn btn-primary my-2"
on:click={() => dispatch("save")}
class:hidden={!dirty}
class:loading
>
Update
</button>
</div>

View File

@@ -0,0 +1,135 @@
<script>
import {
listServices,
listAppServices,
linkServiceToApp,
unlinkServiceFromApp,
} from "$lib/api";
import { commandExecutionIds, executionIdDescriptions } from "$lib/stores";
import { page } from "$app/stores";
import Loader from "$common/Loader.svelte";
import Error from "$common/Error.svelte";
import LinkCard from "$components/links/LinkCard.svelte";
import {
useMutation,
useQuery,
useQueryClient,
} from "@sveltestack/svelte-query";
import Cards from "$common/Cards.svelte";
import Card from "$common/Card.svelte";
const appName = $page.params.name;
const queryClient = useQueryClient();
const linkedQueryKey = [{ appName }, "listAppServices"];
const allServices = useQuery("listServices", listServices);
const getAppServices = useQuery(linkedQueryKey, () =>
listAppServices(appName)
);
let linkedServices = [];
let unlinkedServices = [];
$: if ($getAppServices.isSuccess && $allServices.isSuccess) {
linkedServices = $getAppServices.data;
unlinkedServices = $allServices.data.filter((service) => {
return (
linkedServices.find((el) => service.name === el.name) === undefined
);
});
}
const invalidateServicesQuery = () =>
queryClient.invalidateQueries(linkedQueryKey);
let loading = {};
const linkService = async ({ serviceName, options }) => {
loading[serviceName] = true;
const id = await linkServiceToApp(serviceName, appName, options);
$executionIdDescriptions[id] = `Linking ${serviceName} to ${appName}`;
return await commandExecutionIds.addID(id);
};
const unlinkService = async ({ serviceName }) => {
loading[serviceName] = true;
const id = await unlinkServiceFromApp(serviceName, appName);
$executionIdDescriptions[id] = `Unlinking ${serviceName} from ${appName}`;
return await commandExecutionIds.addID(id);
};
const mutationOutcomes = {
onSuccess: () => invalidateServicesQuery(),
onSettled: (_, __, { serviceName }) => {
loading[serviceName] = false;
},
};
const linkMutation = useMutation(linkService, mutationOutcomes);
const unlinkMutation = useMutation(unlinkService, mutationOutcomes);
</script>
{#if $allServices.isLoading || $getAppServices.isLoading}
<Loader />
{/if}
{#if $allServices.isError}
<Error action="loading services" error={$allServices.error} />
{/if}
{#if $getAppServices.isError}
<Error action="loading linked services" error={$getAppServices.error} />
{/if}
{#if $allServices.isSuccess && $getAppServices.isSuccess}
<Cards>
{#if linkedServices.length > 0}
<Card title="Linked">
<div class="flex flex-row flex-wrap gap-2">
{#each linkedServices as service}
<LinkCard
cardType="service"
{appName}
isLinked={true}
serviceName={service.name}
serviceType={service.type}
on:unlink={({ detail }) => $unlinkMutation.mutate(detail)}
loading={loading[service.name]}
/>
{/each}
</div>
</Card>
{/if}
{#if unlinkedServices.length > 0}
<Card title="Unlinked">
<div class="flex flex-row flex-wrap w-auto gap-2">
<!-- unlinked services -->
{#each unlinkedServices as service}
<LinkCard
cardType="service"
{appName}
isLinked={false}
serviceName={service.name}
serviceType={service.type}
on:link={({ detail }) => $linkMutation.mutate(detail)}
loading={loading[service.name]}
/>
{/each}
</div>
</Card>
{:else}
<Card title="No Services">
<a href="/service/new" class="link">
<button class="btn btn-secondary">Create a service</button>
</a>
</Card>
{/if}
</Cards>
{/if}
{#if $linkMutation.isError}
<Error action="linking service" error={$linkMutation.error} />
{/if}
{#if $unlinkMutation.isError}
<Error action="unlinking service" error={$unlinkMutation.error} />
{/if}

View File

@@ -0,0 +1,16 @@
<script>
import DestroyAppButton from "./DestroyAppButton.svelte";
import RenameAppButton from "./RenameAppButton.svelte";
import Cards from "$common/Cards.svelte";
import Card from "$common/Card.svelte";
</script>
<Cards>
<Card title="Rename App">
<RenameAppButton />
</Card>
<Card title="Destroy App">
<DestroyAppButton />
</Card>
</Cards>

View File

@@ -0,0 +1,50 @@
<script>
import { goto } from "$app/navigation";
import { destroyApp } from "$lib/api";
import { page } from "$app/stores";
import ConfirmationModal from "$common/ConfirmationModal.svelte";
import Icon from "$common/Icon.svelte";
import { useMutation, useQueryClient } from "@sveltestack/svelte-query";
import Error from "$common/Error.svelte";
const appName = $page.params.name;
let confirmationModalOpen = false;
const queryClient = useQueryClient();
const onSuccess = async () => {
await queryClient.invalidateQueries("getAllAppsOverview");
await goto("/");
};
const destroyAppMutation = useMutation(destroyApp, { onSuccess });
</script>
<div class="">
<button
class="btn btn-error gap-2 w-56"
class:loading={$destroyAppMutation.isLoading}
on:click={() => (confirmationModalOpen = true)}
>
{#if $destroyAppMutation.isLoading}
Destroying...
{:else}
Destroy App
<Icon type="delete" />
{/if}
</button>
</div>
{#if $destroyAppMutation.isError}
<div class="my-2">
<Error action="destroying app" error={$destroyAppMutation.error} />
</div>
{/if}
<ConfirmationModal
name="destroy-app-modal"
title="Destroy App"
action="destroy {appName}"
on:accepted={() => $destroyAppMutation.mutate(appName)}
bind:open={confirmationModalOpen}
/>

View File

@@ -0,0 +1,63 @@
<script>
import { renameApp } from "$lib/api";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import Modal from "$common/Modal.svelte";
import Icon from "$common/Icon.svelte";
import { useMutation } from "@sveltestack/svelte-query";
const appName = $page.params.name;
let newName = appName;
let renameModalOpen = false;
const renameMutation = useMutation((newName) => renameApp(appName, newName), {
onSuccess: () => goto(`/app/${newName}`),
});
</script>
<div class="">
<button
class="btn btn-info gap-2 w-56"
on:click={() => (renameModalOpen = true)}
>
Rename App
<Icon type="edit" />
</button>
</div>
<Modal
name="create-app"
title="Rename '{appName}'"
bind:open={renameModalOpen}
preventClose={$renameMutation.isLoading}
>
<div class="form-control w-full max-w-xs mb-4">
<label class="label" for="new-name">
<span class="label-text">Enter a new name</span>
</label>
<input
id="new-name"
type="text"
bind:value={newName}
disabled={$renameMutation.isLoading}
class="input input-bordered w-full max-w-xs"
/>
</div>
<button
class="btn btn-primary"
on:click={() => $renameMutation.mutate(newName)}
class:loading={$renameMutation.isLoading}
>
Save
</button>
<button
class="btn btn-ghost"
on:click={() => (renameModalOpen = false)}
disabled={$renameMutation.isLoading}
>
Cancel
</button>
</Modal>

View File

@@ -0,0 +1,79 @@
<script>
import { page } from "$app/stores";
import { goto } from "$app/navigation";
import { useMutation, useQueryClient } from "@sveltestack/svelte-query";
import { setupApp, setupAppAsync } from "$lib/api";
import { commandExecutionIds, executionIdDescriptions } from "$lib/stores";
import Steps from "$common/Steps.svelte";
import Select from "./steps/Select.svelte";
import Configure from "./steps/Configure.svelte";
import Confirm from "./steps/Confirm.svelte";
import GitNew from "./configs/GitNew.svelte";
import GitSync from "./configs/GitSync.svelte";
import DockerImage from "./configs/DockerImage.svelte";
const appName = $page.params.name;
const steps = [
{ label: "Select an app source", component: Select },
{ label: "Configure source", component: Configure },
{ label: "Confirm", component: Confirm },
];
const sources = {
"new-repo": {
label: "Create New Git Repo",
createText: "create a git repo",
component: GitNew,
},
"sync-repo": {
label: "Sync Existing Git Repo",
createText: "sync an existing git repo",
component: GitSync,
},
"pull-image": {
label: "Docker Image",
createText: "pull a docker image",
component: DockerImage,
},
// "upload-archive": {label: "Upload Archive File", component: Archive, options: null},
};
const doSyncSetup = ({ source, options }) => {
return setupApp(appName, source, options);
};
const doAsyncSetup = async ({ source, options }) => {
const id = await setupAppAsync(appName, source, options);
$executionIdDescriptions[id] = `Setting up ${appName}`;
const success = await commandExecutionIds.addID(id);
if (success) return true;
throw new Error("execution was unsuccessful");
};
const queryClient = useQueryClient();
const onSuccess = async () => {
await queryClient.invalidateQueries([{ appName }]);
await goto(`/app/${appName}`);
};
const setupAppMutation = useMutation(doSyncSetup, { onSuccess });
const setupAppAsyncMutation = useMutation(doAsyncSetup, { onSuccess });
const trySetup = () => {
const source = data.selectedSource;
const options = data.sourceOptions[source];
if (source === "new-repo") $setupAppMutation.mutate({ source, options });
else $setupAppAsyncMutation.mutate({ source, options });
};
const props = { sources };
let data = {};
let loading;
$: loading = $setupAppMutation.isLoading || $setupAppAsyncMutation.isLoading;
</script>
<Steps {steps} {props} {data} {loading} on:complete={trySetup} />

View File

@@ -0,0 +1,62 @@
<script>
import { createEventDispatcher } from "svelte";
import Dropzone from "svelte-file-dropzone";
import Icon from "$common/Icon.svelte";
export let options;
if (!options) options = { archive: null };
const dispatch = createEventDispatcher();
const checkOptionsValid = () => {
dispatch("validityChange", { valid: !!options["archive"] });
};
function handleFilesSelect(e) {
const { acceptedFiles } = e.detail;
if (acceptedFiles.length > 0) {
const file = acceptedFiles[0];
if (file.type === "application/x-gzip" && !file.name.endsWith(".tar.gz"))
return;
archiveChosen(file);
}
}
const archiveChosen = (archiveFile) => {
options["archive"] = archiveFile;
checkOptionsValid();
};
let hovering = false;
const dragEnter = () => (hovering = true);
const dragLeave = () => (hovering = false);
</script>
<div
class="p-6 items-center rounded-lg cursor-pointer text-neutral-content bg-neutral hover:bg-neutral-focus"
class:bg-neutral-focus={hovering}
>
<Dropzone
disableDefaultStyles="true"
accept=".zip, application/gzip, .gz"
on:drop={handleFilesSelect}
on:dragenter={dragEnter}
on:dragleave={dragLeave}
>
<div class="flex flex-col items-center p-6">
<Icon type="upload" size="lg" />
<p class="my-1">
<span class="font-semibold">Click to upload</span> or drag and drop
</p>
<p class="text-sm">.zip or .tar.gz</p>
</div>
</Dropzone>
</div>
{#if options["archive"]}
<div class="mt-2">
<span
>Uploading <span class="font-bold">{options["archive"].name}</span></span
>
</div>
{/if}

View File

@@ -0,0 +1,23 @@
<script>
import { createEventDispatcher } from "svelte";
export let options;
if (!options) options = { image: "" };
const dispatch = createEventDispatcher();
const checkOptionsValid = () => {
dispatch("validityChange", { valid: !!options["image"] });
};
</script>
<div class="flex flex-col gap-2">
<label class="input-group input-group-md">
<span class="w-auto">Docker Image</span>
<input
type="text"
class="input input-md input-bordered flex-grow"
on:change={checkOptionsValid}
bind:value={options["image"]}
/>
</label>
</div>

View File

@@ -0,0 +1,35 @@
<script>
import { createEventDispatcher, onMount } from "svelte";
export let options;
if (!options) {
options = {
deployment_branch: "main",
// envVar: "GIT_REV",
};
}
const dispatch = createEventDispatcher();
const checkOptionsValid = () => {
dispatch("validityChange", { valid: options.deployment_branch !== "" });
};
onMount(() => dispatch("validityChange", { valid: true }));
</script>
<div class="flex flex-col gap-2">
<label class="input-group input-group-md">
<span class="w-auto">Deployment Branch</span>
<input
type="text"
class="input input-md input-bordered flex-grow"
bind:value={options["deployment_branch"]}
on:change={checkOptionsValid}
/>
</label>
<!--label class="input-group input-group-md">
<span class="w-auto">Git Revision Environment Variable</span>
<input type="text" class="input input-md input-bordered flex-grow"
bind:value={options["envVar"]} />
</label-->
</div>

View File

@@ -0,0 +1,75 @@
<script>
import { createEventDispatcher } from "svelte";
export let options;
if (!options) {
options = {
repository_url: "",
git_ref: "",
_use_custom_dockerfile_path: false,
custom_dockerfile_path: "",
};
}
const dispatch = createEventDispatcher();
const checkOptionsValid = () => {
// TODO: check repo access etc
dispatch("validityChange", { valid: !!options["repository_url"] });
};
</script>
<div class="flex flex-col gap-2">
<label class="input-group input-group-md">
<span class="w-auto">Repository URL</span>
<input
type="text"
class="input input-md input-bordered flex-grow"
placeholder="https://github.com/heroku/node-js-getting-started.git"
bind:value={options["repository_url"]}
on:change={checkOptionsValid}
/>
</label>
<span class="text-sm"
>If the repository is private, ensure you set up access using the
credentials found in
<a href="/settings" class="link" target="_blank">settings</a>.
</span>
<label class="input-group input-group-md">
<span class="w-auto">Git Reference (branch, tag, commit)</span>
<input
type="text"
class="input input-md input-bordered flex-grow"
placeholder="optional"
bind:value={options["git_ref"]}
on:change={checkOptionsValid}
/>
</label>
<!--div class="form-control w-60 rounded-lg">
<label class="label cursor-pointer">
<span class="label-text">Specify Dockerfile Path?</span>
<input
type="checkbox"
class="checkbox checkbox-accent"
bind:checked={options["_use_custom_dockerfile_path"]}
on:change={checkOptionsValid}
/>
</label>
</div>
{#if options["_use_custom_dockerfile_path"]}
<label class="input-group input-group-md">
<span class="w-auto">Dockerfile Path</span>
<input
type="text"
class="input input-md input-bordered flex-grow"
placeholder="Dockerfile"
on:change={checkOptionsValid}
bind:value={options["custom_dockerfile_path"]}
/>
</label>
{/if}
!-->
</div>

View File

@@ -0,0 +1,30 @@
<script>
import { createEventDispatcher } from "svelte";
export let data = {};
export let props = {};
let sourceComponent;
$: if (data.selectedSource && props.sources[data.selectedSource]) {
sourceComponent = props.sources[data.selectedSource]?.component;
}
$: if (!data.sourceOptions) {
data.sourceOptions = {};
}
const dispatch = createEventDispatcher();
const onValidityChanged = ({ valid }) => {
dispatch("statusChange", { complete: valid });
};
</script>
<span class="text-lg">Configure source:</span>
{#if sourceComponent}
<svelte:component
this={sourceComponent}
bind:options={data.sourceOptions[data.selectedSource]}
on:validityChange={(e) => onValidityChanged(e.detail)}
/>
{/if}

View File

@@ -0,0 +1,40 @@
<script>
import { createEventDispatcher, onMount } from "svelte";
export let props = {};
export let data = {};
let selectedSource = data.selectedSource;
let sourceOptions = data.sourceOptions[selectedSource];
const optionDisplayText = (option) => {
const value = sourceOptions[option];
if (typeof value === "object") return value.name;
return value;
};
const dispatch = createEventDispatcher();
onMount(() => dispatch("statusChange", { complete: true }));
</script>
<span
>Going to
<span class="font-bold">{props.sources[selectedSource].createText}</span>
with configuration</span
>
<div class="overflow-x-auto mt-3">
<table class="table table-compact w-full">
<thead>
<tr><th>Key</th><th>Value</th></tr>
</thead>
<tbody>
{#each Object.keys(sourceOptions) as option}
{#if !option.startsWith("_") && optionDisplayText(option) !== ""}
<tr>
<td>{option}</td>
<td>{optionDisplayText(option)}</td>
</tr>
{/if}
{/each}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,26 @@
<script>
import { createEventDispatcher } from "svelte";
export let props = {};
export let data = {};
const dispatch = createEventDispatcher();
const sourceSelected = (source) => {
data.selectedSource = source;
dispatch("statusChange", { complete: true });
};
</script>
<span class="text-lg">Choose a source:</span>
<div class="tabs tabs-boxed bg-base-100 mt-4">
{#each Object.keys(props.sources) as source}
<a
class="tab"
class:tab-active={source === data.selectedSource}
on:click={() => sourceSelected(source)}
>
{props.sources[source].label}
</a>
{/each}
</div>

View File

@@ -0,0 +1,74 @@
<script>
import { page } from "$app/stores";
import {
useMutation,
useQuery,
useQueryClient,
} from "@sveltestack/svelte-query";
import { getAppStorages, mountAppStorage, unmountAppStorage } from "$lib/api";
import Icon from "$common/Icon.svelte";
import QueryDataWrapper from "$common/QueryDataWrapper.svelte";
import StorageMount from "./StorageMount.svelte";
import CreateStorageModal from "./CreateStorageModal.svelte";
import Card from "$common/Card.svelte";
const appName = $page.params.name;
const queryClient = useQueryClient();
const queryKey = [{ appName }, "getAppStorages"];
const storageReport = useQuery(queryKey, () => getAppStorages(appName));
const invalidateStorages = () => queryClient.invalidateQueries(queryKey);
let createModalOpen = false;
const mountMutation = useMutation(
(options) => mountAppStorage(appName, options),
{
onSuccess: () => {
createModalOpen = false;
invalidateStorages();
},
}
);
let loading = {};
const unmountMutation = useMutation(
(options) => {
loading[options["hostDir"]] = true;
return unmountAppStorage(appName, options);
},
{
onSuccess: invalidateStorages,
onSettled: (_, __, options) => (loading[options["hostDir"]] = false),
}
);
</script>
<QueryDataWrapper query={storageReport} action="fetching app storage">
<Card title="Storage Mounts">
<div class="flex flex-col gap-3">
{#each $storageReport.data["mounts"] as mount, i}
<StorageMount
{...mount}
loading={loading[mount["host_dir"]]}
on:unmount={({ detail }) => $unmountMutation.mutate(detail)}
/>
{/each}
</div>
<div slot="actions">
<button class="btn gap-2" on:click={() => (createModalOpen = true)}>
<Icon type="add" />
New Storage Mount
</button>
</div>
</Card>
</QueryDataWrapper>
<CreateStorageModal
bind:open={createModalOpen}
loading={$mountMutation.isLoading}
error={$mountMutation.error}
on:create={({ detail }) => $mountMutation.mutate(detail)}
/>

View File

@@ -0,0 +1,84 @@
<script>
import { createEventDispatcher } from "svelte";
import Modal from "$common/Modal.svelte";
import Error from "$common/Error.svelte";
export let open;
export let loading;
export let error;
const storageTypes = {
"New Storage": { label: "Storage Name" },
// "Mount Existing Directory": {"label": "Existing Path to Mount"}
};
let selectedType = "New Storage";
let hostDir = "";
let mountDir = "";
let typeOptions = Object.keys(storageTypes);
const dispatch = createEventDispatcher();
const dispatchCreateStorage = () => {
const options = { selectedType, hostDir, mountDir };
dispatch("create", options);
};
</script>
<Modal
name="create-storage"
title="Create App Storage"
bind:open
preventClose={loading}
>
<div class="form-control" class:hidden={typeOptions.length < 2}>
<label class="label">
<span class="label-text">Select Storage Type</span>
</label>
<select
class="select select-bordered w-full max-w-xs"
bind:value={selectedType}
>
{#each typeOptions as storageType}
<option value={storageType}>
{storageType}
</option>
{/each}
</select>
</div>
<div class="my-3 p-2 border rounded-box flex flex-col gap-2">
<label class="input-group input-group-md">
<span class="w-auto">{storageTypes[selectedType]["label"]}</span>
<input
type="text"
placeholder="foo"
class="input input-md input-bordered flex-grow"
bind:value={hostDir}
/>
</label>
<label class="input-group input-group-md flex">
<span class="w-auto">In-App Path</span>
<input
type="text"
placeholder="/data"
class="input input-md input-bordered w-auto flex-grow"
bind:value={mountDir}
/>
</label>
</div>
{#if error}
<div class="mb-4">
<Error {error} action="mounting storage" />
</div>
{/if}
<div class="mt-3">
<button class="btn" class:loading on:click={dispatchCreateStorage}>
Submit
</button>
</div>
</Modal>

View File

@@ -0,0 +1,77 @@
<script>
import Icon from "$common/Icon.svelte";
import { createEventDispatcher } from "svelte";
import ConfirmationModal from "$common/ConfirmationModal.svelte";
export let hostDir;
export let mountDir;
export let loading;
export let isBuildMount;
export let isRunMount;
export let isDeployMount;
let hostDokkuDir;
let isDokkuManaged = false;
const dokkuDir = "/var/lib/dokku/data/storage/";
$: if (hostDir) {
if (hostDir.startsWith(dokkuDir)) {
isDokkuManaged = true;
hostDokkuDir = hostDir.slice(dokkuDir.length);
}
}
let confirmationModalOpen = false;
const dispatch = createEventDispatcher();
const tryUnmount = (e) => {
const restart = e.detail.extraOptionChecked;
dispatch("unmount", { hostDir, mountDir, restart });
};
</script>
<div class="flex flex-row gap-2">
<div class="flex items-center" class:flex-grow={!isDokkuManaged}>
<label class="input-group input-group-md" class:hidden={isDokkuManaged}>
<span class="w-auto">Mounted Path</span>
<input
type="text"
value={hostDir}
class="input input-md input-bordered"
disabled
class:flex-grow={!isDokkuManaged}
/>
</label>
<div class="" class:hidden={!isDokkuManaged}>
<span class="font-bold">{hostDokkuDir}</span>
</div>
</div>
<div class="flex-grow">
<label class="input-group input-group-md">
<span>Container Path</span>
<input
type="text"
class="input input-md input-bordered w-auto flex-grow"
disabled
value={mountDir}
/>
</label>
</div>
<button
class="btn btn-ghost btn-circle text-error-content ml-2"
class:loading
on:click={() => (confirmationModalOpen = true)}
>
<Icon type="remove" />
</button>
</div>
<ConfirmationModal
name="unmount-storage"
action="unmount this storage"
extraOption="Restart app?"
bind:open={confirmationModalOpen}
on:accepted={tryUnmount}
/>

View File

@@ -0,0 +1,74 @@
<script>
import { getAppProcesses, executeCommandInProcess } from "$lib/api";
import { page } from "$app/stores";
import Terminal from "$components/commands/Terminal.svelte";
import Error from "$common/Error.svelte";
const appName = $page.params.name;
let processes;
let selectedProcess;
let error;
let errorAction;
const initProcessSelection = async () => {
try {
let psList = await getAppProcesses(appName);
processes = psList;
if (psList.length === 1) {
selectedProcess = psList[0];
}
} catch (e) {
error = e;
errorAction = "loading processes";
}
};
let terminalOutput = [];
const onTerminalInput = async (cmd) => {
if (cmd === "clear") {
terminalOutput = [];
return;
}
terminalOutput = [...terminalOutput, { input: cmd }];
try {
const res = await executeCommandInProcess(appName, selectedProcess, cmd);
terminalOutput = [...terminalOutput, res];
} catch (e) {
console.error(e);
}
};
initProcessSelection();
</script>
{#if processes && !selectedProcess}
<div class="form-control w-full max-w-md p-2 flex flex-row gap-2">
<label class="label">
<span class="label-text">Process type:</span>
</label>
<select
class="select select-primary select-bordered"
bind:value={selectedProcess}
>
{#each processes as process}
<option>{process}</option>
{/each}
</select>
</div>
{/if}
{#if selectedProcess}
<Terminal onInput={onTerminalInput} bind:output={terminalOutput}>
<div slot="titlebar">
<span class="text-md"
>Connected to <span class="font-bold">{selectedProcess}</span></span
>
</div>
</Terminal>
{/if}
{#if error}
<Error {error} action={errorAction} />
{/if}