diff --git a/docs/content/modeling/agents/agents-as-principals.mdx b/docs/content/modeling/agents/agents-as-principals.mdx index 1189572261..9e22132b6b 100644 --- a/docs/content/modeling/agents/agents-as-principals.mdx +++ b/docs/content/modeling/agents/agents-as-principals.mdx @@ -100,6 +100,7 @@ Check whether the agent can read a specific issue. The agent's project membershi object={'issue:issue-123'} allowed={true} skipSetup={true} + pseudoCodeMode={true} /> The agent cannot delete the issue because `can_delete` requires `reporter` or project-level `can_delete` (which requires `owner` or `admin`): @@ -110,6 +111,7 @@ The agent cannot delete the issue because `can_delete` requires `reporter` or pr object={'issue:issue-123'} allowed={false} skipSetup={true} + pseudoCodeMode={true} /> ## Direct assignment for fine-grained control diff --git a/src/components/Docs/SnippetViewer/BatchCheckRequestViewer.tsx b/src/components/Docs/SnippetViewer/BatchCheckRequestViewer.tsx index a4f760d844..ee72e177c3 100644 --- a/src/components/Docs/SnippetViewer/BatchCheckRequestViewer.tsx +++ b/src/components/Docs/SnippetViewer/BatchCheckRequestViewer.tsx @@ -1,5 +1,5 @@ import { getFilteredAllowedLangs, SupportedLanguage, DefaultAuthorizationModelId } from './SupportedLanguage'; -import { defaultOperationsViewer } from './DefaultTabbedViewer'; +import { defaultOperationsViewer, type DefaultTabbedViewerOpts } from './DefaultTabbedViewer'; import assertNever from 'assert-never/index'; import { TupleKey } from '@openfga/sdk'; @@ -13,10 +13,9 @@ interface Check { // eslint-disable-next-line @typescript-eslint/no-explicit-any context?: Record; } -interface BatchCheckRequestViewerOpts { +interface BatchCheckRequestViewerOpts extends DefaultTabbedViewerOpts { authorizationModelId?: string; checks: Check[]; - skipSetup?: boolean; allowedLanguages?: SupportedLanguage[]; } @@ -32,8 +31,7 @@ function batchCheckRequestViewer(lang: SupportedLanguage, opts: BatchCheckReques throw new Error('Batch check is not supported in the CLI'); case SupportedLanguage.JS_SDK: - return `// Requires >=v0.8.0 for the server side BatchCheck, earlier versions support a client-side BatchCheck with a slightly different interface -const body = { + return `const body = { checks: [ ${checks .map( @@ -108,8 +106,7 @@ const { result } = await fgaClient.batchCheck(body, options); */`; case SupportedLanguage.GO_SDK: - return `// Requires >=v0.7.0 for the server side BatchCheck, earlier versions support a client-side BatchCheck with a slightly different interface -body := ClientBatchCheckRequest{ + return `body := ClientBatchCheckRequest{ Checks: []ClientBatchCheckItem{${checks .map( (check) => ` @@ -249,9 +246,7 @@ response.Result = [${checks `; case SupportedLanguage.PYTHON_SDK: - return `# Requires >=v0.9.0 for the server side BatchCheck, earlier versions support a client-side BatchCheck with a slightly different interface - -checks = [${checks + return `checks = [${checks .map( (check) => ` ClientBatchCheckItem( @@ -298,7 +293,7 @@ response = await fga_client.batch_check(ClientBatchCheckRequest(checks=checks), .join(', ')}]`; case SupportedLanguage.JAVA_SDK: - return ` // Requires >=v0.8.0 for the server side BatchCheck, earlier versions support a client-side BatchCheck with a slightly different interface + return ` var request = new ClientBatchCheckRequest().checks( List.of( ${checks diff --git a/src/components/Docs/SnippetViewer/CheckRequestViewer.tsx b/src/components/Docs/SnippetViewer/CheckRequestViewer.tsx index e39ff745b8..070a3545ec 100644 --- a/src/components/Docs/SnippetViewer/CheckRequestViewer.tsx +++ b/src/components/Docs/SnippetViewer/CheckRequestViewer.tsx @@ -14,6 +14,7 @@ interface CheckRequestViewerOpts { // eslint-disable-next-line @typescript-eslint/no-explicit-any context?: Record; skipSetup?: boolean; + pseudoCodeMode?: boolean; allowedLanguages?: SupportedLanguage[]; // Optional custom headers @@ -256,12 +257,22 @@ response = await fga_client.check(body, options) return `check( user = "${user}", // check if the user \`${user}\` relation = "${relation}", // has an \`${relation}\` relation - object = "${object}", // with the object \`${object}\` - ${ + object = "${object}", // with the object \`${object}\`${ + headers && Object.keys(headers).length > 0 + ? ` + headers = { ${Object.entries(headers) + .map(([key, value]) => `"${key}" = "${value}"`) + .join(', ')} },` + : '' + }${ contextualTuples - ? `contextual_tuples = [ // Assuming the following is true + ? ` + contextual_tuples = [ // Assuming the following is true ${contextualTuples - .map((tuple) => `{user = "${tuple.user}", relation = "${tuple.relation}", object = "${tuple.object}"}`) + .map( + (tuple) => + `{\n user = "${tuple.user}",\n relation = "${tuple.relation}",\n object = "${tuple.object}",\n }`, + ) .join(',\n ')} ],` : '' @@ -272,13 +283,6 @@ response = await fga_client.check(body, options) .map(([k, v]) => `${k} = "${v}"`) .join(', ')} },` : '' - } authorization_id = "${modelId}"${ - headers && Object.keys(headers).length > 0 - ? ` - extra_headers = { ${Object.entries(headers) - .map(([k, v]) => `"${k}": "${v}"`) - .join(', ')} },` - : '' } ); diff --git a/src/components/Docs/SnippetViewer/DefaultTabbedViewer.tsx b/src/components/Docs/SnippetViewer/DefaultTabbedViewer.tsx index d2ec8d675f..b04cf27f40 100644 --- a/src/components/Docs/SnippetViewer/DefaultTabbedViewer.tsx +++ b/src/components/Docs/SnippetViewer/DefaultTabbedViewer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; @@ -8,6 +8,62 @@ import { GenerateSetupHeader, LanguageWrapper } from './SdkSetup'; export interface DefaultTabbedViewerOpts { skipSetup?: boolean; + pseudoCodeMode?: boolean; +} + +function SdkToggle({ + allowedLanguages, + opts, + tabViewFn, + langMappings, +}: { + allowedLanguages: SupportedLanguage[]; + opts: T; + tabViewFn: (lang: SupportedLanguage, opts: T, langMappings: LanguageMappings) => string; + langMappings: LanguageMappings; +}): JSX.Element { + const [showSdk, setShowSdk] = useState(false); + + const sdkLanguages = allowedLanguages.filter((language) => language !== SupportedLanguage.RPC); + const toggleLabel = showSdk ? 'View pseudocode' : 'View code'; + + return ( +
+
+ +
+ {showSdk ? ( + sdkLanguages.length > 0 && ( + + {sdkLanguages.map((allowedLanguage) => ( + + {GenerateSetupHeader(allowedLanguage, opts.skipSetup)} + {LanguageWrapper({ + allowedLanguage: allowedLanguage, + content: tabViewFn(allowedLanguage, opts, langMappings), + })} + + ))} + + ) + ) : ( +
+ {LanguageWrapper({ + allowedLanguage: SupportedLanguage.RPC, + content: tabViewFn(SupportedLanguage.RPC, opts, langMappings), + })} +
+ )} +
+ ); } export function defaultOperationsViewer( @@ -16,9 +72,33 @@ export function defaultOperationsViewer( tabViewFn: (lang: SupportedLanguage, opts: T, langMappings: LanguageMappings) => string, ): JSX.Element { const { siteConfig } = useDocusaurusContext(); - const configuredLanguage = siteConfig.customFields.languageMapping as LanguageMappings; + const configuredLanguageMapping = siteConfig.customFields?.languageMapping; + + if (!configuredLanguageMapping) { + throw new Error('Missing required siteConfig.customFields.languageMapping configuration.'); + } + + const configuredLanguage = configuredLanguageMapping as LanguageMappings; + + const pseudoCodeMode = opts.pseudoCodeMode ?? false; + const hasRpc = allowedLanguages.includes(SupportedLanguage.RPC); + const hasSdkLanguages = allowedLanguages.some((language) => language !== SupportedLanguage.RPC); + + if (pseudoCodeMode && hasRpc && hasSdkLanguages) { + return ( +
+ +
+ ); + } + return ( - <> +
{allowedLanguages.map((allowedLanguage) => ( @@ -30,6 +110,6 @@ export function defaultOperationsViewer( ))} - +
); } diff --git a/src/components/Docs/SnippetViewer/ListObjectsRequestViewer.tsx b/src/components/Docs/SnippetViewer/ListObjectsRequestViewer.tsx index 23064930ec..1e1eac4595 100644 --- a/src/components/Docs/SnippetViewer/ListObjectsRequestViewer.tsx +++ b/src/components/Docs/SnippetViewer/ListObjectsRequestViewer.tsx @@ -14,6 +14,7 @@ interface ListObjectsRequestViewerOpts { context?: Record; expectedResults: string[]; skipSetup?: boolean; + pseudoCodeMode?: boolean; allowedLanguages?: SupportedLanguage[]; } @@ -206,8 +207,7 @@ response = await fga_client.list_objects(body, options) return `listObjects( "${user}", // list the objects that the user \`${user}\` "${relation}", // has an \`${relation}\` relation - "${objectType}", // and that are of type \`${objectType}\` - authorization_model_id = "${modelId}", // for this particular authorization model id ${ + "${objectType}", // and that are of type \`${objectType}\` ${ contextualTuples ? ` contextual_tuples = [ // Assuming the following is true diff --git a/src/components/Docs/SnippetViewer/ListUsersRequestViewer.tsx b/src/components/Docs/SnippetViewer/ListUsersRequestViewer.tsx index 2e8f87ed87..e5bbe59fae 100644 --- a/src/components/Docs/SnippetViewer/ListUsersRequestViewer.tsx +++ b/src/components/Docs/SnippetViewer/ListUsersRequestViewer.tsx @@ -16,6 +16,7 @@ interface ListUsersRequestViewerOpts { context?: Record; expectedResults: ListUsersResponse; skipSetup?: boolean; + pseudoCodeMode?: boolean; allowedLanguages?: SupportedLanguage[]; } @@ -263,8 +264,7 @@ var response = await fgaClient.ListUsers(body, options); return `listUsers( user_filter=[ "${userFilterType}" ], // list users of type \`${userFilterType}\` "${relation}", // that have the \`${relation}\` relation - "${objectType}:${objectId}", // for the object \`${objectType}:${objectId}\` - authorization_model_id = "${modelId}", // for this particular authorization model id ${ + "${objectType}:${objectId}", // for the object \`${objectType}:${objectId}\` ${ contextualTuples ? ` contextual_tuples = [ // Assuming the following is true diff --git a/src/components/Docs/SnippetViewer/WriteRequestViewer.tsx b/src/components/Docs/SnippetViewer/WriteRequestViewer.tsx index 2a143aa4c4..2f51e38401 100644 --- a/src/components/Docs/SnippetViewer/WriteRequestViewer.tsx +++ b/src/components/Docs/SnippetViewer/WriteRequestViewer.tsx @@ -24,6 +24,7 @@ interface WriteRequestViewerOpts { deleteRelationshipTuples: RelationshipTupleWithoutCondition[]; isDelete?: boolean; skipSetup?: boolean; + pseudoCodeMode?: boolean; allowedLanguages?: SupportedLanguage[]; conflictOptions?: { onDuplicateWrites?: 'error' | 'ignore'; @@ -439,8 +440,8 @@ response = await fga_client.write(body, options) }`, ) .join(','); - const writes = `write([${writeTuples}\n], authorization_model_id="${modelId}")`; - const deletes = `delete([${deleteTuples}\n], authorization_model_id="${modelId}")`; + const writes = `write([${writeTuples}\n])`; + const deletes = `delete([${deleteTuples}\n])`; const separator = `${opts.deleteRelationshipTuples && opts.relationshipTuples ? ',' : ''}`; return `${opts.relationshipTuples ? writes : ''}${separator} diff --git a/src/css/custom.css b/src/css/custom.css index d855cfbaac..334f90dbdf 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -89,6 +89,59 @@ table td { order: 2; } +.snippet-mode-toggle-wrapper { + position: relative; +} + +.snippet-mode-toggle { + position: absolute; + right: 0; + top: 0.85rem; + z-index: 2; +} + +.snippet-mode-toggle__button { + border: 0; + border-bottom: 1px solid transparent; + border-radius: 0; + background: transparent; + color: var(--ifm-font-color-base); + cursor: pointer; + font: inherit; + font-size: 0.9rem; + font-weight: 700; + line-height: 1; + letter-spacing: 0.01em; + opacity: 0.78; + padding: 0.2rem 0; + transition: + border-color 0.2s ease, + color 0.2s ease, + opacity 0.2s ease; +} + +.snippet-mode-toggle__button:focus-visible { + outline: 2px solid color-mix(in srgb, var(--ifm-font-color-base), transparent 35%); + outline-offset: 2px; +} + +.snippet-mode-toggle__button:hover { + color: var(--ifm-font-color-base); + border-color: currentColor; + opacity: 1; +} + +[data-theme='light'] .snippet-mode-toggle__button { + color: var(--ifm-font-color-base); + opacity: 0.72; +} + +[data-theme='light'] .snippet-mode-toggle__button:hover { + color: var(--ifm-font-color-base); + border-color: currentColor; + opacity: 0.9; +} + /* Mobile Layout */ @media (max-width: 996px) {