Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editable table data #295

Merged
merged 25 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c1cd9f3
editing in memory data working
asutula Jun 18, 2024
550cae1
wip on add row
asutula Jun 20, 2024
f735a17
add and remove row(s)
asutula Jun 20, 2024
383fb6b
working editing in react state
asutula Jul 2, 2024
6797b3d
handle deleting of edited row explicitly
asutula Jul 3, 2024
6648adc
better color for deleted row
asutula Jul 3, 2024
4f76ffd
construct and execute sql statements working
asutula Jul 4, 2024
4cd70e1
display table owner card
asutula Jul 5, 2024
3c7163c
user logo for owner card
asutula Jul 5, 2024
7f14a79
wip on table permissions
asutula Jul 8, 2024
795551f
updates to console table data display
asutula Jul 9, 2024
014e7da
account and permissions display, acl usage
asutula Jul 10, 2024
c49cae3
handle generating queries where there is no primary key
asutula Jul 10, 2024
dce279b
disable ui when txn is pending, properly reset data on router refresh
asutula Jul 10, 2024
2054a8b
acl display
asutula Jul 11, 2024
c8f8c29
return map for studio users
asutula Jul 11, 2024
da26c59
fix console freezing
asutula Jul 14, 2024
3f68cdb
use proper signer for table updates
asutula Jul 15, 2024
3c26a27
remove console log
asutula Jul 15, 2024
584a0ed
pr feedback changes
asutula Jul 17, 2024
71a2c75
use unique constraint in addition to pk for where clause calculation
asutula Jul 17, 2024
4a9c9c1
warning ui for no unique constraint
asutula Jul 17, 2024
7ee03a7
handle blob columns
asutula Jul 18, 2024
e639b67
styling fix in hashdisplay
asutula Jul 18, 2024
1b69050
css fix for hash display
asutula Jul 18, 2024
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
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/api/src/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { providersRouter } from "./routers/providers";
import { defsRouter } from "./routers/defs";
import { teamsRouter } from "./routers/teams";
import { tablesRouter } from "./routers/tables";
import { usersRouter } from "./routers/users";

export function appRouter(
store: Store,
Expand Down Expand Up @@ -43,6 +44,7 @@ export function appRouter(
infura: infuraKey,
quickNode: quickNodeKey,
}),
users: usersRouter(store),
});
}

Expand Down
21 changes: 21 additions & 0 deletions packages/api/src/routers/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type Store } from "@tableland/studio-store";
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../trpc";

export function usersRouter(store: Store) {
return createTRPCRouter({
usersForAddresses: publicProcedure
.input(z.object({ addresses: z.array(z.string().trim()).min(1) }))
.query(async ({ input }) => {
const users = await store.users.usersForAddresses(input.addresses);
const res = users.reduce<Map<string, (typeof users)[number]>>(
(acc, item) => {
acc.set(item.user.address, item);
return acc;
},
new Map<string, (typeof users)[number]>(),
);
return res;
}),
Comment on lines +7 to +19
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added this new users router with this function that, given a list of addresses, returns a map of Studio accounts (keyed by address). If an address in the input doesn't correspond to a Studio user, there will be no entry in the map for it. You'll see the store package implementaiton as well.

});
}
16 changes: 15 additions & 1 deletion packages/store/src/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { eq } from "drizzle-orm";
import { eq, inArray } from "drizzle-orm";
import { type DrizzleD1Database } from "drizzle-orm/d1";
import * as schema from "../schema/index.js";

const users = schema.users;
const teams = schema.teams;

export function initUsers(db: DrizzleD1Database<typeof schema>) {
return {
Expand All @@ -16,5 +17,18 @@ export function initUsers(db: DrizzleD1Database<typeof schema>) {

return user?.teamId;
},

usersForAddresses: async function (addresses: string[]) {
const res = await db
.select({
user: users,
team: teams,
})
.from(users)
.innerJoin(teams, eq(users.teamId, teams.id))
.where(inArray(users.address, addresses))
.all();
return res;
},
};
}
13 changes: 13 additions & 0 deletions packages/web/app/[team]/[project]/[env]/[table]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
} from "@/lib/api-helpers";
import DefDetails from "@/components/def-details";
import TableWrapper from "@/components/table-wrapper";
import {
getRegistryRecord,
type RegistryRecord,
} from "@/lib/validator-queries";

