Sample application for blog post
This commit is contained in:
Родитель
334f4a96af
Коммит
989c2efc9f
|
@ -0,0 +1,5 @@
|
|||
# .env.defaults should not contain private information. Create a .env file locally to override these values.
|
||||
AZURE_TENANT_ID=<tenant-id>
|
||||
AZURE_ORDERER=<orderer-url>
|
||||
AZURE_FUNCTION_TOKEN_PROVIDER_URL=<token-provider-url>
|
||||
FLUID_CLIENT=azure
|
|
@ -0,0 +1,36 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect",
|
||||
},
|
||||
},
|
||||
"rules": {
|
||||
"no-undef": "off",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
shell.nix
|
||||
|
||||
**/node_modules/
|
||||
dist/
|
||||
.env
|
|
@ -0,0 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
...require("@fluidframework/build-common/prettier.config.cjs"),
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,73 @@
|
|||
# Shared Tree Dice Roller Demo
|
||||
|
||||
This app is a simple dice roller that uses the Fluid Framework SharedTree data structure to manage the dice rolls.
|
||||
The app is built using React and the Fluid Framework.
|
||||
|
||||
This is a companion app for a blog post on using Fluid Devtools.
|
||||
|
||||
## Setting up the Fluid Framework
|
||||
|
||||
This app is designed to use [Azure Fluid Relay](https://aka.ms/azurefluidrelay) a Fluid relay service offered by Microsoft.
|
||||
You can also run a local service for development purposes.
|
||||
Instructions on how to set up a Fluid relay are on the [Fluid Framework website](https://aka.ms/fluid).
|
||||
|
||||
To use AzureClient's local mode, you first need to start a local server.
|
||||
|
||||
```bash
|
||||
npm run start:server
|
||||
```
|
||||
|
||||
Running this command from your terminal window will launch the Azure Fluid Relay local server.
|
||||
Once the server is started, you can run your application against the local service.
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
One important note is that you will need to use a token provider or, purely for testing and development, use the insecure token provider.
|
||||
There are instructions on how to set this up on the [Fluid Framework website](https://aka.ms/fluid).
|
||||
|
||||
All the code required to set up the Fluid Framework and SharedTree data structure is in the infra folder.
|
||||
Most of this code will be the same for any app.
|
||||
|
||||
## Schema Definition
|
||||
|
||||
The SharedTree schema is defined in the schema.ts source file.
|
||||
This schema is passed into the SharedTree when it is initialized in index.tsx.
|
||||
For more details, see the schema.ts comments.
|
||||
|
||||
## Working with Data
|
||||
|
||||
Working with data in the SharedTree is very simple; however, working with distributed data is always a little more
|
||||
complicated than working with local data.
|
||||
One important note about managing local state and events: ideally, in any app you write, it is best to not special
|
||||
case local changes.
|
||||
Treat the SharedTree as your local data and rely on tree events to update your view.
|
||||
This makes the code reliable and easy to maintain.
|
||||
Also, never mutate tree nodes within events listeners.
|
||||
|
||||
## User Interface
|
||||
|
||||
This app is built using React.
|
||||
If you want to change the css you must run 'npx tailwindcss -i ./src/index.css -o ./src/output.css --watch' in the
|
||||
root folder of your project so that tailwind can update the output.css file.
|
||||
|
||||
## Building and Running
|
||||
|
||||
You can use the following npm scripts (`npm run SCRIPT-NAME`) to build and run the app.
|
||||
|
||||
<!-- AUTO-GENERATED-CONTENT:START (SCRIPTS) -->
|
||||
|
||||
| Script | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------- |
|
||||
| `build` | `npm run format && npm run webpack` |
|
||||
| `compile` | Compile the TypeScript source code to JavaScript. |
|
||||
| `dev` | Runs the app in webpack-dev-server. Expects local-azure-service running on port 7070. |
|
||||
| `dev:azure` | Runs the app in webpack-dev-server using the Azure Fluid Relay config. |
|
||||
| `docs` | Update documentation. |
|
||||
| `format` | Format source code using Prettier. |
|
||||
| `lint` | Lint source code using ESLint |
|
||||
| `webpack` | `webpack` |
|
||||
| `start` | `npm run dev` |
|
||||
|
||||
<!-- AUTO-GENERATED-CONTENT:END -->
|
|
@ -0,0 +1,15 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
matchWord: 'AUTO-GENERATED-CONTENT',
|
||||
transforms: {
|
||||
/* Match <!-- AUTO-GENERATED-CONTENT:START (SCRIPTS) --> */
|
||||
SCRIPTS: require('markdown-magic-package-scripts'),
|
||||
},
|
||||
callback: function () {
|
||||
console.log('markdown processing done')
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"name": "item-counter",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run format && npm run webpack",
|
||||
"ci:test": "start-server-and-test start:server 7070 ci:test:jest",
|
||||
"ci:test:jest": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --ci --reporters=default --reporters=jest-junit",
|
||||
"compile": "tsc -b",
|
||||
"dev": "cross-env FLUID_CLIENT='local' webpack-dev-server",
|
||||
"dev:azure": "cross-env FLUID_CLIENT='azure' webpack-dev-server",
|
||||
"format": "prettier src --write",
|
||||
"lint": "eslint src",
|
||||
"webpack": "cross-env FLUID_CLIENT='azure' webpack",
|
||||
"start": "npm run dev",
|
||||
"start:server": "npx @fluidframework/azure-local-service@2.0.0-rc.4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react-icons": "^2.0.221",
|
||||
"@fluidframework/azure-client": "2.0.0-rc.4.0.1",
|
||||
"@fluidframework/telemetry-utils": "2.0.0-rc.4.0.1",
|
||||
"@fluidframework/test-runtime-utils": "2.0.0-rc.4.0.1",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"axios": "^1.6.2",
|
||||
"dotenv": "^16.0.2",
|
||||
"fluid-framework": "2.0.0-rc.4.0.1",
|
||||
"guid-typescript": "^1.0.9",
|
||||
"hashids": "^2.2.10",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fluidframework/build-common": "^2.0.0",
|
||||
"@fluidframework/devtools": "2.0.0-rc.4.0.1",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/jsrsasign": "^10.5.13",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
||||
"@typescript-eslint/parser": "^5.38.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5",
|
||||
"dotenv-webpack": "^7.1.1",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"mini-css-extract-plugin": "^1",
|
||||
"prettier": "^2.7.1",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"style-loader": "^2",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"ts-loader": "^8.0.0",
|
||||
"typescript": "~5.4",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
},
|
||||
"jest-junit": {
|
||||
"outputDirectory": "nyc",
|
||||
"outputName": "jest-junit-report.xml"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -0,0 +1,71 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
/* eslint-disable react/jsx-key */
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { loadFluidData, containerSchema } from "./infra/fluid.js";
|
||||
import { treeConfiguration } from "./schema.js";
|
||||
import "./output.css";
|
||||
import { ReactApp } from "./react_app.js";
|
||||
import { AttachState, Tree } from "fluid-framework";
|
||||
import { initializeDevtools } from "@fluidframework/devtools/beta";
|
||||
import { devtoolsLogger } from "./infra/clientProps.js";
|
||||
|
||||
async function start() {
|
||||
// create the root element for React
|
||||
const app = document.createElement("div");
|
||||
app.id = "app";
|
||||
document.body.appendChild(app);
|
||||
const root = createRoot(app);
|
||||
|
||||
// Get the root container id from the URL
|
||||
// If there is no container id, then the app will make
|
||||
// a new container.
|
||||
let containerId = location.hash.substring(1);
|
||||
|
||||
// Initialize Fluid Container - this will either make a new container or load an existing one
|
||||
const { container } = await loadFluidData(containerId, containerSchema);
|
||||
|
||||
// Initialize the SharedTree Data Structure
|
||||
const appData = container.initialObjects.appData.schematize(
|
||||
treeConfiguration, // This is defined in schema.ts
|
||||
);
|
||||
|
||||
// Render the app - note we attach new containers after render so
|
||||
// the app renders instantly on create new flow. The app will be
|
||||
// interactive immediately.
|
||||
root.render(<ReactApp data={appData} />);
|
||||
|
||||
initializeDevtools({
|
||||
logger: devtoolsLogger,
|
||||
initialContainers: [
|
||||
{
|
||||
container,
|
||||
containerKey: "My Container",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// If the app is in a `createNew` state - no containerId, and the container is detached, we attach the container.
|
||||
// This uploads the container to the service and connects to the collaboration session.
|
||||
if (container.attachState === AttachState.Detached) {
|
||||
containerId = await container.attach();
|
||||
|
||||
// The newly attached container is given a unique ID that can be used to access the container in another session.
|
||||
// This adds that id to the url.
|
||||
history.replaceState(undefined, "", "#" + containerId);
|
||||
} else {
|
||||
// When loading an existing container...
|
||||
|
||||
Tree.on(appData.root, "treeChanged", () => {
|
||||
for (let i = 0; i < 1_000_000_000; i++) {
|
||||
/* Simulate long-running application logic */
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
start().catch((error) => console.error(error));
|
|
@ -0,0 +1,49 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AzureRemoteConnectionConfig,
|
||||
AzureClientProps,
|
||||
AzureLocalConnectionConfig,
|
||||
} from "@fluidframework/azure-client";
|
||||
import {
|
||||
AzureFunctionTokenProvider,
|
||||
azureUser,
|
||||
InsecureTokenProvider,
|
||||
user,
|
||||
} from "./tokenProvider.js";
|
||||
|
||||
import { createDevtoolsLogger } from "@fluidframework/devtools/beta";
|
||||
|
||||
const useAzure = process.env.FLUID_CLIENT === "azure";
|
||||
if (!useAzure) {
|
||||
console.warn(`Configured to use local tinylicious.`);
|
||||
}
|
||||
|
||||
const remoteConnectionConfig: AzureRemoteConnectionConfig = {
|
||||
type: "remote",
|
||||
tenantId: process.env.AZURE_TENANT_ID!,
|
||||
tokenProvider: new AzureFunctionTokenProvider(
|
||||
process.env.AZURE_FUNCTION_TOKEN_PROVIDER_URL!,
|
||||
azureUser,
|
||||
),
|
||||
endpoint: process.env.AZURE_ORDERER!,
|
||||
};
|
||||
|
||||
const localConnectionConfig: AzureLocalConnectionConfig = {
|
||||
type: "local",
|
||||
tokenProvider: new InsecureTokenProvider("VALUE_NOT_USED", user),
|
||||
endpoint: "http://localhost:7070",
|
||||
};
|
||||
|
||||
export const devtoolsLogger = createDevtoolsLogger();
|
||||
|
||||
const connectionConfig: AzureRemoteConnectionConfig | AzureLocalConnectionConfig = useAzure
|
||||
? remoteConnectionConfig
|
||||
: localConnectionConfig;
|
||||
export const clientProps: AzureClientProps = {
|
||||
connection: connectionConfig,
|
||||
logger: devtoolsLogger,
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import { AzureClient, AzureContainerServices } from "@fluidframework/azure-client";
|
||||
import { ContainerSchema, IFluidContainer, SharedTree } from "fluid-framework";
|
||||
import { clientProps } from "./clientProps.js";
|
||||
|
||||
const client = new AzureClient(clientProps);
|
||||
|
||||
/**
|
||||
* This function will create a container if no container ID is passed on the hash portion of the URL.
|
||||
* If a container ID is provided, it will load the container.
|
||||
*
|
||||
* @returns The loaded container and container services.
|
||||
*/
|
||||
export async function loadFluidData<T extends ContainerSchema>(
|
||||
containerId: string,
|
||||
containerSchema: T,
|
||||
): Promise<{
|
||||
services: AzureContainerServices;
|
||||
container: IFluidContainer<T>;
|
||||
}> {
|
||||
let container: IFluidContainer<T>;
|
||||
let services: AzureContainerServices;
|
||||
|
||||
// Get or create the document depending if we are running through the create new flow
|
||||
if (containerId.length === 0) {
|
||||
// The client will create a new detached container using the schema
|
||||
// A detached container will enable the app to modify the container before attaching it to the client
|
||||
({ container, services } = await client.createContainer(containerSchema));
|
||||
} else {
|
||||
// Use the unique container ID to fetch the container created earlier. It will already be connected to the
|
||||
// collaboration session.
|
||||
({ container, services } = await client.getContainer(containerId, containerSchema));
|
||||
}
|
||||
return { services, container };
|
||||
}
|
||||
|
||||
export const containerSchema = {
|
||||
initialObjects: {
|
||||
appData: SharedTree,
|
||||
},
|
||||
} satisfies ContainerSchema;
|
|
@ -0,0 +1,202 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AzureMember,
|
||||
ITokenClaims,
|
||||
ITokenProvider,
|
||||
ITokenResponse,
|
||||
IUser,
|
||||
} from "@fluidframework/azure-client";
|
||||
import { ScopeType } from "@fluidframework/protocol-definitions";
|
||||
import axios from "axios";
|
||||
import { KJUR as jsrsasign } from "jsrsasign";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
/**
|
||||
* Insecure user definition. *
|
||||
*/
|
||||
export interface IInsecureUser extends IUser {
|
||||
/**
|
||||
* Name of the user making the connection to the service.
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token Provider implementation for connecting to an Azure Function endpoint for
|
||||
* Azure Fluid Relay token resolution.
|
||||
*/
|
||||
export class AzureFunctionTokenProvider implements ITokenProvider {
|
||||
/**
|
||||
* Creates a new instance using configuration parameters.
|
||||
* @param azFunctionUrl - URL to Azure Function endpoint
|
||||
* @param user - User object
|
||||
*/
|
||||
constructor(
|
||||
private readonly azFunctionUrl: string,
|
||||
private readonly user?: Pick<AzureMember, "name" | "id" | "additionalDetails">,
|
||||
) {}
|
||||
|
||||
public async fetchOrdererToken(tenantId: string, documentId?: string): Promise<ITokenResponse> {
|
||||
return {
|
||||
jwt: await this.getToken(tenantId, documentId),
|
||||
};
|
||||
}
|
||||
|
||||
public async fetchStorageToken(tenantId: string, documentId: string): Promise<ITokenResponse> {
|
||||
return {
|
||||
jwt: await this.getToken(tenantId, documentId),
|
||||
};
|
||||
}
|
||||
|
||||
private async getToken(tenantId: string, documentId: string | undefined): Promise<string> {
|
||||
const response = await axios.get(this.azFunctionUrl, {
|
||||
params: {
|
||||
tenantId,
|
||||
documentId,
|
||||
userName: this.user?.name,
|
||||
userId: this.user?.id,
|
||||
additionalDetails: this.user?.additionalDetails,
|
||||
},
|
||||
});
|
||||
return response.data as string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an in memory implementation of a Fluid Token Provider that can be
|
||||
* used to insecurely connect to the Fluid Relay.
|
||||
*
|
||||
* As the name implies, this is not secure and should not be used in production.
|
||||
* It simply makes examples where authentication is not relevant easier to bootstrap.
|
||||
*/
|
||||
export class InsecureTokenProvider implements ITokenProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Private server tenantKey for generating tokens.
|
||||
*/
|
||||
private readonly tenantKey: string,
|
||||
|
||||
/**
|
||||
* User with whom generated tokens will be associated.
|
||||
*/
|
||||
private readonly user: IInsecureUser,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc @fluidframework/routerlicious-driver#ITokenProvider.fetchOrdererToken}
|
||||
*/
|
||||
public async fetchOrdererToken(tenantId: string, documentId?: string): Promise<ITokenResponse> {
|
||||
return {
|
||||
fromCache: true,
|
||||
jwt: generateToken(
|
||||
tenantId,
|
||||
this.tenantKey,
|
||||
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
|
||||
documentId,
|
||||
this.user,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc @fluidframework/routerlicious-driver#ITokenProvider.fetchStorageToken}
|
||||
*/
|
||||
public async fetchStorageToken(tenantId: string, documentId: string): Promise<ITokenResponse> {
|
||||
return {
|
||||
fromCache: true,
|
||||
jwt: generateToken(
|
||||
tenantId,
|
||||
this.tenantKey,
|
||||
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
|
||||
documentId,
|
||||
this.user,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const user = generateUser();
|
||||
|
||||
export const azureUser = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a {@link https://en.wikipedia.org/wiki/JSON_Web_Token | JSON Web Token} (JWT)
|
||||
* to authorize access to a Routerlicious-based Fluid service.
|
||||
*
|
||||
* @remarks Note: this function uses a browser friendly auth library
|
||||
* ({@link https://www.npmjs.com/package/jsrsasign | jsrsasign}) and may only be used in client (browser) context.
|
||||
* It is **not** Node.js-compatible.
|
||||
*
|
||||
* @param tenantId - See {@link @fluidframework/protocol-definitions#ITokenClaims.tenantId}
|
||||
* @param key - API key to authenticate user. Must be {@link https://en.wikipedia.org/wiki/UTF-8 | UTF-8}-encoded.
|
||||
* @param scopes - See {@link @fluidframework/protocol-definitions#ITokenClaims.scopes}
|
||||
* @param documentId - See {@link @fluidframework/protocol-definitions#ITokenClaims.documentId}.
|
||||
* If not specified, the token will not be associated with a document, and an empty string will be used.
|
||||
* @param user - User with whom generated tokens will be associated.
|
||||
* If not specified, the token will not be associated with a user, and a randomly generated mock user will be
|
||||
* used instead.
|
||||
* See {@link @fluidframework/protocol-definitions#ITokenClaims.user}
|
||||
* @param lifetime - Used to generate the {@link @fluidframework/protocol-definitions#ITokenClaims.exp | expiration}.
|
||||
* Expiration = now + lifetime.
|
||||
* Expressed in seconds.
|
||||
* Default: 3600 (1 hour).
|
||||
* @param ver - See {@link @fluidframework/protocol-definitions#ITokenClaims.ver}.
|
||||
* Default: `1.0`.
|
||||
*/
|
||||
export function generateToken(
|
||||
tenantId: string,
|
||||
key: string,
|
||||
scopes: ScopeType[],
|
||||
documentId?: string,
|
||||
user?: IInsecureUser,
|
||||
lifetime: number = 60 * 60,
|
||||
ver = "1.0",
|
||||
): string {
|
||||
let userClaim = user ? user : generateUser();
|
||||
if (userClaim.id === "" || userClaim.id === undefined) {
|
||||
userClaim = generateUser();
|
||||
}
|
||||
|
||||
// Current time in seconds
|
||||
const now = Math.round(Date.now() / 1000);
|
||||
const docId = documentId ?? "";
|
||||
|
||||
const claims: ITokenClaims & { jti: string } = {
|
||||
documentId: docId,
|
||||
scopes,
|
||||
tenantId,
|
||||
user: userClaim,
|
||||
iat: now,
|
||||
exp: now + lifetime,
|
||||
ver,
|
||||
jti: uuid(),
|
||||
};
|
||||
|
||||
const utf8Key = { utf8: key };
|
||||
return jsrsasign.jws.JWS.sign(
|
||||
null,
|
||||
JSON.stringify({ alg: "HS256", typ: "JWT" }),
|
||||
claims,
|
||||
utf8Key,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an arbitrary ("random") {@link IInsecureUser} by generating a
|
||||
* random UUID for its {@link @fluidframework/protocol-definitions#IUser.id | id} and {@link IInsecureUser.name | name} properties.
|
||||
*/
|
||||
export function generateUser(): IInsecureUser {
|
||||
const randomUser = {
|
||||
id: uuid(),
|
||||
name: uuid(),
|
||||
};
|
||||
|
||||
return randomUser;
|
||||
}
|
|
@ -0,0 +1,758 @@
|
|||
/*
|
||||
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: "";
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
7. Disable tap highlights on iOS
|
||||
*/
|
||||
|
||||
html,
|
||||
:host {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
font-variation-settings: normal;
|
||||
/* 6 */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
/* 7 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font-family by default.
|
||||
2. Use the user's configured `mono` font-feature-settings by default.
|
||||
3. Use the user's configured `mono` font-variation-settings by default.
|
||||
4. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
/* 1 */
|
||||
font-feature-settings: normal;
|
||||
/* 2 */
|
||||
font-variation-settings: normal;
|
||||
/* 3 */
|
||||
font-size: 1em;
|
||||
/* 4 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-feature-settings: inherit;
|
||||
/* 1 */
|
||||
font-variation-settings: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
letter-spacing: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
input:where([type="button"]),
|
||||
input:where([type="reset"]),
|
||||
input:where([type="submit"]) {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset default styling for dialogs.
|
||||
*/
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder,
|
||||
textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1536px;
|
||||
}
|
||||
}
|
||||
|
||||
.m-6 {
|
||||
margin: 1.5rem;
|
||||
}
|
||||
|
||||
.my-8 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.h-24 {
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.max-w-sm {
|
||||
max-width: 24rem;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.content-center {
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-full {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.bg-black {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-extrabold {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.text-blue-300 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(147 197 253 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.shadow-md {
|
||||
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
|
||||
0 2px 4px -2px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
|
||||
var(--tw-shadow);
|
||||
}
|
||||
|
||||
.hover\:bg-blue-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-white:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import { TreeView, Tree } from "fluid-framework";
|
||||
import { DiceRoller } from "./schema.js";
|
||||
|
||||
export function ReactApp(props: { data: TreeView<typeof DiceRoller> }): JSX.Element {
|
||||
const [invalidations, setInvalidations] = useState(0);
|
||||
|
||||
const diceRoller = props.data.root;
|
||||
|
||||
// Register for tree deltas when the component mounts.
|
||||
// Any time the tree changes, the app will update.
|
||||
useEffect(() => {
|
||||
const unsubscribe = Tree.on(diceRoller, "treeChanged", () => {
|
||||
setInvalidations(invalidations + Math.random());
|
||||
});
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-3 justify-center flex-wrap w-full h-full">
|
||||
<div className="flex flex-col gap-3 justify-center content-center m-4">
|
||||
<VisualDie diceRoller={diceRoller} />
|
||||
<RollButton diceRoller={diceRoller} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function VisualDie(props: { diceRoller: DiceRoller }): JSX.Element {
|
||||
const divStyle = {
|
||||
// color: `hsl(${parseInt(props.diceRoller.value, 10) * 60}, 70%, 50%)`,
|
||||
color: `hsl(${props.diceRoller.value * 60}, 70%, 50%)`,
|
||||
fontSize: "200px",
|
||||
};
|
||||
|
||||
// Unicode 0x2680-0x2685 are the sides of a dice (⚀⚁⚂⚃⚄⚅)
|
||||
return (
|
||||
<div style={divStyle}>
|
||||
{String.fromCodePoint((0x267f + props.diceRoller.value) as unknown as number)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RollButton(props: { diceRoller: DiceRoller }): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded w-full"
|
||||
onClick={() => props.diceRoller.roll()}
|
||||
>
|
||||
Roll
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import { TreeConfiguration, SchemaFactory } from "fluid-framework";
|
||||
|
||||
// Define a schema factory that is used to generate classes for the schema
|
||||
const schemaFactory = new SchemaFactory("d302b84c-75f6-4ecd-9663-524f467013e3");
|
||||
|
||||
export class DiceRoller extends schemaFactory.object("DiceRoller", {
|
||||
// value: schemaFactory.string
|
||||
value: schemaFactory.number
|
||||
}) {
|
||||
public roll() {
|
||||
let newValue = -1;
|
||||
// while (newValue === parseInt(this.value, 10) || newValue === -1) {
|
||||
while (newValue === this.value || newValue === -1) {
|
||||
newValue = Math.floor(Math.random() * 6) + 1;
|
||||
}
|
||||
// this.value = newValue.toString();
|
||||
this.value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
// This object is passed into the SharedTree via the schematize method.
|
||||
export const treeConfiguration = new TreeConfiguration(
|
||||
// Specify the root type - our class.
|
||||
DiceRoller,
|
||||
// Initial state of the tree which is used for new trees.
|
||||
// () => new DiceRoller({ value: "1" }),
|
||||
() => new DiceRoller({ value: 1 }),
|
||||
);
|
|
@ -0,0 +1,13 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// DO NOT EDIT
|
||||
"outDir": "./dist",
|
||||
"target": "es6",
|
||||
"module": "Node16",
|
||||
"sourceMap": true,
|
||||
// Set your preferences here for TypeScript
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"moduleResolution": "Node16",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*!
|
||||
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require("path");
|
||||
const Dotenv = require("dotenv-webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
module.exports = {
|
||||
// Basic configuration
|
||||
entry: "./src/index.tsx",
|
||||
// Necessary in order to use source maps and debug directly TypeScript files
|
||||
devtool: "source-map",
|
||||
mode: "development",
|
||||
performance: {
|
||||
maxAssetSize: 4000000,
|
||||
maxEntrypointSize: 4000000,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
// Necessary in order to use TypeScript
|
||||
{
|
||||
test: /\.ts$|tsx/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: "style-loader",
|
||||
},
|
||||
{
|
||||
loader: "css-loader",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensionAlias: {
|
||||
".js": [".ts", ".tsx", ".js", ".cjs", ".mjs"],
|
||||
},
|
||||
extensions: [".ts", ".tsx", ".js", ".cjs", ".mjs"],
|
||||
},
|
||||
output: {
|
||||
filename: "bundle.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
// This line is VERY important for VS Code debugging to attach properly
|
||||
// Tamper with it at your own risks
|
||||
devtoolModuleFilenameTemplate: "[absolute-resource-path]",
|
||||
clean: true,
|
||||
},
|
||||
plugins: [
|
||||
// No need to write a index.html
|
||||
new HtmlWebpackPlugin({
|
||||
title: "Fluid Demo",
|
||||
favicon: "",
|
||||
}),
|
||||
// Load environment variables during webpack bundle
|
||||
new Dotenv({
|
||||
systemvars: true,
|
||||
}),
|
||||
// Extract CSS to separate file
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "css/mystyles.css",
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
// keep port in sync with VS Code launch.json
|
||||
port: 8080,
|
||||
// Hot-reloading, the sole reason to use webpack here <3
|
||||
hot: true,
|
||||
},
|
||||
};
|
Загрузка…
Ссылка в новой задаче