diff --git a/public/static/main.wasm b/public/static/main.wasm index 1d59dec..59434d1 100755 --- a/public/static/main.wasm +++ b/public/static/main.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4a79b27c3497d77cc6db7caf4f68d7de675837fedbb512f99861f3e342371fd -size 58051129 +oid sha256:6191707f87362d2cf4882d780faead632b7a2ccefe6339c1f42ddf1634b01689 +size 58228983 diff --git a/public/static/zed.wasm b/public/static/zed.wasm index 3f79069..e019d19 100755 --- a/public/static/zed.wasm +++ b/public/static/zed.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4af7789400c49a72f0b20fee9a4f0fa355492a5e0096dc337174f3d25523e0fc -size 64685206 +oid sha256:b9547dab1a5a61db646d6a4ef20f3277d8d4684f53085d177a494791a1960294 +size 64894237 diff --git a/src/spicedb-common/components/relationshipeditor/RelationshipEditor.tsx b/src/spicedb-common/components/relationshipeditor/RelationshipEditor.tsx index d6716b5..50038ac 100644 --- a/src/spicedb-common/components/relationshipeditor/RelationshipEditor.tsx +++ b/src/spicedb-common/components/relationshipeditor/RelationshipEditor.tsx @@ -1,8 +1,3 @@ -import { - ParseRelationshipError, - parseRelationshipsWithComments, - parseRelationshipWithError, -} from "../../parsing"; import DataEditor, { CompactSelection, EditableGridCell, @@ -37,13 +32,18 @@ import { useMemo, useRef, useState, - type ReactNode, type KeyboardEvent, + type ReactNode, } from "react"; import { useCookies } from "react-cookie"; import { ThemeProvider } from "styled-components"; import { useDeepCompareEffect, useDeepCompareMemo } from "use-deep-compare"; import { Resolver } from "../../parsers/dsl/resolution"; +import { + ParseRelationshipError, + parseRelationshipsWithComments, + parseRelationshipWithError, +} from "../../parsing"; import { RelationTuple as Relationship } from "../../protodefs/core/v1/core"; import { useRelationshipsService } from "../../services/relationshipsservice"; import { @@ -81,6 +81,8 @@ import { CaveatContextCell, CAVEATNAME_CELL_KIND, CaveatNameCell, + EXPIRATION_CELL_KIND, + ExpirationCell, OBJECTID_CELL_KIND, ObjectIdCell, RELATION_CELL_KIND, @@ -216,7 +218,7 @@ export function RelationshipEditor(props: RelationshipEditorProps) { relData.subjectRelation ? `#${relData.subjectRelation}` : "" }${ relData.caveatName ? `[${relData.caveatName}${caveatContext}]` : "" - }`; + }${relData.expiration ? `[expiration:${relData.expiration}]` : ""}`; return parseRelationshipWithError(str); }) .filter((value: Relationship | ParseRelationshipError) => { @@ -571,6 +573,19 @@ export function RelationshipEditor(props: RelationshipEditorProps) { copyData: data[row].columnData[col], } as CaveatContextCell; + case DataKind.EXPIRATION: + return { + kind: GridCellKind.Custom, + data: { + kind: EXPIRATION_CELL_KIND, + row: row, + col: col, + dataValue: data[row].columnData[col], + }, + allowOverlay: true, + copyData: data[row].columnData[col], + } as ExpirationCell; + default: return { kind: GridCellKind.Text, diff --git a/src/spicedb-common/components/relationshipeditor/columns.ts b/src/spicedb-common/components/relationshipeditor/columns.ts index e3a42d7..eb5fdc4 100644 --- a/src/spicedb-common/components/relationshipeditor/columns.ts +++ b/src/spicedb-common/components/relationshipeditor/columns.ts @@ -20,6 +20,7 @@ export enum DataKind { SUBJECT_RELATION = 6, CAVEAT_NAME = 7, CAVEAT_CONTEXT = 8, + EXPIRATION = 9, } export type DataValidator = RegExp | ((input: string) => boolean); @@ -57,6 +58,14 @@ export const DataRegex: Record = { return false; } }, + [DataKind.EXPIRATION]: (input: string) => { + try { + Date.parse(input); + return true; + } catch { + return false; + } + }, }; export const DataTitle: Record = { @@ -69,6 +78,7 @@ export const DataTitle: Record = { [DataKind.SUBJECT_RELATION]: "", [DataKind.CAVEAT_NAME]: "", [DataKind.CAVEAT_CONTEXT]: "", + [DataKind.EXPIRATION]: "", }; export enum RelationshipSection { @@ -76,6 +86,7 @@ export enum RelationshipSection { RESOURCE = 1, SUBJECT = 2, CAVEAT = 3, + EXPIRATION = 4, } export type Column = SizedGridColumn & { @@ -167,4 +178,13 @@ export const COLUMNS: Column[] = [ section: RelationshipSection.CAVEAT, dataDescription: "JSON", }, + { + title: "Date/Time", + id: "expiration", + width: DEFAULT_COLUMN_WIDTH, + group: "Expiration (optional)", + dataKind: DataKind.EXPIRATION, + section: RelationshipSection.EXPIRATION, + dataDescription: "datetime", + }, ]; diff --git a/src/spicedb-common/components/relationshipeditor/customcells.ts b/src/spicedb-common/components/relationshipeditor/customcells.ts index 4713ed1..f503892 100644 --- a/src/spicedb-common/components/relationshipeditor/customcells.ts +++ b/src/spicedb-common/components/relationshipeditor/customcells.ts @@ -16,6 +16,8 @@ import { CaveatContextCellRenderer, CaveatNameCell, CaveatNameCellRenderer, + ExpirationCell, + ExpirationCellRenderer, ObjectIdCell, ObjectIdCellRenderer, RelationCell, @@ -33,7 +35,8 @@ export type RelEditorCustomCell = | ObjectIdCell | RelationCell | CaveatNameCell - | CaveatContextCell; + | CaveatContextCell + | ExpirationCell; // Copied from: https://github.com/glideapps/glide-data-grid/blob/6b0a04f9d6550378890580b4db1e1168e4268c54/packages/cells/src/index.ts#L12 export type DrawCallback = NonNullable; @@ -179,6 +182,28 @@ export function useCustomCells( return { caveatcontext: annotatedData[row].columnData[col] }; }, [gridSelection, annotatedData]); + const selectedExpiration = useMemo(() => { + if (!gridSelection?.current?.cell) { + return undefined; + } + + const [col, row] = gridSelection.current.cell; + if (row >= annotatedData.length) { + return undefined; + } + + if (col >= COLUMNS.length) { + return undefined; + } + + const dataKind = COLUMNS[col].dataKind; + if (dataKind !== DataKind.EXPIRATION) { + return undefined; + } + + return { expiration: annotatedData[row].columnData[col] }; + }, [gridSelection, annotatedData]); + const props = useRef({ relationshipsService: relationshipsService, annotatedData: annotatedData, @@ -190,6 +215,7 @@ export function useCustomCells( selectedRelation: selectedRelation, selectedCaveatName: selectedCaveatName, selectedCaveatContext: selectedCaveatContext, + selectedExpiration: selectedExpiration, }, similarHighlighting: similarHighlighting, columnsWithWidths: columnsWithWidths, @@ -208,6 +234,7 @@ export function useCustomCells( selectedRelation: selectedRelation, selectedCaveatName: selectedCaveatName, selectedCaveatContext: selectedCaveatContext, + selectedExpiration: selectedExpiration, }, similarHighlighting: similarHighlighting, columnsWithWidths: columnsWithWidths, @@ -222,6 +249,7 @@ export function useCustomCells( RelationCellRenderer(props), CaveatNameCellRenderer(props), CaveatContextCellRenderer(props), + ExpirationCellRenderer(props), ]; }, []); diff --git a/src/spicedb-common/components/relationshipeditor/data.ts b/src/spicedb-common/components/relationshipeditor/data.ts index bc53cce..0786a3d 100644 --- a/src/spicedb-common/components/relationshipeditor/data.ts +++ b/src/spicedb-common/components/relationshipeditor/data.ts @@ -27,6 +27,7 @@ export type PartialRelationship = { subjectRelation: string; caveatName: string; caveatContext: string; + expiration: string; }; /** @@ -94,6 +95,7 @@ export function emptyAnnotatedDatum( subjectRelation: "", caveatName: "", caveatContext: "", + expiration: "", }, index, ); @@ -143,11 +145,14 @@ export function toPartialRelationshipString( const caveat = annotated.caveatName ? `[${annotated.caveatName}${caveatContext}]` : ""; + const expiration = annotated.expiration + ? `[expiration:${annotated.expiration}]` + : ""; return `${annotated.resourceType}:${annotated.resourceId}#${ annotated.relation }@${annotated.subjectType}:${annotated.subjectId}${ annotated.subjectRelation ? `#${annotated.subjectRelation}` : "" - }${caveat}`; + }${caveat}${expiration}`; } /** @@ -181,6 +186,7 @@ export function getColumnData(datum: RelationshipDatum) { datum.subjectRelation ?? "", datum.caveatName ?? "", datum.caveatContext ?? "", + datum.expiration ?? "", ]; return colData; @@ -206,6 +212,11 @@ export function relationshipToDatum(rel: Relationship): PartialRelationship { caveatContext: rel.caveat?.context ? Struct.toJsonString(rel.caveat?.context) : "", + expiration: rel.optionalExpirationTime + ? new Date(parseFloat(rel.optionalExpirationTime.seconds) * 1000) + .toISOString() + .replace(".000", "") + : "", }; } @@ -228,6 +239,7 @@ function fromColumnData(columnData: ColumnData): PartialRelationship | Comment { subjectRelation: columnData[5] ?? "", caveatName: columnData[6] ?? "", caveatContext: columnData[7] ?? "", + expiration: columnData[8] ?? "", }; } @@ -257,6 +269,11 @@ export function relationshipToColumnData( userRel, relationship.caveat?.caveatName ?? "", caveatContext, + relationship.optionalExpirationTime + ? new Date(parseFloat(relationship.optionalExpirationTime.seconds) * 1000) + .toISOString() + .replace(".000", "") + : "", ]; if (columnData.length !== COLUMNS.length) { diff --git a/src/spicedb-common/components/relationshipeditor/fieldcell.tsx b/src/spicedb-common/components/relationshipeditor/fieldcell.tsx index a52818f..7840dd3 100644 --- a/src/spicedb-common/components/relationshipeditor/fieldcell.tsx +++ b/src/spicedb-common/components/relationshipeditor/fieldcell.tsx @@ -22,6 +22,7 @@ export const OBJECTID_CELL_KIND = "objectid-field-cell"; export const RELATION_CELL_KIND = "relation-field-cell"; export const CAVEATNAME_CELL_KIND = "caveatname-field-cell"; export const CAVEATCONTEXT_CELL_KIND = "caveatcontext-field-cell"; +export const EXPIRATION_CELL_KIND = "expiration-field-cell"; interface FieldCellProps { readonly dataValue: string; @@ -54,11 +55,17 @@ type CaveatContextCellProps = FieldCellProps & { readonly readonly: false; }; +type ExpirationCellProps = FieldCellProps & { + readonly kind: "expiration-field-cell"; + readonly readonly: false; +}; + export type TypeCell = CustomCell; export type ObjectIdCell = CustomCell; export type RelationCell = CustomCell; export type CaveatNameCell = CustomCell; export type CaveatContextCell = CustomCell; +export type ExpirationCell = CustomCell; type SelectedType = { type: string; @@ -72,6 +79,8 @@ type SelectedCaveatName = SelectedRelation & { caveatname: string }; type SelectedCaveatContext = { caveatcontext: string }; +type SelectedExpiration = { expiration: string }; + export interface FieldCellRendererProps { relationshipsService: RelationshipsService; annotatedData: AnnotatedData; @@ -83,6 +92,7 @@ export interface FieldCellRendererProps { selectedRelation: SelectedRelation | undefined; selectedCaveatName: SelectedCaveatName | undefined; selectedCaveatContext: SelectedCaveatContext | undefined; + selectedExpiration: SelectedExpiration | undefined; }; similarHighlighting: boolean; columnsWithWidths: Column[]; @@ -208,6 +218,9 @@ function fieldCellRenderer, Q extends FieldCellProps>( case DataKind.CAVEAT_CONTEXT: // No highlighting for context values break; + case DataKind.EXPIRATION: + // No highlighting for expiration + break; } } @@ -451,3 +464,11 @@ export const CaveatContextCellRenderer = fieldCellRenderer< // No autocomplete support return []; }); + +export const ExpirationCellRenderer = fieldCellRenderer< + ExpirationCell, + ExpirationCellProps +>(EXPIRATION_CELL_KIND, () => { + // No autocomplete support + return []; +}); diff --git a/src/spicedb-common/lang/dslang.ts b/src/spicedb-common/lang/dslang.ts index 07b76fe..f239204 100644 --- a/src/spicedb-common/lang/dslang.ts +++ b/src/spicedb-common/lang/dslang.ts @@ -1,7 +1,7 @@ +import * as monacoEditor from "monaco-editor"; +import { Position, editor, languages } from "monaco-editor"; import { findReferenceNode, parse } from "../parsers/dsl/dsl"; import { ResolvedReference, Resolver } from "../parsers/dsl/resolution"; -import { Position, editor, languages } from "monaco-editor"; -import * as monacoEditor from "monaco-editor"; export const DS_LANGUAGE_NAME = "dsl"; export const DS_THEME_NAME = "dsl-theme"; @@ -95,6 +95,7 @@ export default function registerDSLanguage(monaco: typeof monacoEditor) { /caveat/, { token: "keyword.caveat", bracket: "@open", next: "@caveat" }, ], + [/use expiration$/, { token: "keyword.expiration" }], [ /permission/, { @@ -282,6 +283,9 @@ export default function registerDSLanguage(monaco: typeof monacoEditor) { ], [/\w+#/, { token: "@rematch", next: "@relationref" }], [/\w+:/, { token: "@rematch", next: "@wildcardref" }], + [/expiration/, { token: "keyword.expiration" }], + [/and/, { token: "keyword.and" }], + [/with/, { token: "keyword.with" }], [/\w+/, { token: "type.identifier" }], { include: "@whitespace" }, ], @@ -607,6 +611,7 @@ export default function registerDSLanguage(monaco: typeof monacoEditor) { { token: "identifier.relorperm", foreground: "666666" }, + { token: "keyword.expiration", foreground: "ddaaaa" }, { token: "keyword.permission", foreground: "158a64" }, { token: "keyword.relation", foreground: "883425" }, { token: "keyword.definition", foreground: "4242ff" }, @@ -652,6 +657,7 @@ export default function registerDSLanguage(monaco: typeof monacoEditor) { { token: "identifier.relorperm", foreground: "cccccc" }, + { token: "keyword.expiration", foreground: "ddaaaa" }, { token: "keyword.permission", foreground: "1acc92" }, { token: "keyword.relation", foreground: "ffa887" }, { token: "keyword.definition", foreground: "8787ff" }, diff --git a/src/wasm-config.json b/src/wasm-config.json index 97c61d9..49b9421 100644 --- a/src/wasm-config.json +++ b/src/wasm-config.json @@ -1,4 +1,4 @@ { - "spicedb": "v1.37.1", - "zed": "v0.22.0" + "spicedb": "v1.40.0", + "zed": "v0.25.0" }