-
Notifications
You must be signed in to change notification settings - Fork 1
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
Editable table data #295
Changes from all commits
c1cd9f3
550cae1
f735a17
383fb6b
6797b3d
6648adc
4f76ffd
4cd70e1
3c7163c
7f14a79
795551f
014e7da
c49cae3
dce279b
2054a8b
c8f8c29
da26c59
3f68cdb
3c26a27
584a0ed
71a2c75
4a9c9c1
7ee03a7
e639b67
1b69050
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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; | ||
}), | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ( | ||
|
@@ -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} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just updated the |
||
}) { | ||
const [latestTables, setLatestTables] = | ||
useState<RegistryRecord[]>(initialData); | ||
const [selectedChain, setSelectedChain] = useState< | ||
number | "mainnets" | "testnets" | ||
>("testnets"); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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( | ||
|
@@ -80,6 +84,7 @@ export default async function TablePage({ | |
schema={schema} | ||
tableName={params.name} | ||
tableId={tableId} | ||
owner={registryRecord.controller} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm deriving our table data here using |
||
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" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The address cell uses our |
||
); | ||
} | ||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The boolean cell display a check mark for |
||
} | ||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The user cell displays a link to that user's page. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
|
@@ -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 | ||
|
@@ -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> | ||
); | ||
})} | ||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
getCoreRowModel: getCoreRowModel(), | ||
}); | ||
|
||
return ( | ||
<div className="table-results"> | ||
|
@@ -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> | ||
); | ||
} |
There was a problem hiding this comment.
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.