Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions web-admin/src/features/projects/ProjectTabs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@
hasPermission: projectPermissions.manageProject,
},
{
// TODO: Change this back to `/${organization}/${project}/-/settings`
// Once project settings are implemented
route: `/${organization}/${project}/-/settings/environment-variables`,
route: `/${organization}/${project}/-/settings`,
label: "Settings",
hasPermission: projectPermissions.manageProject,
},
Expand Down
60 changes: 60 additions & 0 deletions web-admin/src/features/projects/settings/DeleteProject.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts">
import { goto } from "$app/navigation";
import {
createAdminServiceDeleteProject,
getAdminServiceGetProjectQueryKey,
getAdminServiceListProjectsForOrganizationQueryKey,
} from "@rilldata/web-admin/client";
import SettingsContainer from "@rilldata/web-admin/features/organizations/settings/SettingsContainer.svelte";
import { Button } from "@rilldata/web-common/components/button";
import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient";
import AlertDialogGuardedConfirmation from "@rilldata/web-common/components/alert-dialog/alert-dialog-guarded-confirmation.svelte";

export let organization: string;
export let project: string;

const deleteProjectMutation = createAdminServiceDeleteProject();

$: deleteProjectResult = $deleteProjectMutation;

async function deleteProject() {
await $deleteProjectMutation.mutateAsync({
org: organization,
project: project,
});

void goto(`/${organization}`);
queryClient.removeQueries({
queryKey: getAdminServiceGetProjectQueryKey(organization, project),
});
await queryClient.invalidateQueries({
queryKey:
getAdminServiceListProjectsForOrganizationQueryKey(organization),
});
eventBus.emit("notification", {
message: "Deleted project",
});
}
</script>

<SettingsContainer title="Delete project">
<svelte:fragment slot="body">
Permanently remove all contents of this project. This action cannot be
undone.
</svelte:fragment>

<AlertDialogGuardedConfirmation
slot="action"
title="Delete this project?"
description={`The project ${project} will be deleted permanently. This action cannot be undone.`}
confirmText={`delete ${project}`}
loading={deleteProjectResult.isPending}
error={deleteProjectResult.error?.message ?? ""}
onConfirm={deleteProject}
>
<svelte:fragment let:builder>
<Button builders={[builder]} type="destructive">Delete project</Button>
</svelte:fragment>
</AlertDialogGuardedConfirmation>
</SettingsContainer>
57 changes: 57 additions & 0 deletions web-admin/src/features/projects/settings/HibernateProject.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script lang="ts">
import {
createAdminServiceHibernateProject,
getAdminServiceGetProjectQueryKey,
} from "@rilldata/web-admin/client";
import SettingsContainer from "@rilldata/web-admin/features/organizations/settings/SettingsContainer.svelte";
import { Button } from "@rilldata/web-common/components/button";
import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient";
import AlertDialogGuardedConfirmation from "@rilldata/web-common/components/alert-dialog/alert-dialog-guarded-confirmation.svelte";

export let organization: string;
export let project: string;

const hibernateProjectMutation = createAdminServiceHibernateProject();

$: hibernateResult = $hibernateProjectMutation;

async function hibernateProject() {
await $hibernateProjectMutation.mutateAsync({
org: organization,
project: project,
});

void queryClient.refetchQueries({
queryKey: getAdminServiceGetProjectQueryKey(organization, project),
});

eventBus.emit("notification", {
message: "Project hibernated",
});
}
</script>

<SettingsContainer title="Hibernate project">
<svelte:fragment slot="body">
Put this project into hibernation mode. Hibernated projects are paused and
do not consume resources. The project can be woken up at any time by
accessing it.
</svelte:fragment>

