Skip to content

Commit

Permalink
Expose environments in UX/UI (#288)
Browse files Browse the repository at this point in the history
* wip

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* wip

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* working edit env and new env in project settings

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* better handing of envs in sidebar

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* fully working env switcher and new env form

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* sidebar improvements

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* env selector in import table form

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* refetch user envs on edit/delete of env

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* icon size update in team sidebar

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* wip on envs in new project form

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* lint fix

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* working envs in new project form

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* working env preferences stored in session and used

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* fix cli

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* couple little fixes

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* don't show new env if not authorized

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* better undeployed indicator color

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* fix an edge case in resolving env

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* breadcrumb icons

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* key fix

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* better link text size

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* better sidebar heading font and layout

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* only display deploy indicator for authorized users

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* style tab better if there are no other tabs

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* actual single tab fix

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* standardized link component, new styling on section headers

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* prop name update

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* better breadcrumb separator style

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* clean up env name validator schema

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* radius update

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* fix form defaults when updated by props

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* improve title and description spacing

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* support importing a table to an existing def

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* cli fix for def

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* refresh after table delete

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* better sidebar section header font

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* assert that imported table schema matches existing def schema

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* order projects by slug

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* new team page listing projects

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* allow sidebar link button height to be auto

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* clean up definition/table wording

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* alignment fix

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* better menu action wording

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* import form fix

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* fixed width sidebar

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

* fix referencing projects link

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>

---------

Signed-off-by: Aaron Sutula <asutula@users.noreply.github.com>
Co-authored-by: Aaron Sutula <asutula@users.noreply.github.com>
  • Loading branch information
asutula and asutula authored Jun 20, 2024
1 parent e4de8e3 commit 9309fc5
Show file tree
Hide file tree
Showing 48 changed files with 1,680 additions and 516 deletions.
99 changes: 93 additions & 6 deletions packages/api/src/routers/environments.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { type Store } from "@tableland/studio-store";
import { type schema, type Store } from "@tableland/studio-store";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { projectProcedure, publicProcedure, createTRPCRouter } from "../trpc";
import {
publicProcedure,
createTRPCRouter,
projectAdminProcedure,
environmentAdminProcedure,
} from "../trpc";

export function environmentsRouter(store: Store) {
return createTRPCRouter({
projectEnvironments: publicProcedure
.input(z.object({ projectId: z.string().trim() }))
nameAvailable: publicProcedure
.input(
z.object({
projectId: z.string().trim().min(1),
name: z.string().trim().min(1),
envId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await store.environments.getEnvironmentsByProjectId(
return await store.environments.nameAvailable(
input.projectId,
input.name,
input.envId,
);
}),
newEnvironment: projectProcedure(store)
newEnvironment: projectAdminProcedure(store)
.input(
z.object({
name: z.string().trim(),
Expand All @@ -25,6 +38,80 @@ export function environmentsRouter(store: Store) {
});
return environment;
}),
updateEnvironment: environmentAdminProcedure(store)
.input(
z.object({
name: z.string().trim(),
}),
)
.mutation(async ({ input }) => {
const environment = await store.environments.updateEnvironment({
id: input.envId,
name: input.name,
});
return environment;
}),
deleteEnvironment: environmentAdminProcedure(store).mutation(
async ({ input, ctx }) => {
const envs = await store.environments.getEnvironmentsByProjectId(
ctx.project.id,
);
if (envs.length === 1) {
throw new TRPCError({
code: "PRECONDITION_FAILED",
message: "Cannot delete the last environment in a project",
});
}
await store.environments.deleteEnvironment(input.envId);
},
),
projectEnvironments: publicProcedure
.input(z.object({ projectId: z.string().trim() }))
.query(async ({ input }) => {
return await store.environments.getEnvironmentsByProjectId(
input.projectId,
);
}),
environmentPreferenceForProject: publicProcedure
.input(
z.object({
projectId: z.string().uuid(),
}),
)
.query(async ({ input, ctx }) => {
let env: schema.Environment | undefined;
const envId = ctx.session.projectEnvs?.[input.projectId];
if (envId) {
env = await store.environments.environmentById(envId);
}
if (!env) {
const envs = await store.environments.getEnvironmentsByProjectId(
input.projectId,
);
env = envs.length ? envs[0] : undefined;
}
if (!env) {
throw new TRPCError({
code: "NOT_FOUND",
message: "No environment preference found for this project",
});
}
return env;
}),
setEnvironmentPreferenceForProject: publicProcedure
.input(
z.object({
projectId: z.string().uuid(),
envId: z.string().uuid(),
}),
)
.mutation(async ({ input, ctx }) => {
ctx.session.projectEnvs = {
...ctx.session.projectEnvs,
[input.projectId]: input.envId,
};
await ctx.session.save();
}),
environmentBySlug: publicProcedure
.input(
z.object({
Expand Down
6 changes: 1 addition & 5 deletions packages/api/src/routers/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,8 @@ export function projectsRouter(store: Store) {
ctx.teamId,
input.name,
input.description,
input.envNames.map((env) => env.name),
);
// TODO: This is temporary to make sure all projects have a default environment.
await store.environments.createEnvironment({
projectId: project.id,
name: "default",
});
return project;
} catch (err) {
throw internalError("Error creating project", err);
Expand Down
46 changes: 35 additions & 11 deletions packages/api/src/routers/tables.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import assert, { AssertionError } from "assert";
import { ApiError, type Table, Validator, helpers } from "@tableland/sdk";
import { type Store, unescapeSchema } from "@tableland/studio-store";
import {
type Store,
unescapeSchema,
type schema,
} from "@tableland/studio-store";
import { TRPCError } from "@trpc/server";
import { importTableSchema } from "@tableland/studio-validators";
import { projectProcedure, createTRPCRouter } from "../trpc";
Expand Down Expand Up @@ -41,14 +46,26 @@ export function tablesRouter(store: Store) {
});
}

let def: schema.Def | undefined;
try {
// TODO: Execute different table inserts in a batch txn.
const def = await store.defs.createDef(
input.projectId,
input.defName,
input.defDescription,
schema,
);
if (typeof input.def === "string") {
def = await store.defs.defById(input.def);
if (!def) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Definition not found.`,
});
}
assert.deepStrictEqual(def.schema, schema);
} else {
def = await store.defs.createDef(
input.projectId,
input.def.name,
input.def.description,
schema,
);
}
const deployment = await store.deployments.recordDeployment({
defId: def.id,
environmentId: input.environmentId,
Expand All @@ -59,10 +76,17 @@ export function tablesRouter(store: Store) {
});
return { def, deployment };
} catch (err) {
throw internalError(
"Error saving definition and deployment records.",
err,
);
if (err instanceof AssertionError) {
throw new TRPCError({
code: "CONFLICT",
message: `Schema of table ${input.tableId} on chain ${input.chainId} does not match the ${def?.name ?? "<unknown>"} definition.`,
});
} else {
throw internalError(
"Error saving definition and deployment records.",
err,
);
}
}
}),
});
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/session-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ export interface SessionData {
nonce?: string;
siweFields?: SiweFields;
auth?: Auth;
projectEnvs?: Record<string, string>;
}

export const defaultSession: SessionData = {
nonce: undefined,
siweFields: undefined,
auth: undefined,
projectEnvs: undefined,
};

export const sessionOptions: SessionOptions = {
Expand Down
50 changes: 50 additions & 0 deletions packages/api/src/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,56 @@ export const projectAdminProcedure = (store: Store) =>
return await next({ ctx });
});

export const environmentProcedure = (store: Store) =>
protectedProcedure
.input(z.object({ envId: z.string().uuid() }))
.use(async ({ ctx, input, next }) => {
const { team, project } =
(await store.environments.environmentTeamAndProject(input.envId)) ?? {};
if (!team) {
throw new TRPCError({
code: "NOT_FOUND",
message: "no team for env id found",
});
}
if (!project) {
throw new TRPCError({
code: "NOT_FOUND",
message: "no project for env id found",
});
}
const membership = await store.teams.isAuthorizedForTeam(
ctx.session.auth.user.teamId,
team.id,
);
if (!membership) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "not authorized for team",
});
}
return await next({
ctx: {
...ctx,
session: ctx.session,
team,
project,
teamAuthorization: membership,
},
});
});

export const environmentAdminProcedure = (store: Store) =>
environmentProcedure(store).use(async ({ ctx, next }) => {
if (!ctx.teamAuthorization.isOwner) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "not authorized as environment admin",
});
}
return await next({ ctx });
});

export const defProcedure = (store: Store) =>
protectedProcedure
.input(z.object({ defId: z.string().trim().uuid() }))
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/commands/import-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,8 @@ export const builder = function (args: Yargs) {
chainId,
tableId,
projectId,
defName,
def: { name: defName, description: defDescription },
environmentId,
defDescription,
});

logger.log(
Expand Down Expand Up @@ -210,9 +209,8 @@ export const builder = function (args: Yargs) {
chainId,
tableId,
projectId,
defName,
def: { name: defName, description: defDescription },
environmentId,
defDescription,
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export const builder = function (args: Yargs) {
teamId,
name,
description,
// TODO: Allow user to specify env names
envNames: [{ name: "default" }],
});

logger.log(JSON.stringify(result, null, 4));
Expand Down
9 changes: 9 additions & 0 deletions packages/store/src/api/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ export function initDefs(db: DrizzleD1Database<typeof schema>, tbl: Database) {
await tbl.batch(batch);
},

defById: async function (defId: string) {
const res = await db
.select({ defs })
.from(defs)
.where(eq(defs.id, defId))
.get();
return res?.defs;
},

defsByProjectId: async function (projectId: string) {
const res = await db
.select({ defs })
Expand Down
Loading

1 comment on commit 9309fc5

@vercel
Copy link

@vercel vercel bot commented on 9309fc5 Jun 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.