export default async function Deployments({
params,
Expand All @@ -37,6 +41,14 @@ export default async function Deployments({
}
}

let registryRecord: RegistryRecord | undefined;
if (deployment) {
registryRecord = await cache(getRegistryRecord)(
deployment.chainId,
deployment.tableId,
);
Comment on lines +46 to +49
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a card to show the table owner to the table page. This required a new validator query that reads information about the registry. This data is also used deeper into the view hierarchy for ACL related stuff.

}

const isAuthorized = await cache(api.teams.isAuthorized)({ teamId: team.id });

return (
Expand All @@ -59,6 +71,7 @@ export default async function Deployments({
tableName={deployment.tableName}
chainId={deployment.chainId}
tableId={deployment.tableId}
owner={registryRecord?.controller}
createdAt={new Date(deployment.createdAt)}
schema={def.schema}
environment={env}
Expand Down
11 changes: 8 additions & 3 deletions packages/web/app/_components/latest-tables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ import ChainSelector from "@/components/chain-selector";
import { TimeSince } from "@/components/time";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { type Table, getLatestTables } from "@/lib/validator-queries";
import { type RegistryRecord, getLatestTables } from "@/lib/validator-queries";

export function LatestTables({ initialData }: { initialData: Table[] }) {
const [latestTables, setLatestTables] = useState<Table[]>(initialData);
export function LatestTables({
initialData,
}: {
initialData: RegistryRecord[];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just updated the Table type name to RegistryRecord to better represent what it is and avoid using our already-overloaded word "table".

}) {
const [latestTables, setLatestTables] =
useState<RegistryRecord[]>(initialData);
const [selectedChain, setSelectedChain] = useState<
number | "mainnets" | "testnets"
>("testnets");
Expand Down
5 changes: 4 additions & 1 deletion packages/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import { getLatestTables, getPopularTables } from "@/lib/validator-queries";
import { api } from "@/trpc/server";

export default async function Page() {
const session = await getSession({ headers: headers(), cookies: cookies() });
const session = await cache(getSession)({
headers: headers(),
cookies: cookies(),
});
Comment on lines +27 to +30
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unrelated update, just noticed we should be caching this call so the results are reused during a single server side rendering pass.


let teams: RouterOutputs["teams"]["userTeams"] = [];
if (session.auth) {
Expand Down
5 changes: 5 additions & 0 deletions packages/web/app/table/[name]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { type Table as TblTable, Validator, helpers } from "@tableland/sdk";
import { getSession } from "@tableland/studio-api";
import { cookies, headers } from "next/headers";
import { unescapeSchema } from "@tableland/studio-store";
import { cache } from "react";
import Table from "@/components/table";
import TableWrapper from "@/components/table-wrapper";
import { ensureError } from "@/lib/ensure-error";
import { getRegistryRecord } from "@/lib/validator-queries";

export default async function TablePage({
params,
Expand Down Expand Up @@ -51,6 +53,8 @@ export default async function TablePage({
);
}

const registryRecord = await cache(getRegistryRecord)(chainId, tableId);

const schema = unescapeSchema(tablelandTable.schema);

const createdAttr = tablelandTable.attributes?.find(
Expand Down Expand Up @@ -80,6 +84,7 @@ export default async function TablePage({
schema={schema}
tableName={params.name}
tableId={tableId}
owner={registryRecord.controller}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just querying and passing the new prop for the table owner here in the page for a Tableland table out of the context of a project.

/>
</TableWrapper>
</main>
Expand Down
130 changes: 130 additions & 0 deletions packages/web/components/acl.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's a component that displays ACL information for a table. It's used inside a new tab in the table page.

Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
type ColumnDef,
getCoreRowModel,
useReactTable,
type Cell,
} from "@tanstack/react-table";
import { type RouterOutputs } from "@tableland/studio-api";
import { useEffect, useState } from "react";
import { Check } from "lucide-react";
import Link from "next/link";
import { DataTable } from "./data-table";
import HashDisplay from "./hash-display";
import { type ACLItem } from "@/lib/validator-queries";

type UserValue =
RouterOutputs["users"]["usersForAddresses"] extends Map<any, infer I>
? I
: never;
Comment on lines +15 to +18
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is gross and how you can infer the type of the values held inside a Map.


type TableRowData = ACLItem &
Partial<UserValue> & {
isOwner: boolean;
};

interface Props {
acl: ACLItem[];
authorizedStudioUsers: RouterOutputs["users"]["usersForAddresses"];
owner: string;
}

export default function ACL({ acl, authorizedStudioUsers, owner }: Props) {
const data: TableRowData[] = acl.map((item) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm deriving our table data here using map and realizing this goes against the freezing bug I found related to using derived data with react table. I think this works because the data doesn't change, and I now officially update my finding about that bug by adding: It's seems to be a problem when the data isn't react state AND is updated.

const studioUser = authorizedStudioUsers.get(item.controller);
const isOwner = item.controller === owner;
return {
...item,
...studioUser,
isOwner,
};
});

const columns: Array<ColumnDef<TableRowData>> = [
{
accessorKey: "controller",
header: "Address",
cell: AddressCell,
},
{
accessorFn: (row) => row.team?.name ?? "",
header: "Studio User",
cell: UserCell,
},
{
accessorKey: "isOwner",
header: "Table Owner",
cell: BoolCell,
},
{
accessorKey: "privileges.insert",
header: "Insert",
cell: BoolCell,
},
{
accessorKey: "privileges.update",
header: "Update",
cell: BoolCell,
},
{
accessorKey: "privileges.delete",
header: "Delete",
cell: BoolCell,
},
];

const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return <DataTable table={table} />;
}

function AddressCell({
getValue,
}: ReturnType<Cell<TableRowData, unknown>["getContext"]>) {
const initialValue = getValue<string>();

const [value, setValue] = useState(initialValue);

useEffect(() => {
setValue(initialValue);
}, [initialValue]);

return (
<HashDisplay hash={value} copy className="justify-start text-foreground" />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The address cell uses our HashDisplay component.

);
}

function BoolCell({
getValue,
}: ReturnType<Cell<TableRowData, unknown>["getContext"]>) {
const initialValue = getValue<boolean>();

const [value, setValue] = useState(initialValue);

useEffect(() => {
setValue(initialValue);
}, [initialValue]);

return value ? <Check /> : null;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The boolean cell display a check mark for true and nothing for false.

}

function UserCell({
getValue,
row,
}: ReturnType<Cell<TableRowData, unknown>["getContext"]>) {
const initialValue = getValue<string>();

const [value, setValue] = useState(initialValue);

useEffect(() => {
setValue(initialValue);
}, [initialValue]);

return row.original.team ? (
<Link href={`/${row.original.team.slug}`}>{value}</Link>
) : (
<span>{value}</span>
);
Comment on lines +125 to +129
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The user cell displays a link to that user's page.

}
20 changes: 10 additions & 10 deletions packages/web/components/console.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from "react";
import { Database, helpers } from "@tableland/sdk";
import { studioAliases, getBaseUrl } from "@tableland/studio-client";
import { init } from "@tableland/sqlparser";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { CodeEditor } from "./code-editor";
import { DataTable } from "./data-table";
import { cn, objectToTableData } from "@/lib/utils";
Expand Down Expand Up @@ -82,7 +83,7 @@ export function Console({ environmentId }: { environmentId: string }) {
tab.error = null;
tab.messages = [];
tab.columns = columns;
tab.results = data.results;
tab.results = objectToTableData(data.results);

// if there is a transactionHash it means that this is the response from a mutation
// the template rendering logic will key off the existence of messages
Expand Down Expand Up @@ -218,11 +219,7 @@ export function Console({ environmentId }: { environmentId: string }) {
loading={loading}
/>

<ResultSetPane
tab={tab}
results={tab.results}
loading={loading}
/>
<ResultSetPane tab={tab} loading={loading} />
</div>
);
})}
Expand Down Expand Up @@ -362,7 +359,12 @@ function TabLabel(props: {

function ResultSetPane(props: any): React.JSX.Element {
const { tab } = props;
const formattedData = objectToTableData(tab.results);

const table = useReactTable({
data: tab.results,
columns: tab.columns,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The react table lib needs the data as some sort of reactive state (prop, state, memo, etc). If you give it another object derived from some reactive state, like we were, you get that freeze up. I'm not sure what's it's doing internally that results in this behavior, but I was able to verify it using a simplified version of the console code I wrote and then made sure that fix worked here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This wasn't a problem previously because previous to my branch here, the data was passed as a prop to the DataTable component which internally passed that data prop to useReactTable. We now must call useReactTable outside DataTable and pass the resulting table into DataTable as a prop.

getCoreRowModel: getCoreRowModel(),
});

return (
<div className="table-results">
Expand All @@ -379,9 +381,7 @@ function ResultSetPane(props: any): React.JSX.Element {
})}
</div>
)}
{!tab.error && !tab.messages?.length && (
<DataTable columns={tab.columns} data={formattedData} />
)}
{!tab.error && !tab.messages?.length && <DataTable table={table} />}
</div>
);
}
Loading
Loading