Skip to content

Commit

Permalink
Improve the "protecting your first app" guide
Browse files Browse the repository at this point in the history
- Add a warning around use of zedtokens or consistency
- Implement a handler for adjusting code samples based on entered endpoint, tenant, token etc
  • Loading branch information
josephschorr committed Feb 3, 2022
1 parent 470837d commit 483a85e
Show file tree
Hide file tree
Showing 8 changed files with 2,738 additions and 2,465 deletions.
529 changes: 267 additions & 262 deletions docs/guides/first-app.md → docs/guides/first-app.mdx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ module.exports = {
},
},
plugins: [
'./webpack-fallbacks-plugin',
[
'@docusaurus/plugin-client-redirects',
{
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@
"clear": "docusaurus clear"
},
"dependencies": {
"@docusaurus/core": "2.0.0-beta.8",
"@docusaurus/module-type-aliases": "^2.0.0-beta.8",
"@docusaurus/plugin-client-redirects": "^2.0.0-beta.8",
"@docusaurus/preset-classic": "2.0.0-beta.8",
"@docusaurus/core": "^2.0.0-beta.15",
"@docusaurus/module-type-aliases": "^2.0.0-beta.15",
"@docusaurus/plugin-client-redirects": "^2.0.0-beta.15",
"@docusaurus/preset-classic": "^2.0.0-beta.15",
"@mdx-js/react": "^1.6.21",
"amplitude-js": "^8.9.1",
"clsx": "^1.1.1",
"react": "^17.0.2",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.2",
"string.prototype.replaceall": "^1.0.6"
"string.prototype.replaceall": "^1.0.6",
"twig": "^1.15.4"
},
"resolutions": {
"*/ua-parser-js": "0.7.24"
Expand Down
188 changes: 188 additions & 0 deletions src/components/SampleConfigProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import React, { PropsWithChildren, useContext, useMemo, useState } from "react";
import { CookiesProvider, useCookies } from 'react-cookie';
import * as replaceAll from 'string.prototype.replaceall';
import CodeBlock from '@theme/CodeBlock';
import Twig from 'twig';
import { useDebouncedChecker } from "./debouncer";

enum InstanceKind {
AUTHZED = 'authzed',
SPICEDB = 'spicedb',
}

type SampleConfigContextValue = {
readonly defaultTenant: string,
readonly instanceKind: () => InstanceKind | undefined,
readonly setInstanceKind: (kind: InstanceKind) => void,
readonly token: () => string | undefined;
readonly setToken: (token: string) => void;
readonly tenant: () => string | undefined;
readonly setTenant: (token: string) => void;
readonly endpoint: () => string | undefined;
readonly setEndpoint: (token: string) => void;
readonly buildTempalte: (template: string) => any;
};

const SampleConfigContext = React.createContext<SampleConfigContextValue | undefined>(undefined);
const DEFAULT_SAMPLE_TOKEN = 't_your_token_here_1234567deadbeef';
const DEFAULT_ENDPOINT = 'localhost:50051';
const AUTHZED_ENDPOINT = 'grpc.authzed.com:443';

/**
* SampleConfigProvider is a React context provider which holds state for SampleCodeBlocks.
*/
export function SampleConfigProvider(props: PropsWithChildren<{ defaultTenant: string }>): JSX.Element {
const [tenant, setTenant] = useState<string | undefined>(undefined);
const [token, setToken] = useState<string | undefined>(undefined);
const [endpoint, setEndpoint] = useState<string | undefined>(undefined);
const [instanceKind, setInstanceKind] = useState<InstanceKind | undefined>(undefined);
const [templateCache, setTemplateCache] = useState<Record<string, any>>({});

const contextValue = useMemo(() => {
return {
defaultTenant: props.defaultTenant,
token: () => token,
setToken: setToken,
instanceKind: () => instanceKind,
setInstanceKind: setInstanceKind,
tenant: () => tenant,
setTenant: setTenant,
endpoint: () => endpoint,
setEndpoint: setEndpoint,
buildTemplate: (template: string) => {
if (template in templateCache) {
return templateCache[template];
}

const built = Twig.twig({
data: template
});
templateCache[template] = built;
setTemplateCache(templateCache);
return built;
}
}
}, [props.defaultTenant, token, setToken, endpoint, setEndpoint, instanceKind, setInstanceKind, tenant, setTenant, templateCache, setTemplateCache]);

return (
<SampleConfigContext.Provider value={contextValue}>
{props.children}
</SampleConfigContext.Provider>
);
}

function normalizeContent(children: React.ReactNode[]): string {
return Array.isArray(children)
? children.map((child: React.ReactNode) => {
if (React.isValidElement(child)) {
return normalizeContent(child.props.children);
} else {
return child.toString()
}
}).join('')
: (children as string);
}

/**
* SampleCodeBlock is a component which takes as its child a single *twig* template
* string, to produce a CodeBlock with the values found in the SampleConfigContext.
*
* Exmaple:
* <SampleCodeBlock lang="java">
* {`class SomeClass {
* ·
* private int someField = 2;
* ·
* void DoSomething() { ... }
* }`}
* </SampleCodeBlock>
*
* NOTE the use of · on the otherwise blanks lines to ensure MDX properly parses the template
* string.
*/
export function SampleCodeBlock(props: PropsWithChildren<{ lang: string }>) {
const context = useContext(SampleConfigContext);
const token = context.token() ? context.token() : DEFAULT_SAMPLE_TOKEN;
const endpoint = context.instanceKind() !== InstanceKind.SPICEDB ? AUTHZED_ENDPOINT : (context.endpoint() ?? DEFAULT_ENDPOINT);

let tenant = context.tenant() ? context.tenant() : context.defaultTenant;
tenant = tenant.replace('/', '');

let content = normalizeContent(props.children);

// NOTE: due to a bug in MDX, a completely blank line is interpreted as breaking up
// the template string typically passed to SampleCodeBlock. Therefore, we support ·
// for otherwise blank links, to allow for MDX parsing.
content = replaceAll(content, '·', '');

const template = context.buildTemplate(content);
const processed = template.render({ token: token, endpoint: endpoint, tenant: tenant, authzed: context.instanceKind() !== InstanceKind.SPICEDB });
return <CodeBlock className={`language-${props.lang}`}>{processed}</CodeBlock>
}

/**
* SampleConfigEditor is the editor for the values stored in the SampleConfigContext.
*/
export function SampleConfigEditor() {
const context = useContext(SampleConfigContext);
const instanceKind = context.instanceKind();
const [token, setToken] = useState(context.token() ?? '');
const [tenant, setTenant] = useState(context.tenant() ?? '');
const [endpoint, setEndpoint] = useState(context.endpoint() ?? '');

const debouncedUpdateToken = useDebouncedChecker(100, context.setToken);
const debouncedUpdateTenant = useDebouncedChecker(100, context.setTenant);
const debouncedUpdateEndpoint = useDebouncedChecker(100, context.setEndpoint);

const handleChangeToken = (e: React.ChangeEvent) => {
setToken(e.target.value);
debouncedUpdateToken.run(e.target.value);
};

const handleChangeTenant = (e: React.ChangeEvent) => {
setTenant(e.target.value);
debouncedUpdateTenant.run(e.target.value);
};

const handleChangeEndpoint = (e: React.ChangeEvent) => {
setEndpoint(e.target.value);
debouncedUpdateEndpoint.run(e.target.value);
};

return <div className="sample-config-editor">
<div className="system-options">
<div className="system-option">
<input type="radio" id="authzed" name="system" value="authzed"
checked={instanceKind === InstanceKind.AUTHZED}
onChange={() => context.setInstanceKind(InstanceKind.AUTHZED)} />
<label for="authzed">I have an <a href="https://app.authzed.com" target="_blank">Authzed permissions system</a> created</label>
</div>
<div className="system-option">
<input type="radio" id="spicedb" name="system" value="spicedb"
checked={instanceKind === InstanceKind.SPICEDB}
onChange={() => {
setTenant(undefined);
debouncedUpdateTenant.run('');
context.setInstanceKind(InstanceKind.SPICEDB);
}} />
<label for="spicedb">I have a <a href="https://github.com/authzed/spicedb" target="_blank">SpiceDB instance</a> running</label>
</div>
</div>
{instanceKind !== undefined &&
<div className="system-parameters">
{instanceKind === InstanceKind.SPICEDB && <div>
<label>Endpoint</label>
<input type="text" value={endpoint} onChange={handleChangeEndpoint} placeholder="localhost:50051" />
<label>Preshared key</label>
<input type="text" value={token} onChange={handleChangeToken} placeholder="Enter your preshared key" />
</div>}
{instanceKind === InstanceKind.AUTHZED && <div>
<label>Permissions System Prefix</label>
<input type="text" value={tenant} onChange={handleChangeTenant} placeholder="mypermissionssystem/" />
<label>Token</label>
<input type="text" value={token} onChange={handleChangeToken} placeholder="tc_some_token" />
</div>}
</div>
}
</div>;
}
90 changes: 90 additions & 0 deletions src/components/debouncer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useRef } from "react";

enum CheckerStatus {
SLEEPING = 0,
SCHEDULED = 1,
REQUESTED = 2,
RUNNING = 3,
}

interface CheckerState<T> {
status: CheckerStatus,
runIndex: number
argument: T | undefined
}

/**
* useDebouncedChecker is a hook for invoking a specific checker function after the given
* rate of ms, and automatically handling debouncing and rechecking.
*/
export function useDebouncedChecker<T>(rate: number, checker: (arg: T) => Promise<void>) {
const state = useRef<CheckerState<T>>({
status: CheckerStatus.SLEEPING,
runIndex: -1,
argument: undefined
});

const runChecker = (existingIndex: number) => {
const arg = state.current.argument;
if (arg === undefined) {
return;
}

const currentIndex = state.current.runIndex;
if (currentIndex > existingIndex) {
// Things are being changed rapidly. Wait until they are done.
setTimeout(() => runChecker(currentIndex), rate);
return;
}

(async () => {
state.current = {
status: CheckerStatus.RUNNING,
runIndex: existingIndex,
argument: arg
};
await checker(arg);

// If the run went stale, issue another call.
const nextIndex = state.current.runIndex;
if (nextIndex > existingIndex) {
setTimeout(() => runChecker(nextIndex), rate);
return;
}

state.current = {
status: CheckerStatus.SLEEPING,
runIndex: existingIndex,
argument: undefined
};
})();
};

return {
run: (arg: T) => {
// To prevent it blocking the main thread.
(async () => {
if (state.current.status === CheckerStatus.SLEEPING) {
const currentIndex = state.current.runIndex + 1;
state.current = {
status: CheckerStatus.SCHEDULED,
runIndex: currentIndex,
argument: arg,
}

// Kick off the timeout.
setTimeout(() => runChecker(currentIndex), rate);
} else {
state.current = {
...state.current,
runIndex: state.current.runIndex + 1,
argument: arg
};
};
})();
},
isActive: () => {
return state.current.status !== CheckerStatus.SLEEPING
}
};
}
44 changes: 44 additions & 0 deletions src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,47 @@ table td {
height: 4rem;
margin-bottom: 1rem;
}

.sample-config-editor {
padding: 10px;
border: 1px solid #494b6c;
}

.sample-config-editor .system-options {
}

.sample-config-editor .system-parameters {
margin-top: 10px;
}

.sample-config-editor .system-parameters label {
margin-bottom: 4px;
font-size: 85%;
margin-top: 10px;
display: block;
}

.sample-config-editor .system-parameters label:first-child {
margin-top: 0px;
}

.sample-config-editor .system-parameters input {
padding: 8px;
border-radius: 6px;
border: 1px solid #ccc;
display: block;
width: 100%;
font-size: 105%;
}

.sample-config-editor .system-option {
display: flex;
align-items: center;
}

.sample-config-editor input[type="radio"] {
width: 1.2em;
height: 1.2em;
margin: 0px;
margin-right: 6px;
}
22 changes: 22 additions & 0 deletions webpack-fallbacks-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function fallbacksPlugin(
context,
opts,
) {
return {
name: 'webpack-fallbacks-plugin',
configureWebpack(config, isServer, utils, content) {
return {
resolve: {
alias: {
// path: require.resolve('path-browserify'),
},
fallback: {
path: false
},
},
};
},
};
}

module.exports = fallbacksPlugin;
Loading

0 comments on commit 483a85e

Please sign in to comment.