<AlertDialogGuardedConfirmation
slot="action"
title="Hibernate this project?"
description={`The project ${project} will be put into hibernation mode. It can be reactivated by accessing it again.`}
confirmText={`hibernate ${project}`}
loading={hibernateResult.isPending}
error={hibernateResult.error?.message ?? ""}
onConfirm={hibernateProject}
>
<svelte:fragment slot="default" let:builder>
<Button builders={[builder]} type="secondary-destructive"
>Hibernate project</Button
>
</svelte:fragment>
</AlertDialogGuardedConfirmation>
</SettingsContainer>
170 changes: 170 additions & 0 deletions web-admin/src/features/projects/settings/ProjectNameSettings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<script lang="ts">
import { goto } from "$app/navigation";
import {
createAdminServiceGetProject,
createAdminServiceUpdateProject,
getAdminServiceGetProjectQueryKey,
getAdminServiceListProjectsForOrganizationQueryKey,
type RpcStatus,
} from "@rilldata/web-admin/client";
import { parseUpdateProjectError } from "@rilldata/web-admin/features/projects/settings/errors";
import SettingsContainer from "@rilldata/web-admin/features/organizations/settings/SettingsContainer.svelte";
import { Button } from "@rilldata/web-common/components/button";
import Input from "@rilldata/web-common/components/forms/Input.svelte";
import { sanitizeOrgName } from "@rilldata/web-common/features/organization/sanitizeOrgName";
import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient";
import type { AxiosError } from "axios";
import { defaults, superForm } from "sveltekit-superforms";
import { yup } from "sveltekit-superforms/adapters";
import { object, string } from "yup";

export let organization: string;
export let project: string;

// Reuse org name sanitizer for project names
const sanitizeProjectName = sanitizeOrgName;

const initialValues: {
name: string;
description: string;
} = {
name: "",
description: "",
};
const schema = yup(
object({
name: string().required(),
description: string(),
}),
);

const updateProjectMutation = createAdminServiceUpdateProject();

const { form, errors, enhance, submit } = superForm(
defaults(initialValues, schema),
{
SPA: true,
validators: schema,
async onUpdate({ form }) {
if (!form.valid) return;
const values = form.data;

const newProject = sanitizeProjectName(values.name);

try {
await $updateProjectMutation.mutateAsync({
org: organization,
project: project,
data: {
newName: newProject,
description: values.description,
},
});

await queryClient.invalidateQueries({
queryKey:
getAdminServiceListProjectsForOrganizationQueryKey(organization),
});
} catch (err) {
const parsedErr = parseUpdateProjectError(
err as AxiosError<RpcStatus>,
);
if (parsedErr.duplicateProject) {
form.errors.name = [`The name ${newProject} is already taken`];
}
return;
}

if (project !== newProject) {
queryClient.removeQueries({
queryKey: getAdminServiceGetProjectQueryKey(organization, project),
});
setTimeout(() => goto(`/${organization}/${newProject}/-/settings`));
} else {
void queryClient.refetchQueries({
queryKey: getAdminServiceGetProjectQueryKey(organization, project),
});
}
eventBus.emit("notification", {
message: "Updated project",
});
},
resetForm: false,
},
);

$: projectResp = createAdminServiceGetProject(organization, project);
$: if ($projectResp.data?.project) {
$form.name = $projectResp.data.project.name ?? "";
$form.description = $projectResp.data.project.description ?? "";
}

$: changed =
$projectResp.data?.project?.name !== $form.name ||
$projectResp.data?.project?.description !== $form.description;

$: error = parseUpdateProjectError(
$updateProjectMutation.error as unknown as AxiosError<RpcStatus>,
);
</script>

<SettingsContainer title="Project">
<form
slot="body"
id="project-update-form"
on:submit|preventDefault={submit}
class="update-project-form"
use:enhance
>
<Input
bind:value={$form.name}
errors={$errors?.name}
id="name"
label="Name"
description={`Your project URL will be https://ui.rilldata.com/${organization}/${sanitizeProjectName($form.name)}, to comply with our naming rules.`}
textClass="text-sm"
alwaysShowError
additionalClass="max-w-[520px]"
/>
{#if $form.name && sanitizeProjectName($form.name) !== project}
<div class="warning-message">
Renaming this project will invalidate all existing URLs and shared
links.
</div>
{/if}
<Input
bind:value={$form.description}
errors={$errors?.description}
id="description"
label="Description"
placeholder="Describe your project"
textClass="text-sm"
additionalClass="max-w-[520px]"
/>
</form>
{#if error?.message}
<div class="text-red-500 text-sm py-px">
{error.message}
</div>
{/if}
<Button
onClick={submit}
type="primary"
loading={$updateProjectMutation.isPending}
disabled={!changed}
slot="action"
>
Save
</Button>
</SettingsContainer>

<style lang="postcss">
.update-project-form {
@apply flex flex-col gap-y-5 w-full;
}

.warning-message {
@apply text-sm text-yellow-600 bg-yellow-50 border border-yellow-200 rounded px-3 py-2;
}
</style>
Loading
Loading