forked from cerc-io/snowballtools-base
Compare commits
167
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86e6600749 | ||
|
|
3f11ff3da1 | ||
|
|
f614199bb8 | ||
|
|
0787d2008b | ||
|
|
b9ffb691dc | ||
|
|
25e47aa5ec | ||
|
|
8488cfab83 | ||
|
|
8376aff7bd | ||
|
|
583c0b9d26 | ||
|
|
e12c94e087 | ||
|
|
879cfdb2bf | ||
|
|
04b6a84440 | ||
|
|
41b9ce1096 | ||
|
|
ad69ebe4a0 | ||
|
|
ca8863e1d6 | ||
|
|
0fb1127b96 | ||
|
|
b34e0783c1 | ||
|
|
050f404776 | ||
|
|
2e30a1aae1 | ||
|
|
7c8e9f2448 | ||
|
|
065b12e3d9 | ||
|
|
3668a8edf7 | ||
|
|
28366ea725 | ||
|
|
4c3072ed50 | ||
|
|
6975360d48 | ||
|
|
671321ef4d | ||
|
|
d04ad68f18 | ||
|
|
16df36715a | ||
|
|
6fdbcf6f46 | ||
|
|
702cef24b3 | ||
|
|
d3cf0453e7 | ||
|
|
d76db4fb96 | ||
|
|
19545c48bd | ||
|
|
8d96be625e | ||
|
|
1c05ba8822 | ||
|
|
fd7d06b9e2 | ||
|
|
550ef91968 | ||
|
|
c710fb5a53 | ||
|
|
a95f18a66b | ||
|
|
ccc36fd175 | ||
|
|
ce7e9174e4 | ||
|
|
43d7c94cb0 | ||
|
|
294675c276 | ||
|
|
2e0b228aa5 | ||
|
|
907b5ee02d | ||
|
|
8f456c04f5 | ||
|
|
29caf510ea | ||
|
|
a63d0f69ed | ||
|
|
1aa57aaecc | ||
|
|
a27331f54f | ||
|
|
ebe6d35b54 | ||
|
|
29557d7597 | ||
|
|
df540e06eb | ||
|
|
3fe0718532 | ||
|
|
039753df56 | ||
|
|
386f40952e | ||
|
|
e1c4f77ec1 | ||
|
|
57601e6b4b | ||
|
|
e9a367db42 | ||
|
|
6ae04251d2 | ||
|
|
690fc8d2cb | ||
|
|
2719b3e385 | ||
|
|
de6d5c302b | ||
|
|
dc91fa0d7f | ||
|
|
571d58d57d | ||
|
|
947337acb1 | ||
|
|
f8908c1c06 | ||
|
|
6e7385b118 | ||
|
|
abbda18fc7 | ||
|
|
2d0de785f9 | ||
|
|
68f1a265f4 | ||
|
|
9b7a021e8b | ||
|
|
cdfc72a5a0 | ||
|
|
47b322c212 | ||
|
|
e4c39e0955 | ||
|
|
9f46290ecc | ||
|
|
b71b7579a6 | ||
|
|
8aa1362be6 | ||
|
|
e1d2b789a3 | ||
|
|
cf161bcba0 | ||
|
|
57956ec269 | ||
|
|
fb77433bea | ||
|
|
146b904556 | ||
|
|
756cc52e1c | ||
|
|
ba5f281671 | ||
|
|
60a66e94bd | ||
|
|
5f5b0a4d4f | ||
|
|
5ebdd3003a | ||
|
|
987643b153 | ||
|
|
090a9054e4 | ||
|
|
7dcd6581a7 | ||
|
|
928958131c | ||
|
|
327ac62186 | ||
|
|
37c250873c | ||
|
|
f7fa47dbc8 | ||
|
|
1bba82ee86 | ||
|
|
99cc35459b | ||
|
|
4b54f2fc2e | ||
|
|
4906740876 | ||
|
|
73ff7896f8 | ||
|
|
67d28910d4 | ||
|
|
939b1c40e8 | ||
|
|
c3b048d273 | ||
|
|
48552310e0 | ||
|
|
c82e1110d3 | ||
|
|
b7f29781f2 | ||
|
|
cf531127c4 | ||
|
|
44de7bb0f1 | ||
|
|
ed0de90118 | ||
|
|
11c8f7b09b | ||
|
|
3dd62114b4 | ||
|
|
4f3ea09b79 | ||
|
|
f4f80903f8 | ||
|
|
5307eab443 | ||
|
|
00199424f7 | ||
|
|
aaadfeab72 | ||
|
|
3ffac1a019 | ||
|
|
56a264199b | ||
|
|
df8bc3784a | ||
|
|
464b0463af | ||
|
|
c29134bfd9 | ||
|
|
9b0ad34831 | ||
|
|
89b3cb8c31 | ||
|
|
c52d695ca7 | ||
|
|
187f75d561 | ||
|
|
14deecfad6 | ||
|
|
808b2ad61b | ||
|
|
775e731f4d | ||
|
|
813cea055b | ||
|
|
072e2c20e3 | ||
|
|
83d5e3d769 | ||
|
|
9ed52c67c9 | ||
|
|
f7d6d02b27 | ||
|
|
d7dc9a07f9 | ||
|
|
6cbc87a7d1 | ||
|
|
ddb54de49b | ||
|
|
cf9fd04272 | ||
|
|
535c37d0b4 | ||
|
|
46476bef28 | ||
|
|
27f5c57c37 | ||
|
|
d1c3249b98 | ||
|
|
d34a5f29cd | ||
|
|
4a14681753 | ||
|
|
aea6bfde54 | ||
|
|
4aac93b504 | ||
|
|
e6b68dcce4 | ||
|
|
d2daed4cac | ||
|
|
c395be82b5 | ||
|
|
748ca507da | ||
|
|
bc210fdb0f | ||
|
|
bace4a6ce6 | ||
|
|
0d36cc1b6d | ||
|
|
1ed4ee979c | ||
|
|
72f1abcdf6 | ||
|
|
57add027ab | ||
|
|
7ad02d27bf | ||
|
|
b17e9df1e4 | ||
|
|
baadc507e7 | ||
|
|
6316aa852b | ||
|
|
2274e8d145 | ||
|
|
f8d706233e | ||
|
|
cc8f9527da | ||
|
|
47231a6eab | ||
|
|
4774074b67 | ||
|
|
0da7c3541e | ||
|
|
b7b4ab1f14 | ||
|
|
ea6ad52a73 |
@@ -19,5 +19,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: yarn
|
||||
- name: Build libs
|
||||
run: yarn workspace gql-client run build
|
||||
- name: Linter check
|
||||
run: yarn lint
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Test webapp deployment
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test_app_deployment:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Test webapp deployment
|
||||
run: ./packages/deployer/test/test-webapp-deployment-undeployment.sh
|
||||
- name: Notify Vulcanize Slack on CI failure
|
||||
if: ${{ always() && github.ref_name == 'main' }}
|
||||
uses: ravsamhq/notify-slack-action@v2
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
notify_when: 'failure'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.VULCANIZE_SLACK_CI_ALERTS_WEBHOOK }}
|
||||
- name: Notify DeepStack Slack on CI failure
|
||||
if: ${{ always() && github.ref_name == 'main' }}
|
||||
uses: ravsamhq/notify-slack-action@v2
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
notify_when: 'failure'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.DEEPSTACK_SLACK_CI_ALERTS_WEBHOOK }}
|
||||
@@ -5,3 +5,5 @@ yarn-error.log
|
||||
.yarnrc
|
||||
|
||||
packages/backend/environments/local.toml
|
||||
packages/backend/dev/
|
||||
packages/frontend/dist/
|
||||
@@ -0,0 +1 @@
|
||||
v20.12.1
|
||||
+12
-6
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
PKG_DIR="./packages/frontend"
|
||||
OUTPUT_DIR="${PKG_DIR}/build"
|
||||
OUTPUT_DIR="${PKG_DIR}/dist"
|
||||
DEST_DIR=${1:-/data}
|
||||
|
||||
if [[ -d "$DEST_DIR" ]]; then
|
||||
@@ -10,11 +10,17 @@ if [[ -d "$DEST_DIR" ]]; then
|
||||
fi
|
||||
|
||||
cat > $PKG_DIR/.env <<EOF
|
||||
REACT_APP_SERVER_URL = 'LACONIC_HOSTED_CONFIG_app_server_url'
|
||||
REACT_APP_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_app_github_clientid'
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo'
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo'
|
||||
REACT_APP_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_app_wallet_connect_id'
|
||||
VITE_SERVER_URL = 'LACONIC_HOSTED_CONFIG_server_url'
|
||||
VITE_GITHUB_CLIENT_ID = 'LACONIC_HOSTED_CONFIG_github_clientid'
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_pwa_templaterepo'
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo'
|
||||
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
|
||||
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
|
||||
VITE_ALCHEMY_API_KEY = 'LACONIC_HOSTED_CONFIG_aplchemy_api_key'
|
||||
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
|
||||
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'
|
||||
VITE_TURNKEY_API_BASE_URL = 'LACONIC_HOSTED_CONFIG_turnkey_api_base_url'
|
||||
VITE_TURNKEY_ORGANIZATION_ID = 'LACONIC_HOSTED_CONFIG_turnkey_organization_id'
|
||||
EOF
|
||||
|
||||
yarn || exit 1
|
||||
|
||||
+4
-4
@@ -4,15 +4,15 @@
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"depcheck": "^1.4.2",
|
||||
"husky": "^8.0.3",
|
||||
"lerna": "^8.0.0",
|
||||
"depcheck": "^1.4.2"
|
||||
"patch-package": "^8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"build": "lerna run build --stream",
|
||||
"lint": "lerna run lint --stream -- --max-warnings=0"
|
||||
"lint": "lerna run lint --stream"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
v20.12.1
|
||||
@@ -3,15 +3,18 @@
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@cerc-io/laconic-sdk": "^0.1.16",
|
||||
"@graphql-tools/schema": "^10.0.2",
|
||||
"@graphql-tools/utils": "^10.0.12",
|
||||
"@octokit/oauth-app": "^6.1.0",
|
||||
"@snowballtools/laconic-sdk": "^0.1.17",
|
||||
"@turnkey/sdk-server": "^0.1.0",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"apollo-server-core": "^3.13.0",
|
||||
"apollo-server-express": "^3.13.0",
|
||||
"cookie-session": "^2.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.1",
|
||||
"express": "^4.18.2",
|
||||
@@ -35,30 +38,20 @@
|
||||
"copy-assets": "copyfiles -u 1 src/**/*.gql dist/",
|
||||
"clean": "rm -rf ./dist",
|
||||
"build": "yarn clean && tsc && yarn copy-assets",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "tsc --noEmit",
|
||||
"test:registry:init": "DEBUG=snowball:* ts-node ./test/initialize-registry.ts",
|
||||
"test:registry:publish-deploy-records": "DEBUG=snowball:* ts-node ./test/publish-deploy-records.ts",
|
||||
"test:db:load:fixtures": "DEBUG=snowball:* ts-node ./test/initialize-db.ts",
|
||||
"test:db:delete": "DEBUG=snowball:* ts-node ./test/delete-db.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-session": "^2.0.49",
|
||||
"@types/express-session": "^1.17.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-semistandard": "^15.0.1",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"prettier": "^3.1.1",
|
||||
"workspace": "^0.0.1-preview.1"
|
||||
}
|
||||
|
||||
@@ -52,4 +52,10 @@ export interface Config {
|
||||
gitHub: GitHubConfig;
|
||||
registryConfig: RegistryConfig;
|
||||
misc: MiscConfig;
|
||||
turnkey: {
|
||||
apiBaseUrl: string;
|
||||
apiPublicKey: string;
|
||||
apiPrivateKey: string;
|
||||
defaultOrganizationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,6 +39,12 @@ export class User {
|
||||
@CreateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column()
|
||||
subOrgId!: string;
|
||||
|
||||
@Column()
|
||||
turnkeyWalletId!: string;
|
||||
|
||||
@OneToMany(() => ProjectMember, (projectMember) => projectMember.project, {
|
||||
cascade: ['soft-remove']
|
||||
})
|
||||
|
||||
@@ -9,8 +9,6 @@ import { Database } from './database';
|
||||
import { createAndStartServer } from './server';
|
||||
import { createResolvers } from './resolvers';
|
||||
import { getConfig } from './utils';
|
||||
import { Config } from './config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||
import { Service } from './service';
|
||||
import { Registry } from './registry';
|
||||
|
||||
@@ -18,13 +16,12 @@ const log = debug('snowball:server');
|
||||
const OAUTH_CLIENT_TYPE = 'oauth-app';
|
||||
|
||||
export const main = async (): Promise<void> => {
|
||||
// TODO: get config path using cli
|
||||
const { server, database, gitHub, registryConfig, misc } = await getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
const { server, database, gitHub, registryConfig, misc } = await getConfig();
|
||||
|
||||
const app = new OAuthApp({
|
||||
clientType: OAUTH_CLIENT_TYPE,
|
||||
clientId: gitHub.oAuth.clientId,
|
||||
clientSecret: gitHub.oAuth.clientSecret
|
||||
clientSecret: gitHub.oAuth.clientSecret,
|
||||
});
|
||||
|
||||
const db = new Database(database, misc);
|
||||
@@ -35,7 +32,7 @@ export const main = async (): Promise<void> => {
|
||||
{ gitHubConfig: gitHub, registryConfig },
|
||||
db,
|
||||
app,
|
||||
registry
|
||||
registry,
|
||||
);
|
||||
|
||||
const typeDefs = fs
|
||||
|
||||
@@ -3,7 +3,7 @@ import assert from 'assert';
|
||||
import { inc as semverInc } from 'semver';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { Registry as LaconicRegistry } from '@cerc-io/laconic-sdk';
|
||||
import { Registry as LaconicRegistry } from '@snowballtools/laconic-sdk';
|
||||
|
||||
import { RegistryConfig } from './config';
|
||||
import {
|
||||
|
||||
@@ -1,41 +1,104 @@
|
||||
import { Router } from 'express';
|
||||
import { SiweMessage, generateNonce } from 'siwe';
|
||||
import { SiweMessage } from 'siwe';
|
||||
import { Service } from '../service';
|
||||
import { authenticateUser, createUser } from '../turnkey-backend';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/nonce', async (_, res) => {
|
||||
res.send(generateNonce());
|
||||
//
|
||||
// Turnkey
|
||||
//
|
||||
router.get('/registration/:email', async (req, res) => {
|
||||
const service: Service = req.app.get('service');
|
||||
const user = await service.getUserByEmail(req.params.email);
|
||||
if (user) {
|
||||
return res.send({ subOrganizationId: user?.subOrgId });
|
||||
} else {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
const { email, challenge, attestation } = req.body;
|
||||
const user = await createUser(req.app.get('service'), {
|
||||
challenge,
|
||||
attestation,
|
||||
userEmail: email,
|
||||
userName: email.split('@')[0],
|
||||
});
|
||||
req.session.userId = user.id;
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
router.post('/authenticate', async (req, res) => {
|
||||
const { signedWhoamiRequest } = req.body;
|
||||
const user = await authenticateUser(
|
||||
req.app.get('service'),
|
||||
signedWhoamiRequest,
|
||||
);
|
||||
if (user) {
|
||||
req.session.userId = user.id;
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Lit
|
||||
//
|
||||
|
||||
router.post('/validate', async (req, res) => {
|
||||
const { message, signature } = req.body;
|
||||
const { message, signature, action } = req.body;
|
||||
const { success, data } = await new SiweMessage(message).verify({
|
||||
signature
|
||||
signature,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
req.session.address = data.address;
|
||||
req.session.chainId = data.chainId;
|
||||
if (!success) {
|
||||
return res.send({ success });
|
||||
}
|
||||
const service: Service = req.app.get('service');
|
||||
const user = await service.getUserByEthAddress(data.address);
|
||||
|
||||
if (action === 'signup') {
|
||||
if (user) {
|
||||
return res.send({ success: false, error: 'user_already_exists' });
|
||||
}
|
||||
const newUser = await service.createUser({
|
||||
ethAddress: data.address,
|
||||
email: '',
|
||||
name: '',
|
||||
subOrgId: '',
|
||||
turnkeyWalletId: '',
|
||||
});
|
||||
req.session.userId = newUser.id;
|
||||
} else if (action === 'login') {
|
||||
if (!user) {
|
||||
return res.send({ success: false, error: 'user_not_found' });
|
||||
}
|
||||
req.session.userId = user.id;
|
||||
}
|
||||
|
||||
res.send({ success });
|
||||
});
|
||||
|
||||
//
|
||||
// General
|
||||
//
|
||||
router.get('/session', (req, res) => {
|
||||
if (req.session.address && req.session.chainId) {
|
||||
res.send({ address: req.session.address, chainId: req.session.chainId });
|
||||
if (req.session.userId) {
|
||||
res.send({
|
||||
userId: req.session.userId,
|
||||
});
|
||||
} else {
|
||||
res.status(401).send({ error: 'Unauthorized: No active session' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.send({ success: false });
|
||||
}
|
||||
res.send({ success: true });
|
||||
});
|
||||
// This is how you clear cookie-session
|
||||
(req as any).session = null;
|
||||
res.send({ success: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -6,9 +6,9 @@ import { createServer } from 'http';
|
||||
import {
|
||||
ApolloServerPluginDrainHttpServer,
|
||||
ApolloServerPluginLandingPageLocalDefault,
|
||||
AuthenticationError
|
||||
AuthenticationError,
|
||||
} from 'apollo-server-core';
|
||||
import session from 'express-session';
|
||||
import cookieSession from 'cookie-session';
|
||||
|
||||
import { TypeSource } from '@graphql-tools/utils';
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
@@ -23,8 +23,7 @@ const log = debug('snowball:server');
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
address: string;
|
||||
chainId: number;
|
||||
userId: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +31,7 @@ export const createAndStartServer = async (
|
||||
serverConfig: ServerConfig,
|
||||
typeDefs: TypeSource,
|
||||
resolvers: any,
|
||||
service: Service
|
||||
service: Service,
|
||||
): Promise<ApolloServer> => {
|
||||
const { host, port, gqlPath = DEFAULT_GQL_PATH } = serverConfig;
|
||||
const { appOriginUrl, secret, domain, trustProxy } = serverConfig.session;
|
||||
@@ -45,7 +44,7 @@ export const createAndStartServer = async (
|
||||
// Create the schema
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers
|
||||
resolvers,
|
||||
});
|
||||
|
||||
const server = new ApolloServer({
|
||||
@@ -54,46 +53,31 @@ export const createAndStartServer = async (
|
||||
context: async ({ req }) => {
|
||||
// https://www.apollographql.com/docs/apollo-server/v3/security/authentication#api-wide-authorization
|
||||
|
||||
const { address } = req.session;
|
||||
const { userId } = req.session;
|
||||
|
||||
if (!address) {
|
||||
if (!userId) {
|
||||
throw new AuthenticationError('Unauthorized: No active session');
|
||||
}
|
||||
|
||||
// Find/create user from ETH address in request session
|
||||
const user = await service.loadOrCreateUser(address);
|
||||
const user = await service.getUser(userId);
|
||||
|
||||
return { user };
|
||||
},
|
||||
plugins: [
|
||||
// Proper shutdown for the HTTP server
|
||||
ApolloServerPluginDrainHttpServer({ httpServer }),
|
||||
ApolloServerPluginLandingPageLocalDefault({ embed: true })
|
||||
]
|
||||
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
|
||||
],
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
app.use(cors({
|
||||
origin: appOriginUrl,
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
const sessionOptions: session.SessionOptions = {
|
||||
secret: secret,
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||
// TODO: Set cookie maxAge and handle cookie expiry in frontend
|
||||
// maxAge: SESSION_COOKIE_MAX_AGE,
|
||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax'
|
||||
}
|
||||
};
|
||||
|
||||
if (domain) {
|
||||
sessionOptions.cookie!.domain = domain;
|
||||
}
|
||||
app.use(
|
||||
cors({
|
||||
origin: appOriginUrl,
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (trustProxy) {
|
||||
// trust first proxy
|
||||
@@ -101,7 +85,14 @@ export const createAndStartServer = async (
|
||||
}
|
||||
|
||||
app.use(
|
||||
session(sessionOptions)
|
||||
cookieSession({
|
||||
secret: secret,
|
||||
secure: new URL(appOriginUrl).protocol === 'https:',
|
||||
// 23 hours (less than 24 hours to avoid sessionSigs expiration issues)
|
||||
maxAge: 23 * 60 * 60 * 1000,
|
||||
sameSite: new URL(appOriginUrl).protocol === 'https:' ? 'none' : 'lax',
|
||||
domain: domain || undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
server.applyMiddleware({
|
||||
@@ -109,8 +100,8 @@ export const createAndStartServer = async (
|
||||
path: gqlPath,
|
||||
cors: {
|
||||
origin: [appOriginUrl],
|
||||
credentials: true
|
||||
}
|
||||
credentials: true,
|
||||
},
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -161,32 +161,55 @@ export class Service {
|
||||
});
|
||||
}
|
||||
|
||||
async loadOrCreateUser (ethAddress: string): Promise<User> {
|
||||
// Get user by ETH address
|
||||
let user = await this.db.getUser({
|
||||
async getUserByEmail(email: string): Promise<User | null> {
|
||||
return await this.db.getUser({
|
||||
where: {
|
||||
email
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getUserBySubOrgId(subOrgId: string): Promise<User | null> {
|
||||
return await this.db.getUser({
|
||||
where: {
|
||||
subOrgId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getUserByEthAddress (ethAddress: string): Promise<User | null> {
|
||||
return await this.db.getUser({
|
||||
where: {
|
||||
ethAddress
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
const [org] = await this.db.getOrganizations({});
|
||||
assert(org, 'No organizations exists in database');
|
||||
async createUser (params: {
|
||||
name: string
|
||||
email: string
|
||||
subOrgId: string
|
||||
ethAddress: string
|
||||
turnkeyWalletId: string
|
||||
}): Promise<User> {
|
||||
const [org] = await this.db.getOrganizations({});
|
||||
assert(org, 'No organizations exists in database');
|
||||
|
||||
// Create user with new address
|
||||
user = await this.db.addUser({
|
||||
email: `${ethAddress}@example.com`,
|
||||
name: ethAddress,
|
||||
isVerified: true,
|
||||
ethAddress
|
||||
});
|
||||
// Create user with new address
|
||||
const user = await this.db.addUser({
|
||||
email: params.email,
|
||||
name: params.name,
|
||||
subOrgId: params.subOrgId,
|
||||
ethAddress: params.ethAddress,
|
||||
isVerified: true,
|
||||
turnkeyWalletId: params.turnkeyWalletId,
|
||||
});
|
||||
|
||||
await this.db.addUserOrganization({
|
||||
member: user,
|
||||
organization: org,
|
||||
role: Role.Owner
|
||||
});
|
||||
}
|
||||
await this.db.addUserOrganization({
|
||||
member: user,
|
||||
organization: org,
|
||||
role: Role.Owner
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Turnkey, TurnkeyApiTypes } from '@turnkey/sdk-server';
|
||||
|
||||
// Default path for the first Ethereum address in a new HD wallet.
|
||||
// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form:
|
||||
// m / purpose' / coin_type' / account' / change / address_index
|
||||
// - Purpose is a constant set to 44' following the BIP43 recommendation.
|
||||
// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
|
||||
// - Account, Change, and Address Index are set to 0
|
||||
import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-server';
|
||||
import { getConfig } from './utils';
|
||||
import { Service } from './service';
|
||||
|
||||
type TAttestation = TurnkeyApiTypes['v1Attestation'];
|
||||
|
||||
type CreateUserParams = {
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
challenge: string;
|
||||
attestation: TAttestation;
|
||||
};
|
||||
|
||||
export async function createUser(
|
||||
service: Service,
|
||||
{ userName, userEmail, challenge, attestation }: CreateUserParams,
|
||||
) {
|
||||
try {
|
||||
if (await service.getUserByEmail(userEmail)) {
|
||||
throw new Error(`User already exists: ${userEmail}`);
|
||||
}
|
||||
|
||||
const config = await getConfig();
|
||||
const turnkey = new Turnkey(config.turnkey);
|
||||
|
||||
const apiClient = turnkey.api();
|
||||
|
||||
const walletName = `Default ETH Wallet`;
|
||||
|
||||
const createSubOrgResponse = await apiClient.createSubOrganization({
|
||||
subOrganizationName: `Default SubOrg for ${userEmail}`,
|
||||
rootQuorumThreshold: 1,
|
||||
rootUsers: [
|
||||
{
|
||||
userName,
|
||||
userEmail,
|
||||
apiKeys: [],
|
||||
authenticators: [
|
||||
{
|
||||
authenticatorName: 'Passkey',
|
||||
challenge,
|
||||
attestation,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
wallet: {
|
||||
walletName: walletName,
|
||||
accounts: DEFAULT_ETHEREUM_ACCOUNTS,
|
||||
},
|
||||
});
|
||||
|
||||
const subOrgId = refineNonNull(createSubOrgResponse.subOrganizationId);
|
||||
const wallet = refineNonNull(createSubOrgResponse.wallet);
|
||||
|
||||
const result = {
|
||||
id: wallet.walletId,
|
||||
address: wallet.addresses[0],
|
||||
subOrgId: subOrgId,
|
||||
};
|
||||
console.log('Turnkey success', result);
|
||||
|
||||
const user = await service.createUser({
|
||||
name: userName,
|
||||
email: userEmail,
|
||||
subOrgId,
|
||||
ethAddress: wallet.addresses[0],
|
||||
turnkeyWalletId: wallet.walletId,
|
||||
});
|
||||
console.log('New user', user);
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.error('Failed to create user:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function authenticateUser(
|
||||
service: Service,
|
||||
signedWhoamiRequest: {
|
||||
url: string;
|
||||
body: any;
|
||||
stamp: {
|
||||
stampHeaderName: string;
|
||||
stampHeaderValue: string;
|
||||
};
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const tkRes = await fetch(signedWhoamiRequest.url, {
|
||||
method: 'POST',
|
||||
body: signedWhoamiRequest.body,
|
||||
headers: {
|
||||
[signedWhoamiRequest.stamp.stampHeaderName]:
|
||||
signedWhoamiRequest.stamp.stampHeaderValue,
|
||||
},
|
||||
});
|
||||
console.log('AUTH RESULT', tkRes.status);
|
||||
if (tkRes.status !== 200) {
|
||||
console.log(await tkRes.text());
|
||||
return null;
|
||||
}
|
||||
const orgId = (await tkRes.json()).organizationId;
|
||||
const user = await service.getUserBySubOrgId(orgId);
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.error('Failed to authenticate:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function refineNonNull<T>(
|
||||
input: T | null | undefined,
|
||||
errorMessage?: string,
|
||||
): T {
|
||||
if (input == null) {
|
||||
throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
@@ -3,11 +3,18 @@ import path from 'path';
|
||||
import toml from 'toml';
|
||||
import debug from 'debug';
|
||||
import { DataSource, DeepPartial, EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
import { Config } from './config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from './constants';
|
||||
|
||||
const log = debug('snowball:utils');
|
||||
|
||||
export const getConfig = async <ConfigType>(
|
||||
configFile: string
|
||||
export async function getConfig() {
|
||||
// TODO: get config path using cli
|
||||
return await _getConfig<Config>(DEFAULT_CONFIG_FILE_PATH);
|
||||
}
|
||||
|
||||
const _getConfig = async <ConfigType>(
|
||||
configFile: string,
|
||||
): Promise<ConfigType> => {
|
||||
const configFilePath = path.resolve(configFile);
|
||||
const fileExists = await fs.pathExists(configFilePath);
|
||||
@@ -41,7 +48,7 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
entityType: EntityTarget<Entity>,
|
||||
dataSource: DataSource,
|
||||
entities: any,
|
||||
relations?: any | undefined
|
||||
relations?: any | undefined,
|
||||
): Promise<Entity[]> => {
|
||||
const entityRepository = dataSource.getRepository(entityType);
|
||||
|
||||
@@ -56,7 +63,7 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
|
||||
entity = {
|
||||
...entity,
|
||||
[field]: relations[field][entityData[valueIndex]]
|
||||
[field]: relations[field][entityData[valueIndex]],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -67,4 +74,5 @@ export const loadAndSaveData = async <Entity extends ObjectLiteral>(
|
||||
return savedEntity;
|
||||
};
|
||||
|
||||
export const sleep = async (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
|
||||
export const sleep = async (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import { Registry } from '@cerc-io/laconic-sdk';
|
||||
import { Registry } from '@snowballtools/laconic-sdk';
|
||||
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
import { Config } from '../src/config';
|
||||
|
||||
@@ -2,7 +2,7 @@ import debug from 'debug';
|
||||
import { DataSource } from 'typeorm';
|
||||
import path from 'path';
|
||||
|
||||
import { Registry } from '@cerc-io/laconic-sdk';
|
||||
import { Registry } from '@snowballtools/laconic-sdk';
|
||||
|
||||
import { Config } from '../src/config';
|
||||
import { DEFAULT_CONFIG_FILE_PATH } from '../src/constants';
|
||||
|
||||
@@ -9,9 +9,18 @@
|
||||
```
|
||||
|
||||
- Run script to deploy app
|
||||
```
|
||||
./deploy-frontend.sh
|
||||
```
|
||||
|
||||
- To deploy frontend app to `dashboard.staging.apps.snowballtools.com`
|
||||
|
||||
```
|
||||
./deploy-frontend.staging.sh
|
||||
```
|
||||
|
||||
- To deploy frontend app to `dashboard.apps.snowballtools.com`
|
||||
|
||||
```
|
||||
./deploy-frontend.sh
|
||||
```
|
||||
|
||||
- Commit the updated [ApplicationRecord](records/application-record.yml) and [ApplicationDeploymentRequest](records/application-deployment-request.yml) files to the repository
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
cns:
|
||||
restEndpoint: 'http://console.laconic.com:1317'
|
||||
gqlEndpoint: 'http://console.laconic.com:9473/api'
|
||||
userKey: 87d00f66a73e2ca428adeb49ba9164d0ad9a87edc60e33d46ad3031b9c5701fe
|
||||
bondId: 098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661
|
||||
chainId: laconic_9000-1
|
||||
gas: 550000
|
||||
fees: 200000aphoton
|
||||
@@ -35,12 +35,12 @@ record:
|
||||
dns: dashboard
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_app_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_app_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_app_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_app_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_app_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api-001.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: b7c63b235ca1dd5639ab
|
||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools-platform/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
meta:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Extract version from ../frontend/package.json
|
||||
PACKAGE_VERSION=$(jq -r '.version' ../frontend/package.json)
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=config.staging.yml
|
||||
REGISTRY_BOND_ID="098c906850b87412f02200e41f449bc79e055eab77acfef32c0b22443bb46661"
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "staging-snowballtools-base-frontend" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
cat > ./staging-records/application-deployment-request.yml <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@$PACKAGE_VERSION
|
||||
dns: dashboard.staging.apps.snowballtools.com
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
|
||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
||||
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
||||
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
|
||||
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
|
||||
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
||||
meta:
|
||||
note: Added by Snowball @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
EOF
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
cat > ./staging-records/application-record.yml <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: staging-snowballtools-base-frontend
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
echo "Files generated successfully."
|
||||
|
||||
RECORD_FILE=staging-records/application-record.yml
|
||||
|
||||
# Publish ApplicationRecord
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_CRN="crn://staging-snowballtools/applications/staging-snowballtools-base-frontend"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
|
||||
# Check if record found for REGISTRY_APP_CRN
|
||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_CRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RECORD_FILE=staging-records/application-deployment-request.yml
|
||||
|
||||
sleep 2
|
||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
@@ -4,6 +4,6 @@
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cerc-io/laconic-registry-cli": "^0.1.10"
|
||||
"@snowballtools/laconic-registry-cli": "^0.1.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: staging-snowballtools-base-frontend@0.0.0
|
||||
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@0.0.0
|
||||
dns: dashboard.staging.apps.snowballtools.com
|
||||
config:
|
||||
env:
|
||||
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
|
||||
LACONIC_HOSTED_CONFIG_github_clientid: 905c09553f527d2cdff5
|
||||
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
|
||||
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools-platform/image-upload-pwa-example
|
||||
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
|
||||
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
|
||||
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
|
||||
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
|
||||
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: localhost
|
||||
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com/
|
||||
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
|
||||
meta:
|
||||
note: Added by Snowball @ Tuesday 21 May 2024 06:17:23 AM UTC
|
||||
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
|
||||
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
|
||||
@@ -0,0 +1,8 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.2
|
||||
repository_ref: 8488cfab8353321ed05c4234bf1b914c9ad3aa99
|
||||
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
|
||||
app_type: webapp
|
||||
name: staging-snowballtools-base-frontend
|
||||
app_version: 0.0.0
|
||||
@@ -0,0 +1,23 @@
|
||||
# deployer test
|
||||
|
||||
Check if the live web app deployer is in a working state
|
||||
|
||||
- Web app repo used: https://github.com/snowball-tools-platform/test-progressive-web-app (main branch)
|
||||
- Config used: [../config.yml](../config.yml)
|
||||
- The script [test-webapp-deployment-undeployment.sh](./test-webapp-deployment-undeployment.sh) performs the following:
|
||||
- Create / update [`ApplicationRecord`](./records/application-record.yml) and [`ApplicationDeploymentRequest`](./records/application-deployment-request.yml) records with latest meta data from the repo
|
||||
- Fetch the latest version of `deployment-test-app` from registry and increment `ApplicationRecord` version
|
||||
- Publish the resulting `ApplicationRecord` record
|
||||
- Set names to the record and check name resolution
|
||||
- Publish the `ApplicationDeploymentRequest` record
|
||||
- Check that the deployment occurs
|
||||
- Check that a `ApplicationDeploymentRecord` is created
|
||||
- Check that the deployment record has correct `ApplicationRecord` id
|
||||
- Check that the URL present in deployment record is active
|
||||
- Create and publish a [`ApplicationDeploymentRemovalRequest`](./records/application-deployment-removal-request.yml) record
|
||||
- Check that the deployment is removed
|
||||
- Check that a `ApplicationDeploymentRemovalRecord` is created
|
||||
- Check that the deployment URL goes down
|
||||
- The test script is run in a GitHub CI [workflow](../../../.github/workflows/test-app-deployment.yaml) that:
|
||||
- Is scheduled to run everyday on the default (`main`) branch or can be triggered manually
|
||||
- Sends Slack alerts to configured channels on failure
|
||||
@@ -0,0 +1,4 @@
|
||||
record:
|
||||
deployment: <APPLICATION_DEPLOYMENT_RECORD_ID>
|
||||
type: ApplicationDeploymentRemovalRequest
|
||||
version: 1.0.0
|
||||
@@ -0,0 +1,15 @@
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: deployment-test-app@0.1.24
|
||||
application: crn://snowballtools/applications/deployment-test-app@0.1.24
|
||||
dns: deployment-ci-test
|
||||
config:
|
||||
env:
|
||||
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
|
||||
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
|
||||
CERC_WEBAPP_DEBUG: 0
|
||||
meta:
|
||||
note: Deployment test @ Thu 11 Apr 2024 07:29:19 AM UTC
|
||||
repository: "https://github.com/snowball-tools-platform/test-progressive-web-app"
|
||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
||||
@@ -0,0 +1,8 @@
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: 0.0.1
|
||||
repository_ref: 05819619487a0d2dbc5453b6d1ccff3044c0dd26
|
||||
repository: ["https://github.com/snowball-tools-platform/test-progressive-web-app"]
|
||||
app_type: webapp
|
||||
name: deployment-test-app
|
||||
app_version: 0.1.24
|
||||
@@ -0,0 +1,225 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Repository URL
|
||||
REPO_URL="https://github.com/snowball-tools-platform/test-progressive-web-app"
|
||||
|
||||
# Get the latest commit hash from the repository
|
||||
LATEST_HASH=$(git ls-remote $REPO_URL HEAD | awk '{print $1}')
|
||||
|
||||
# Fetch the package.json file content
|
||||
# Extract version from package.json content
|
||||
package_json=$(wget -qO- "$REPO_URL/raw/$LATEST_HASH/package.json")
|
||||
PACKAGE_VERSION=$(echo "$package_json" | jq -r '.version')
|
||||
|
||||
# Current date and time for note
|
||||
CURRENT_DATE_TIME=$(date -u)
|
||||
|
||||
CONFIG_FILE=packages/deployer/config.yml
|
||||
REGISTRY_BOND_ID="99c0e9aec0ac1b8187faa579be3b54f93fafb6060ac1fd29170b860df605be32"
|
||||
|
||||
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
|
||||
|
||||
APP_NAME=deployment-test-app
|
||||
|
||||
# Get latest version from registry and increment application-record version
|
||||
NEW_APPLICATION_VERSION=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
|
||||
|
||||
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
|
||||
# Set application-record version if no previous records were found
|
||||
NEW_APPLICATION_VERSION=0.0.1
|
||||
fi
|
||||
|
||||
# Generate application-record.yml with incremented version
|
||||
RECORD_FILE=packages/deployer/test/records/application-record.yml
|
||||
|
||||
cat > $RECORD_FILE <<EOF
|
||||
record:
|
||||
type: ApplicationRecord
|
||||
version: $NEW_APPLICATION_VERSION
|
||||
repository_ref: $LATEST_HASH
|
||||
repository: ["$REPO_URL"]
|
||||
app_type: webapp
|
||||
name: $APP_NAME
|
||||
app_version: $PACKAGE_VERSION
|
||||
EOF
|
||||
|
||||
# Generate application-deployment-request.yml
|
||||
REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-request.yml
|
||||
|
||||
cat > $REQUEST_RECORD_FILE <<EOF
|
||||
record:
|
||||
type: ApplicationDeploymentRequest
|
||||
version: '1.0.0'
|
||||
name: $APP_NAME@$PACKAGE_VERSION
|
||||
application: crn://snowballtools/applications/$APP_NAME@$PACKAGE_VERSION
|
||||
dns: deployment-ci-test
|
||||
config:
|
||||
env:
|
||||
CERC_TEST_WEBAPP_CONFIG1: "deployment test config 1"
|
||||
CERC_TEST_WEBAPP_CONFIG2: "deployment test config 2"
|
||||
CERC_WEBAPP_DEBUG: 0
|
||||
meta:
|
||||
note: Deployment test @ $CURRENT_DATE_TIME
|
||||
repository: "$REPO_URL"
|
||||
repository_ref: $LATEST_HASH
|
||||
EOF
|
||||
|
||||
echo "Record files generated successfully."
|
||||
|
||||
# Publish ApplicationRecord
|
||||
RECORD_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationRecord published"
|
||||
echo $RECORD_ID
|
||||
|
||||
# Set name to record
|
||||
REGISTRY_APP_CRN="crn://snowballtools/applications/$APP_NAME"
|
||||
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${PACKAGE_VERSION}" "$RECORD_ID"
|
||||
sleep 2
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN@${LATEST_HASH}" "$RECORD_ID"
|
||||
sleep 2
|
||||
# Set name if latest release
|
||||
yarn --silent laconic -c $CONFIG_FILE cns name set "$REGISTRY_APP_CRN" "$RECORD_ID"
|
||||
echo "$REGISTRY_APP_CRN set for ApplicationRecord"
|
||||
|
||||
# Check if record exists for REGISTRY_APP_CRN
|
||||
APP_RECORD=$(yarn --silent laconic -c $CONFIG_FILE cns name resolve "$REGISTRY_APP_CRN" | jq '.[0]')
|
||||
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
|
||||
echo "No record found for $REGISTRY_APP_CRN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
DEPLOYMENT_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRequest published"
|
||||
echo $DEPLOYMENT_REQUEST_ID
|
||||
|
||||
# Deployment checks
|
||||
RETRY_INTERVAL=30
|
||||
MAX_RETRIES=20
|
||||
|
||||
# Check that a ApplicationDeploymentRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
deployment_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRecord --all --name "$APP_NAME" request $DEPLOYMENT_REQUEST_ID)
|
||||
len_deployment_records=$(echo $deployment_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
if [ $len_deployment_records -eq 0 ]; then
|
||||
# Check if retries are exhausted
|
||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "ApplicationDeploymentRecord for deployment request $DEPLOYMENT_REQUEST_ID not found, exiting"
|
||||
exit 1
|
||||
else
|
||||
echo "ApplicationDeploymentRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||
sleep $RETRY_INTERVAL
|
||||
retry_count=$((retry_count+1))
|
||||
fi
|
||||
else
|
||||
echo "ApplicationDeploymentRecord found"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
DEPLOYMENT_RECORD_ID=$(echo $deployment_records_response | jq -r '.[0].id')
|
||||
echo $DEPLOYMENT_RECORD_ID
|
||||
|
||||
# Check if ApplicationDeploymentRecord has the correct record id
|
||||
fetched_application_record_id=$(echo $deployment_records_response | jq -r '.[0].attributes.application')
|
||||
if [ "$fetched_application_record_id" = "$RECORD_ID" ]; then
|
||||
echo "ApplicationRecord id matched"
|
||||
else
|
||||
echo "ApplicationRecord id does not match, expected: $RECORD_ID, received: $fetched_application_record_id"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the url present in ApplicationDeploymentRecord is active
|
||||
fetched_url=$(echo $deployment_records_response | jq -r '.[0].attributes.url')
|
||||
|
||||
retry_count=0
|
||||
max_retries=10
|
||||
retry_interval=5
|
||||
while true; do
|
||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||
if [ "$url_response" = "200" ]; then
|
||||
echo "Deployment URL $fetched_url is active"
|
||||
break
|
||||
else
|
||||
if [ $retry_count -eq $max_retries ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "Deployment URL $fetched_url is not active, exiting"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment URL $fetched_url is not active, received code $url_response, retrying in $retry_interval sec..."
|
||||
sleep $retry_interval
|
||||
retry_count=$((retry_count+1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Generate application-deployment-removal-request.yml
|
||||
REMOVAL_REQUEST_RECORD_FILE=packages/deployer/test/records/application-deployment-removal-request.yml
|
||||
|
||||
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
|
||||
record:
|
||||
deployment: $DEPLOYMENT_RECORD_ID
|
||||
type: ApplicationDeploymentRemovalRequest
|
||||
version: 1.0.0
|
||||
EOF
|
||||
|
||||
sleep 2
|
||||
REMOVAL_REQUEST_ID=$(yarn --silent laconic -c $CONFIG_FILE cns record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
|
||||
echo "ApplicationDeploymentRemovalRequest published"
|
||||
echo $REMOVAL_REQUEST_ID
|
||||
|
||||
# Check that an ApplicationDeploymentRemovalRecord is published
|
||||
retry_count=0
|
||||
while true; do
|
||||
removal_records_response=$(yarn --silent laconic -c $CONFIG_FILE cns record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
|
||||
len_removal_records=$(echo $removal_records_response | jq 'length')
|
||||
|
||||
# Check if number of records returned is 0
|
||||
if [ $len_removal_records -eq 0 ]; then
|
||||
# Check if retries are exhausted
|
||||
if [ $retry_count -eq $MAX_RETRIES ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
|
||||
exit 1
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
|
||||
sleep $RETRY_INTERVAL
|
||||
retry_count=$((retry_count+1))
|
||||
fi
|
||||
else
|
||||
echo "ApplicationDeploymentRemovalRecord found"
|
||||
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
|
||||
echo $REMOVAL_RECORD_ID
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if the application url is down after deployment removal
|
||||
retry_count=0
|
||||
max_retries=10
|
||||
retry_interval=5
|
||||
while true; do
|
||||
url_response=$(curl -s -o /dev/null -I -w "%{http_code}" $fetched_url)
|
||||
if [ "$url_response" = "404" ]; then
|
||||
echo "Deployment URL $fetched_url is down"
|
||||
break
|
||||
else
|
||||
if [ $retry_count -eq $max_retries ]; then
|
||||
echo "Retries exhausted"
|
||||
echo "Deployment URL $fetched_url is still active, exiting"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment URL $fetched_url is still active, received code $url_response, retrying in $retry_interval sec..."
|
||||
sleep $retry_interval
|
||||
retry_count=$((retry_count+1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Test successful"
|
||||
@@ -1,7 +1,17 @@
|
||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
||||
VITE_SERVER_URL='http://localhost:8000'
|
||||
|
||||
REACT_APP_GITHUB_CLIENT_ID =
|
||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
||||
VITE_GITHUB_CLIENT_ID=
|
||||
VITE_GITHUB_PWA_TEMPLATE_REPO="snowball-tools/test-progressive-web-app"
|
||||
VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO="snowball-tools/image-upload-pwa-example"
|
||||
|
||||
REACT_APP_WALLET_CONNECT_ID =
|
||||
VITE_WALLET_CONNECT_ID=
|
||||
|
||||
VITE_LIT_RELAY_API_KEY=
|
||||
|
||||
VITE_ALCHEMY_API_KEY=
|
||||
|
||||
VITE_BUGSNAG_API_KEY=
|
||||
|
||||
VITE_PASSKEY_WALLET_RPID=
|
||||
VITE_TURNKEY_API_BASE_URL=
|
||||
VITE_TURNKEY_ORGANIZATION_ID=
|
||||
@@ -1 +0,0 @@
|
||||
build
|
||||
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 13,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint"],
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,3 +22,4 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*storybook.log
|
||||
@@ -0,0 +1 @@
|
||||
v20.12.1
|
||||
@@ -1,3 +0,0 @@
|
||||
# artifacts
|
||||
build
|
||||
coverage
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
import { join, dirname } from 'path';
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
|
||||
*/
|
||||
function getAbsolutePath(value: string): any {
|
||||
return dirname(require.resolve(join(value, 'package.json')));
|
||||
}
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
getAbsolutePath('@storybook/addon-onboarding'),
|
||||
getAbsolutePath('@storybook/addon-links'),
|
||||
getAbsolutePath('@storybook/addon-essentials'),
|
||||
getAbsolutePath('@chromatic-com/storybook'),
|
||||
getAbsolutePath('@storybook/addon-interactions'),
|
||||
getAbsolutePath('storybook-addon-remix-react-router'),
|
||||
],
|
||||
framework: {
|
||||
name: getAbsolutePath('@storybook/react-vite'),
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
|
||||
import '../src/index.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
+22
-38
@@ -1,46 +1,30 @@
|
||||
# Getting Started with Create React App
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using [typescript-tailwindcss-eslint-prettier](https://github.com/cufarvid/cra-templates) template.
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
## Available Scripts
|
||||
Currently, two official plugins are available:
|
||||
|
||||
In the project directory, you can run:
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
### `yarn start`
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
### `yarn test`
|
||||
```js
|
||||
export default {
|
||||
// other rules...
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
|
||||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"projectId": "Project:663d04870db27ed66a48e466",
|
||||
"zip": true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="snowball tools dashboard" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Snowball</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="500" height="500" fill="#0F86F5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M191.873 125.126C224.893 126.765 250.458 150.121 274.042 172.995C297.925 196.158 323.089 221.108 324.868 254.114C326.718 288.42 308.902 321.108 283.281 344.355C258.67 366.687 225.288 373.859 191.873 374.788C157.228 375.752 119.038 374.394 95.1648 349.588C71.6207 325.125 74.6696 287.843 75.7341 254.114C76.7518 221.865 79.2961 188.525 101.009 164.41C123.845 139.047 157.543 123.423 191.873 125.126Z" fill="#4BA4F7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M229.373 125.126C262.393 126.765 287.958 150.121 311.542 172.995C335.425 196.158 360.589 221.108 362.368 254.114C364.218 288.42 346.402 321.108 320.781 344.355C296.17 366.687 262.788 373.859 229.373 374.788C194.728 375.752 156.538 374.394 132.665 349.588C109.121 325.125 112.17 287.843 113.234 254.114C114.252 221.865 116.796 188.525 138.509 164.41C161.345 139.047 195.043 123.423 229.373 125.126Z" fill="#8AC4FA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M266.873 125.126C299.893 126.765 325.458 150.121 349.042 172.995C372.925 196.158 398.089 221.108 399.868 254.114C401.718 288.42 383.902 321.108 358.281 344.355C333.67 366.687 300.288 373.859 266.873 374.788C232.228 375.752 194.038 374.394 170.165 349.588C146.621 325.125 149.67 287.843 150.734 254.114C151.752 221.865 154.296 188.525 176.009 164.41C198.845 139.047 232.543 123.423 266.873 125.126Z" fill="#CAE4FD"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M304.373 125.126C337.393 126.765 362.958 150.121 386.542 172.995C410.425 196.158 435.589 221.108 437.368 254.114C439.218 288.42 421.402 321.108 395.781 344.355C371.17 366.687 337.788 373.859 304.373 374.788C269.728 375.752 231.538 374.394 207.665 349.588C184.121 325.125 187.17 287.843 188.234 254.114C189.252 221.865 191.796 188.525 213.509 164.41C236.345 139.047 270.043 123.423 304.373 125.126Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,11 +1,23 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"build": "vite build",
|
||||
"lint": "tsc --noEmit",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bugsnag/browser-performance": "^2.4.1",
|
||||
"@bugsnag/js": "^7.22.7",
|
||||
"@bugsnag/plugin-react": "^7.22.7",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.0.19",
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
@@ -15,14 +27,22 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@snowballtools/auth": "^0.2.0",
|
||||
"@snowballtools/auth-lit": "^0.2.0",
|
||||
"@snowballtools/js-sdk": "^0.1.1",
|
||||
"@snowballtools/link-lit-alchemy-light": "^0.2.0",
|
||||
"@snowballtools/material-tailwind-react-fork": "^2.1.10",
|
||||
"@snowballtools/smartwallet-alchemy-light": "^0.2.0",
|
||||
"@snowballtools/types": "^0.2.0",
|
||||
"@snowballtools/utils": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.22.2",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@turnkey/http": "^2.10.0",
|
||||
"@turnkey/sdk-react": "^0.1.0",
|
||||
"@turnkey/webauthn-stamper": "^0.5.0",
|
||||
"@walletconnect/ethereum-provider": "^2.12.2",
|
||||
"@web3modal/siwe": "^4.0.5",
|
||||
"@web3modal/wagmi": "^4.0.5",
|
||||
"assert": "^2.1.0",
|
||||
@@ -30,7 +50,6 @@
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"downshift": "^8.3.2",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"framer-motion": "^11.0.8",
|
||||
"gql-client": "^1.0.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
@@ -46,54 +65,41 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-oauth-popup": "^1.0.5",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"siwe": "^2.1.4",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.15.1",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
"uuid": "^9.0.1",
|
||||
"viem": "^2.7.11",
|
||||
"wagmi": "^2.5.7",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@chromatic-com/storybook": "^1.3.3",
|
||||
"@storybook/addon-essentials": "^8.0.10",
|
||||
"@storybook/addon-interactions": "^8.0.10",
|
||||
"@storybook/addon-links": "^8.0.10",
|
||||
"@storybook/addon-onboarding": "^8.0.10",
|
||||
"@storybook/blocks": "^8.0.10",
|
||||
"@storybook/react": "^8.0.10",
|
||||
"@storybook/react-vite": "^8.0.10",
|
||||
"@storybook/test": "^8.0.10",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@types/luxon": "^3.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"chromatic": "^11.3.2",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.1.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
"storybook": "^8.0.10",
|
||||
"storybook-addon-remix-react-router": "^3.0.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="197" height="2" viewBox="0 0 197 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0.5" y1="1.19141" x2="197" y2="1.19141" stroke="#94A7B8" stroke-dasharray="1 12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 196 B |
File diff suppressed because one or more lines are too long
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
(cd /Users/rabbit-m2/p/snowball/js-sdk && NO_CLEAN=1 turbo build)
|
||||
|
||||
(cd ../.. && ./scripts/yarn-file-for-local-dev.sh)
|
||||
|
||||
rm -rf node_modules/.vite
|
||||
|
||||
yarn dev
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import Projects from './pages/org-slug';
|
||||
@@ -9,8 +8,10 @@ import {
|
||||
} from './pages/org-slug/projects/routes';
|
||||
import ProjectSearchLayout from './layouts/ProjectSearch';
|
||||
import Index from './pages';
|
||||
import Login from './pages/Login';
|
||||
import { DashboardLayout } from 'pages/org-slug/layout';
|
||||
import AuthPage from './pages/AuthPage';
|
||||
import { DashboardLayout } from './pages/org-slug/layout';
|
||||
import { useEffect } from 'react';
|
||||
import Web3Provider from 'context/Web3Provider';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -46,12 +47,36 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
element: <AuthPage />,
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
element: <AuthPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
// Hacky way of checking session
|
||||
// TODO: Handle redirect backs
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.VITE_SERVER_URL}/auth/session`, {
|
||||
credentials: 'include',
|
||||
}).then((res) => {
|
||||
if (res.status !== 200) {
|
||||
localStorage.clear();
|
||||
const path = window.location.pathname;
|
||||
if (path !== '/login' && path !== '/signup') {
|
||||
window.location.pathname = '/login';
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Web3Provider>
|
||||
<RouterProvider router={router} />
|
||||
</Web3Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -3,14 +3,14 @@ export default [
|
||||
id: '1',
|
||||
name: 'Progressive Web App (PWA)',
|
||||
icon: 'pwa',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||
repoFullName: `${import.meta.env.VITE_GITHUB_PWA_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Image Upload PWA',
|
||||
icon: 'pwa',
|
||||
repoFullName: `${process.env.REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||
repoFullName: `${import.meta.env.VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO}`,
|
||||
isComingSoon: false,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = React.PropsWithChildren<{
|
||||
className?: string;
|
||||
snowZIndex?: number;
|
||||
}>;
|
||||
|
||||
export const CloudyFlow = ({ className, children, snowZIndex }: Props) => {
|
||||
return (
|
||||
<div className={`bg-sky-100 relative ${className || ''}`}>
|
||||
{children}
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ zIndex: snowZIndex || 0 }}
|
||||
>
|
||||
<div className="w-[3.72px] h-[3.72px] left-[587px] top-[147px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.72px] h-[4.72px] left-[742px] top-[336px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.49px] h-[3.49px] left-[36px] top-[68px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[55px] top-[114px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.60px] h-[5.60px] left-[1334px] top-[63px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.53px] h-[3.53px] left-[988px] top-[108px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.65px] h-[2.65px] left-[1380px] top-[16px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.60px] h-[3.60px] left-[1284px] top-[95px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-0.5 h-0.5 left-[1191px] top-[376px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.83px] h-[2.83px] left-[1182px] top-[257px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.41px] h-[2.41px] left-[627px] top-[26px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.71px] h-[5.71px] left-[30px] top-[33px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.09px] h-[4.09px] left-[425px] top-[386px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.38px] h-[3.38px] left-[394px] top-[29px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.70px] h-[4.70px] left-[817px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1.5 h-1.5 left-[1194px] top-[332px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.89px] h-[4.89px] left-[811px] top-[76px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.25px] h-[4.25px] left-[458px] top-[366px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.82px] h-[4.82px] left-[936px] top-[46px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[64px] top-[132px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1 h-1 left-[763px] top-[10px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.67px] h-[3.67px] left-[861px] top-[106px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.62px] h-[3.62px] left-[710px] top-[278px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.45px] h-[3.45px] left-[1069px] top-[329px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.92px] h-[2.92px] left-[1286px] top-[299px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.84px] h-[4.84px] left-[219px] top-[269px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.39px] h-[2.39px] left-[817px] top-[121px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.83px] h-[5.83px] left-[168px] top-[320px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.94px] h-[5.94px] left-[419px] top-[244px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.67px] h-[4.67px] left-[604px] top-[309px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.87px] h-[5.87px] left-[1098px] top-[379px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.85px] h-[5.85px] left-[644px] top-[352px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.19px] h-[4.19px] left-[1361px] top-[349px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.84px] h-[2.84px] left-[1299px] top-[194px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.51px] h-[4.51px] left-[468px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.73px] h-[2.73px] left-[1084px] top-[86px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.43px] h-[3.43px] left-[1271px] top-[28px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.25px] h-[2.25px] left-[106px] top-[197px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.82px] h-[2.82px] left-[122px] top-[173px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.89px] h-[2.89px] left-[343px] top-[345px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.82px] h-[2.82px] left-[433px] top-[40px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[904px] top-[350px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.42px] h-[4.42px] left-[1066px] top-[349px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.67px] h-[4.67px] left-[904px] top-[317px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.54px] h-[5.54px] left-[501px] top-[336px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[1149px] top-[206px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.55px] h-[3.55px] left-[235px] top-[362px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.60px] h-[2.60px] left-[1246px] top-[1px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.94px] h-[2.94px] left-[788px] top-[6px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.19px] h-[4.19px] left-[527px] top-[365px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.13px] h-[4.13px] left-[201px] top-[53px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.94px] h-[2.94px] left-[765px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[1254px] top-[30px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.85px] h-[3.85px] left-[107px] top-[316px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.72px] h-[5.72px] left-[1305px] top-[8px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.46px] h-[5.46px] left-[102px] top-[316px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.77px] h-[3.77px] left-[1322px] top-[334px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.84px] h-[4.84px] left-[1370px] top-[317px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.55px] h-[5.55px] left-[945px] top-[258px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.24px] h-[2.24px] left-[266px] top-[362px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.89px] h-[2.89px] left-[987px] top-[156px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.46px] h-[3.46px] left-[10px] top-[168px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.67px] h-[5.67px] left-[441px] top-[291px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.07px] h-[4.07px] left-[962px] top-[364px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[599px] top-[293px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.41px] h-[4.41px] left-[358px] top-[163px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.31px] h-[2.31px] left-[670px] top-[182px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.60px] h-[2.60px] left-[621px] top-[257px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.16px] h-[2.16px] left-[48px] top-[322px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.91px] h-[5.91px] left-[491px] top-[5px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.50px] h-[5.50px] left-[1139px] top-[274px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[24px] top-[177px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[1166px] top-[316px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5px] h-[5px] left-[445px] top-[326px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.01px] h-[3.01px] left-[438px] top-[252px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.14px] h-[4.14px] left-[554px] top-[131px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.30px] h-[5.30px] left-[1010px] top-[116px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[437px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.87px] h-[5.87px] left-[948px] top-[27px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.87px] h-[2.87px] left-[826px] top-[20px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.89px] h-[3.89px] left-[1222px] top-[112px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.77px] h-[3.77px] left-[796px] top-[395px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.09px] h-[2.09px] left-[272px] top-[103px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.12px] h-[4.12px] left-[76px] top-[2px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[226px] top-[276px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.03px] h-[3.03px] left-[723px] top-[197px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.14px] h-[2.14px] left-[1259px] top-[17px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.28px] h-[3.28px] left-[1244px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.45px] h-[4.45px] left-[118px] top-[128px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.15px] h-[4.15px] left-[490px] top-[204px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[4.93px] h-[4.93px] left-[552px] top-[38px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.56px] h-[5.56px] left-[115px] top-[303px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.35px] h-[2.35px] left-[509px] top-[278px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.24px] h-[5.24px] left-[804px] top-[389px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.44px] h-[2.44px] left-[1013px] top-[50px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.69px] h-[3.69px] left-[1183px] top-[95px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.83px] h-[2.83px] left-[278px] top-[181px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.22px] h-[3.22px] left-[1316px] top-[282px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.55px] h-[3.55px] left-[736px] top-[119px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.29px] h-[2.29px] left-[483px] top-[319px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.14px] h-[2.14px] left-[1135px] top-[19px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.64px] h-[3.64px] left-[39px] top-[126px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.30px] h-[5.30px] left-[237px] top-[369px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.57px] h-[5.57px] left-[1156px] top-[126px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.78px] h-[2.78px] left-[1295px] top-[74px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-0.5 h-0.5 left-[76px] top-[227px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.61px] h-[3.61px] left-[108px] top-[89px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.37px] h-[5.37px] left-[191px] top-[167px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.18px] h-[4.18px] left-[164px] top-[117px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.15px] h-[5.15px] left-[533px] top-[261px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-1.5 h-1.5 left-[327px] top-[157px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.74px] h-[5.74px] left-[1242px] top-[122px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.22px] h-[4.22px] left-[129px] top-[265px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.30px] h-[2.30px] left-[1305px] top-[86px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.70px] h-[2.70px] left-[1235px] top-[120px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.15px] h-[2.15px] left-[596px] top-[103px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.17px] h-[2.17px] left-[483px] top-[233px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.09px] h-[5.09px] left-[706px] top-[188px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.15px] h-[4.15px] left-[141px] top-[2px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.20px] h-[4.20px] left-[48px] top-[124px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[1095px] top-[201px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.21px] h-[3.21px] left-[730px] top-[185px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.61px] h-[2.61px] left-[722px] top-[319px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.28px] h-[2.28px] left-[444px] top-[26px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.49px] h-[4.49px] left-[355px] top-[212px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.69px] h-[3.69px] left-[1280px] top-[312px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.23px] h-[4.23px] left-[1114px] top-[113px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.48px] h-[3.48px] left-[729px] top-[117px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.11px] h-[4.11px] left-[647px] top-[276px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.16px] h-[4.16px] left-[365px] top-[116px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.35px] h-[5.35px] left-[94px] top-[194px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.84px] h-[5.84px] left-[2px] top-[84px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.43px] h-[4.43px] left-[1382px] top-[23px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.38px] h-[5.38px] left-[857px] top-[284px] absolute bg-white rounded-full" />
|
||||
<div className="w-[2.77px] h-[2.77px] left-[1228px] top-[385px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.65px] h-[4.65px] left-[165px] top-[184px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[568px] top-[354px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.59px] h-[3.59px] left-[1303px] top-[371px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.84px] h-[5.84px] left-[235px] top-[188px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.84px] h-[3.84px] left-[902px] top-[211px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.45px] h-[3.45px] left-[367px] top-[161px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.08px] h-[4.08px] left-[855px] top-[394px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[383px] top-[47px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.39px] h-[4.39px] left-[1313px] top-[165px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.60px] h-[5.60px] left-[697px] top-[327px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.09px] h-[2.09px] left-[646px] top-[370px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.13px] h-[3.13px] left-[728px] top-[122px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.53px] h-[5.53px] left-[203px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.83px] h-[5.83px] left-[424px] top-[121px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.82px] h-[4.82px] left-[1358px] top-[176px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.18px] h-[3.18px] left-[1212px] top-[24px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.23px] h-[5.23px] left-[260px] top-[217px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.29px] h-[5.29px] left-[1204px] top-[367px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.47px] h-[3.47px] left-[1163px] top-[159px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.77px] h-[5.77px] left-[1257px] top-[115px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.31px] h-[5.31px] left-[222px] top-[356px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.43px] h-[5.43px] left-[1141px] top-[349px] absolute bg-white rounded-full" />
|
||||
<div className="w-[5.62px] h-[5.62px] left-[683px] top-[81px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.91px] h-[3.91px] left-[269px] top-[3px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.51px] h-[3.51px] left-[305px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.41px] h-[5.41px] left-[530px] top-[94px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.64px] h-[4.64px] left-[730px] top-[301px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.59px] h-[3.59px] left-[716px] top-[14px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.77px] h-[4.77px] left-[544px] top-[13px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.29px] h-[2.29px] left-[357px] top-[281px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.42px] h-[2.42px] left-[1346px] top-[112px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.42px] h-[3.42px] left-[671px] top-[150px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.40px] h-[4.40px] left-[1324px] top-[268px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.21px] h-[5.21px] left-[1028px] top-[376px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.27px] h-[4.27px] left-[499px] top-[50px] absolute bg-white rounded-full" />
|
||||
<div className="w-[4.35px] h-[4.35px] left-[543px] top-[359px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.25px] h-[5.25px] left-[1245px] top-[296px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.52px] h-[5.52px] left-[360px] top-[98px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.46px] h-[4.46px] left-[741px] top-[358px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[3.90px] h-[3.90px] left-[1262px] top-[184px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[5.75px] h-[5.75px] left-[552px] top-[335px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[4.95px] h-[4.95px] left-[120px] top-[178px] absolute bg-white rounded-full" />
|
||||
<div className="w-[3.28px] h-[3.28px] left-[1337px] top-[293px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.43px] h-[2.43px] left-[233px] top-[310px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-1 h-1 left-[218px] top-[322px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.68px] h-[3.68px] left-[984px] top-[8px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[2.44px] h-[2.44px] left-[832px] top-[55px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.93px] h-[3.93px] left-[1105px] top-[209px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.08px] h-[4.08px] left-[957px] top-[23px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[2.33px] h-[2.33px] left-[1066px] top-[390px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
<div className="w-[3.25px] h-[3.25px] left-[737px] top-[118px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.18px] h-[5.18px] left-[202px] top-[19px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.05px] h-[5.05px] left-[466px] top-[17px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.85px] h-[3.85px] left-[144px] top-[153px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.35px] h-[5.35px] left-[233px] top-[330px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-1 h-1 left-[730px] top-[179px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[4.46px] h-[4.46px] left-[1156px] top-[342px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.22px] h-[5.22px] left-[1275px] top-[204px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.50px] h-[5.50px] left-[38px] top-[343px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.14px] h-[5.14px] left-[867px] top-[113px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[2.19px] h-[2.19px] left-[1277px] top-[314px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[3.74px] h-[3.74px] left-[1136px] top-[197px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.37px] h-[5.37px] left-[34px] top-[226px] absolute bg-white bg-opacity-60 rounded-full" />
|
||||
<div className="w-[5.93px] h-[5.93px] left-[727px] top-[272px] absolute bg-white bg-opacity-50 rounded-full" />
|
||||
<div className="w-[5.29px] h-[5.29px] left-[277px] top-[43px] absolute bg-white bg-opacity-80 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
DayPicker,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverHandler,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import HorizontalLine from './HorizontalLine';
|
||||
|
||||
@@ -120,14 +120,11 @@ const DatePicker = ({
|
||||
handler={(value) => setIsOpen(value)}
|
||||
>
|
||||
<PopoverHandler>
|
||||
<Input
|
||||
onChange={() => null}
|
||||
value={inputValue}
|
||||
crossOrigin={undefined}
|
||||
/>
|
||||
<Input onChange={() => null} value={inputValue} />
|
||||
</PopoverHandler>
|
||||
{/* TODO: Figure out what placeholder is for */}
|
||||
<PopoverContent placeholder={''}>
|
||||
{/* @ts-ignore */}
|
||||
<PopoverContent>
|
||||
{mode === 'single' && (
|
||||
<DayPicker
|
||||
mode="single"
|
||||
@@ -152,7 +149,6 @@ const DatePicker = ({
|
||||
className="rounded-full mr-2"
|
||||
variant="outlined"
|
||||
onClick={() => setIsOpen(false)}
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -162,7 +158,6 @@ const DatePicker = ({
|
||||
className="rounded-full"
|
||||
color="gray"
|
||||
onClick={() => handleRangeSelect()}
|
||||
placeholder={''}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
default as ReactDropdown,
|
||||
Option as ReactDropdownOption,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration } from 'luxon';
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
export interface FormatMilliSecondProps
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
const HorizontalLine = () => {
|
||||
return <hr className="h-px bg-gray-100 border-0" />;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Heading } from './shared/Heading';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { StepperNav } from 'vertical-stepper-nav';
|
||||
import { StepperNav } from './VerticalStepper';
|
||||
|
||||
const COLOR_COMPLETED = '#059669';
|
||||
const COLOR_ACTIVE = '#CFE6FC';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { useStopwatch } from 'react-timer-hook';
|
||||
|
||||
import FormatMillisecond, { FormatMilliSecondProps } from './FormatMilliSecond';
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as CSS from 'csstype';
|
||||
|
||||
//
|
||||
// Nav
|
||||
//
|
||||
export interface IStepDescription {
|
||||
stepContent: () => JSX.Element;
|
||||
stepStateColor?: string;
|
||||
stepStatusCircleSize?: number;
|
||||
onClickHandler?: () => void | undefined;
|
||||
}
|
||||
|
||||
export interface IStepperNavProps {
|
||||
steps: IStepDescription[];
|
||||
}
|
||||
|
||||
export const StepperNav = (props: IStepperNavProps): JSX.Element => {
|
||||
return (
|
||||
<nav>
|
||||
{props.steps.map(
|
||||
(
|
||||
{ stepContent, stepStateColor, onClickHandler, stepStatusCircleSize },
|
||||
index,
|
||||
) => (
|
||||
<div key={index}>
|
||||
<Step
|
||||
stepContent={stepContent}
|
||||
statusColor={stepStateColor}
|
||||
onClickHandler={onClickHandler}
|
||||
statusCircleSize={stepStatusCircleSize}
|
||||
/>
|
||||
{index !== props.steps.length - 1 && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: `${(stepStatusCircleSize ?? 16) / 2 + 1}px`,
|
||||
}}
|
||||
>
|
||||
<Separator />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Separator
|
||||
//
|
||||
const separatorStyles = {
|
||||
height: '5vh',
|
||||
width: 2,
|
||||
border: '1px solid #E1E1E1',
|
||||
background: '#E1E1E1',
|
||||
};
|
||||
|
||||
export interface ISeparator {
|
||||
height?: string | number;
|
||||
}
|
||||
|
||||
export const Separator = ({ height }: ISeparator): JSX.Element => {
|
||||
return <div style={{ ...separatorStyles, height: height ?? '5vh' }} />;
|
||||
};
|
||||
|
||||
//
|
||||
// Step
|
||||
//
|
||||
export interface IStep {
|
||||
stepContent: () => JSX.Element;
|
||||
statusColor?: string;
|
||||
statusCircleSize?: number;
|
||||
onClickHandler?: (
|
||||
event?: React.MouseEvent<HTMLDivElement>,
|
||||
) => void | undefined;
|
||||
}
|
||||
|
||||
const buttonContainerStyles: CSS.Properties = {
|
||||
display: 'inline-flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '12px',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
export const Step = ({
|
||||
stepContent,
|
||||
statusColor,
|
||||
statusCircleSize,
|
||||
onClickHandler,
|
||||
}: IStep): JSX.Element => {
|
||||
const circleStyles = {
|
||||
borderRadius: statusCircleSize ?? 16,
|
||||
width: statusCircleSize ?? 16,
|
||||
height: statusCircleSize ?? 16,
|
||||
border: '2px solid #E1E1E1',
|
||||
background: statusColor ?? 'white',
|
||||
};
|
||||
|
||||
const keyDownHandler = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.keyCode === 13 || event.keyCode === 32) {
|
||||
onClickHandler?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
onClick={onClickHandler}
|
||||
onKeyDown={keyDownHandler}
|
||||
role="button"
|
||||
style={{ ...buttonContainerStyles }}
|
||||
>
|
||||
<div>
|
||||
<div style={circleStyles} />
|
||||
</div>
|
||||
<div style={{ paddingBottom: 2 }}>{stepContent()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface CancelDeploymentDialogProps extends ConfirmDialogProps {}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import { Deployment, Domain } from 'gql-client';
|
||||
import React from 'react';
|
||||
|
||||
import DeploymentDialogBodyCard from 'components/projects/project/deployments/DeploymentDialogBodyCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface DeleteDomainDialogProps extends ConfirmDialogProps {
|
||||
projectName: string;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface DeleteVariableDialogProps extends ConfirmDialogProps {
|
||||
variableKey: string;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface DeleteWebhookDialogProps extends ConfirmDialogProps {
|
||||
webhookUrl: string;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface DisconnectRepositoryDialogProps extends ConfirmDialogProps {}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
import { formatAddress } from 'utils/format';
|
||||
|
||||
interface RemoveMemberDialogProps extends ConfirmDialogProps {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ConfirmDialog, {
|
||||
ConfirmDialogProps,
|
||||
} from 'components/shared/ConfirmDialog';
|
||||
import React from 'react';
|
||||
|
||||
interface TransferProjectDialogProps extends ConfirmDialogProps {
|
||||
projectName: string;
|
||||
|
||||
@@ -3,12 +3,8 @@ import {
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
import React, {
|
||||
ComponentPropsWithoutRef,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
import { ComponentPropsWithoutRef, MouseEvent, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Project } from 'gql-client';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
@@ -16,7 +12,7 @@ import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
BranchIcon,
|
||||
ClockIcon,
|
||||
GitHubLogo,
|
||||
GithubLogoIcon,
|
||||
HorizontalDotIcon,
|
||||
WarningDiamondIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
@@ -95,11 +91,9 @@ export const ProjectCard = ({
|
||||
<HorizontalDotIcon />
|
||||
</Button>
|
||||
</MenuHandler>
|
||||
<MenuList placeholder={''}>
|
||||
<MenuItem placeholder={''}>Project settings</MenuItem>
|
||||
<MenuItem className="text-red-500" placeholder={''}>
|
||||
Delete project
|
||||
</MenuItem>
|
||||
<MenuList>
|
||||
<MenuItem>Project settings</MenuItem>
|
||||
<MenuItem className="text-red-500">Delete project</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
@@ -124,7 +118,7 @@ export const ProjectCard = ({
|
||||
<div className={theme.deploymentText()}>
|
||||
{hasDeployment ? (
|
||||
<>
|
||||
<GitHubLogo />
|
||||
<GithubLogoIcon />
|
||||
<span>{relativeTimeMs(project.deployments[0].createdAt)} on</span>
|
||||
<BranchIcon />
|
||||
<span>{project.deployments[0].branch}</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCombobox } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { CrossIcon, SearchIcon } from 'components/shared/CustomIcon';
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { InfoRoundFilledIcon } from 'components/shared/CustomIcon';
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from 'utils/classnames';
|
||||
|
||||
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift';
|
||||
import { Project } from 'gql-client';
|
||||
import React, { ComponentPropsWithoutRef, forwardRef } from 'react';
|
||||
import { ComponentPropsWithoutRef, forwardRef } from 'react';
|
||||
import { OmitCommon } from 'types/common';
|
||||
import { cn } from 'utils/classnames';
|
||||
import { getInitials } from 'utils/geInitials';
|
||||
|
||||
@@ -2,22 +2,22 @@ import React from 'react';
|
||||
import OauthPopup from 'react-oauth-popup';
|
||||
|
||||
import { useGQLClient } from '../../../context/GQLClientContext';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Button } from '../../shared/Button';
|
||||
import {
|
||||
GitIcon,
|
||||
EllipsesIcon,
|
||||
SnowballIcon,
|
||||
GithubIcon,
|
||||
GitTeaIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
import { IconWithFrame } from 'components/shared/IconWithFrame';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
} from '../../shared/CustomIcon';
|
||||
import { useToast } from '../../shared/Toast';
|
||||
import { IconWithFrame } from '../../shared/IconWithFrame';
|
||||
import { Heading } from '../../shared/Heading';
|
||||
import { MockConnectGitCard } from './MockConnectGitCard';
|
||||
|
||||
const SCOPES = 'repo user';
|
||||
const GITHUB_OAUTH_URL = `https://github.com/login/oauth/authorize?client_id=${
|
||||
process.env.REACT_APP_GITHUB_CLIENT_ID
|
||||
import.meta.env.VITE_GITHUB_CLIENT_ID
|
||||
}&scope=${encodeURIComponent(SCOPES)}`;
|
||||
|
||||
interface ConnectAccountInterface {
|
||||
|
||||
@@ -2,11 +2,11 @@ import React, { useCallback, useEffect } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { DeployStep, DeployStatus } from './DeployStep';
|
||||
import { Stopwatch, setStopWatchOffset } from 'components/StopWatch';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { ClockOutlineIcon, WarningIcon } from 'components/shared/CustomIcon';
|
||||
import { CancelDeploymentDialog } from 'components/projects/Dialog/CancelDeploymentDialog';
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import { Heading } from '../../shared/Heading';
|
||||
import { Button } from '../../shared/Button';
|
||||
import { ClockOutlineIcon, WarningIcon } from '../../shared/CustomIcon';
|
||||
import { CancelDeploymentDialog } from '../../projects/Dialog/CancelDeploymentDialog';
|
||||
|
||||
const TIMEOUT_DURATION = 5000;
|
||||
const Deploy = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Collapse } from '@material-tailwind/react';
|
||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { Stopwatch, setStopWatchOffset } from '../../StopWatch';
|
||||
import FormatMillisecond from '../../FormatMilliSecond';
|
||||
|
||||
@@ -74,12 +74,12 @@ export const MockConnectGitCard = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 relative z-0">
|
||||
{IMPORT_CONTENT.map((repo, index) => (
|
||||
<>
|
||||
<MockProjectCard key={index} {...repo} />
|
||||
<React.Fragment key={index}>
|
||||
<MockProjectCard {...repo} />
|
||||
{index !== IMPORT_CONTENT.length - 1 && (
|
||||
<div className="border-b border-base-border" />
|
||||
)}
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Spinner } from '@material-tailwind/react';
|
||||
import { Spinner } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { relativeTimeISO } from 'utils/time';
|
||||
import { GitRepositoryDetails } from 'types';
|
||||
import { GitRepositoryDetails } from '../../../../types/types';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
|
||||
+20
-15
@@ -1,10 +1,9 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Octokit } from 'octokit';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import assert from 'assert';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import { ProjectRepoCard } from 'components/projects/create/ProjectRepoCard';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from 'types';
|
||||
import { GitOrgDetails, GitRepositoryDetails } from '../../../../types/types';
|
||||
import {
|
||||
ChevronGrabberHorizontal,
|
||||
GithubIcon,
|
||||
@@ -14,20 +13,18 @@ import {
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { useOctokit } from 'context/OctokitContext';
|
||||
|
||||
const DEFAULT_SEARCHED_REPO = '';
|
||||
const REPOS_PER_PAGE = 5;
|
||||
|
||||
interface RepositoryListProps {
|
||||
octokit: Octokit;
|
||||
}
|
||||
|
||||
export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
export const RepositoryList = () => {
|
||||
const [searchedRepo, setSearchedRepo] = useState(DEFAULT_SEARCHED_REPO);
|
||||
const [selectedAccount, setSelectedAccount] = useState<SelectOption>();
|
||||
const [orgs, setOrgs] = useState<GitOrgDetails[]>([]);
|
||||
// TODO: Add new type for Git user when required
|
||||
const [gitUser, setGitUser] = useState<GitOrgDetails>();
|
||||
const { octokit, isAuth } = useOctokit();
|
||||
|
||||
const [repositoryDetails, setRepositoryDetails] = useState<
|
||||
GitRepositoryDetails[]
|
||||
@@ -35,15 +32,23 @@ export const RepositoryList = ({ octokit }: RepositoryListProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserAndOrgs = async () => {
|
||||
const user = await octokit.rest.users.getAuthenticated();
|
||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||
setOrgs(orgs.data);
|
||||
setGitUser(user.data);
|
||||
setSelectedAccount({ label: user.data.login, value: user.data.login });
|
||||
try {
|
||||
const user = await octokit.rest.users.getAuthenticated();
|
||||
const orgs = await octokit.rest.orgs.listForAuthenticatedUser();
|
||||
setOrgs(orgs.data);
|
||||
setGitUser(user.data);
|
||||
setSelectedAccount({ label: user.data.login, value: user.data.login });
|
||||
} catch (error) {
|
||||
// Error handled by octokit error hook interceptor in Octokit context
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserAndOrgs();
|
||||
}, [octokit]);
|
||||
if (isAuth) {
|
||||
fetchUserAndOrgs();
|
||||
}
|
||||
}, [octokit, isAuth]);
|
||||
|
||||
const debouncedSearchedRepo = useDebounce<string>(searchedRepo, 500);
|
||||
|
||||
|
||||
+5
-7
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { CopyBlock, atomOneLight } from 'react-code-blocks';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@@ -8,7 +7,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
interface AssignDomainProps {
|
||||
open: boolean;
|
||||
@@ -17,9 +16,9 @@ interface AssignDomainProps {
|
||||
|
||||
const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader placeholder={''}>Assign Domain</DialogHeader>
|
||||
<DialogBody placeholder={''}>
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader>Assign Domain</DialogHeader>
|
||||
<DialogBody>
|
||||
In order to assign a domain to your production deployments, configure it
|
||||
in the{' '}
|
||||
{/* TODO: Fix selection of project settings tab on navigation to domains */}
|
||||
@@ -36,13 +35,12 @@ const AssignDomainDialog = ({ open, handleOpen }: AssignDomainProps) => {
|
||||
theme={atomOneLight}
|
||||
/>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
onClick={handleOpen}
|
||||
placeholder={''}
|
||||
>
|
||||
<span>Okay</span>
|
||||
</Button>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
Deployment,
|
||||
DeploymentStatus,
|
||||
|
||||
-1
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Deployment } from 'gql-client';
|
||||
|
||||
import { relativeTimeMs } from 'utils/time';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Deployment, Domain, Environment, Project } from 'gql-client';
|
||||
import { Button } from 'components/shared/Button';
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
MenuHandler,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
import { ComponentPropsWithRef } from 'react';
|
||||
import AssignDomainDialog from './AssignDomainDialog';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
@@ -96,11 +96,10 @@ export const DeploymentMenu = ({
|
||||
aria-label="Toggle Menu"
|
||||
/>
|
||||
</MenuHandler>
|
||||
<MenuList className="text-elements-high-em" placeholder={''}>
|
||||
<MenuList className="text-elements-high-em">
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized"
|
||||
disabled={!Boolean(deployment.url)}
|
||||
placeholder={''}
|
||||
>
|
||||
<a
|
||||
className="flex items-center gap-3"
|
||||
@@ -114,7 +113,6 @@ export const DeploymentMenu = ({
|
||||
<MenuItem
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setAssignDomainDialog(!assignDomainDialog)}
|
||||
placeholder={''}
|
||||
>
|
||||
<GlobeIcon /> Assign domain
|
||||
</MenuItem>
|
||||
@@ -122,7 +120,6 @@ export const DeploymentMenu = ({
|
||||
className="hover:bg-base-bg-emphasized flex items-center gap-3"
|
||||
onClick={() => setChangeToProduction(!changeToProduction)}
|
||||
disabled={!(deployment.environment !== Environment.Production)}
|
||||
placeholder={''}
|
||||
>
|
||||
<RocketIcon /> Change to production
|
||||
</MenuItem>
|
||||
@@ -136,7 +133,6 @@ export const DeploymentMenu = ({
|
||||
deployment.isCurrent
|
||||
)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
<RefreshIcon /> Redeploy to production
|
||||
</MenuItem>
|
||||
@@ -148,7 +144,6 @@ export const DeploymentMenu = ({
|
||||
deployment.environment !== Environment.Production ||
|
||||
!Boolean(currentDeployment)
|
||||
}
|
||||
placeholder={''}
|
||||
>
|
||||
<UndoIcon /> Rollback to this version
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Input } from 'components/shared/Input';
|
||||
import {
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
WarningTriangleIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
import { DatePicker } from 'components/shared/DatePicker';
|
||||
import { Value } from 'react-calendar/dist/cjs/shared/types';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { Value } from 'types/vendor';
|
||||
|
||||
export enum StatusOptions {
|
||||
ALL_STATUS = 'All status',
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React from 'react';
|
||||
|
||||
import { GitCommitWithBranch } from 'types';
|
||||
import { GitCommitWithBranch } from '../../../../../types/types';
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
import ActivityCard from './ActivityCard';
|
||||
import { Button } from 'components/shared/Button';
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { GitCommitWithBranch } from '../../../../../types';
|
||||
import { GitCommitWithBranch } from '../../../../../types/types';
|
||||
import { Avatar } from 'components/shared/Avatar';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { cloneElement } from 'utils/cloneElement';
|
||||
|
||||
|
||||
+24
-32
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
|
||||
import { Typography, Input, IconButton } from '@material-tailwind/react';
|
||||
|
||||
import { EnvironmentVariablesFormValues } from '../../../../types';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { TrashIcon } from 'components/shared/CustomIcon';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
interface AddEnvironmentVariableRowProps {
|
||||
onDelete: () => void;
|
||||
@@ -19,38 +19,30 @@ const AddEnvironmentVariableRow = ({
|
||||
isDeleteDisabled,
|
||||
}: AddEnvironmentVariableRowProps) => {
|
||||
return (
|
||||
<div className="flex gap-1 p-2">
|
||||
<div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Key
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register(`variables.${index}.key`, {
|
||||
required: 'Key field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Value
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register(`variables.${index}.value`, {
|
||||
required: 'Value field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex py-4 self-stretch">
|
||||
<Input
|
||||
size="md"
|
||||
{...register(`variables.${index}.key`, {
|
||||
required: 'Key field cannot be empty',
|
||||
})}
|
||||
label={index === 0 ? 'Key' : undefined}
|
||||
/>
|
||||
<Input
|
||||
size="md"
|
||||
label={index === 0 ? 'Value' : undefined}
|
||||
{...register(`variables.${index}.value`, {
|
||||
required: 'Value field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
<div className="self-end">
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => onDelete()}
|
||||
<Button
|
||||
size="md"
|
||||
iconOnly
|
||||
onClick={onDelete}
|
||||
disabled={isDeleteDisabled}
|
||||
placeholder={''}
|
||||
>
|
||||
{'>'}
|
||||
</IconButton>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { AddProjectMemberInput, Permission } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
Input,
|
||||
Typography,
|
||||
Checkbox,
|
||||
} from '@material-tailwind/react';
|
||||
import { Typography } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Modal } from 'components/shared/Modal';
|
||||
import { Input } from 'components/shared/Input';
|
||||
import { Checkbox } from 'components/shared/Checkbox';
|
||||
|
||||
interface AddMemberDialogProp {
|
||||
open: boolean;
|
||||
@@ -61,73 +57,47 @@ const AddMemberDialog = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Add member</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
We will send an invitation link to this email address.
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Email address
|
||||
</Typography>
|
||||
<Input
|
||||
type="email"
|
||||
crossOrigin={undefined}
|
||||
{...register('emailAddress', {
|
||||
required: 'email field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Permissions
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
You can change this later if required.
|
||||
</Typography>
|
||||
<Checkbox
|
||||
crossOrigin={undefined}
|
||||
label={Permission.View}
|
||||
{...register(`permissions.view`)}
|
||||
color="blue"
|
||||
/>
|
||||
<Checkbox
|
||||
crossOrigin={undefined}
|
||||
label={Permission.Edit}
|
||||
{...register(`permissions.edit`)}
|
||||
color="blue"
|
||||
/>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
placeholder={''}
|
||||
>
|
||||
Send invite
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
<Modal open={open} onOpenChange={handleOpen}>
|
||||
<Modal.Content>
|
||||
<Modal.Header>Add member</Modal.Header>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<Modal.Body className="flex flex-col gap-2 p-4">
|
||||
<Typography variant="small">
|
||||
We will send an invitation link to this email address.
|
||||
</Typography>
|
||||
<Typography variant="small">Email address</Typography>
|
||||
<Input
|
||||
type="email"
|
||||
{...register('emailAddress', {
|
||||
required: 'email field cannot be empty',
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small">Permissions</Typography>
|
||||
<Typography variant="small">
|
||||
You can change this later if required.
|
||||
</Typography>
|
||||
<Checkbox
|
||||
label={Permission.View}
|
||||
{...register(`permissions.view`)}
|
||||
color="blue"
|
||||
/>
|
||||
<Checkbox
|
||||
label={Permission.Edit}
|
||||
{...register(`permissions.edit`)}
|
||||
color="blue"
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={handleOpen} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!isValid}>
|
||||
Send invite
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+18
-19
@@ -1,7 +1,6 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Project } from 'gql-client';
|
||||
|
||||
import {
|
||||
@@ -12,8 +11,10 @@ import {
|
||||
DialogFooter,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
interface DeleteProjectDialogProp {
|
||||
open: boolean;
|
||||
@@ -26,6 +27,7 @@ const DeleteProjectDialog = ({
|
||||
handleOpen,
|
||||
project,
|
||||
}: DeleteProjectDialogProp) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
const { orgSlug } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const client = useGQLClient();
|
||||
@@ -46,28 +48,32 @@ const DeleteProjectDialog = ({
|
||||
if (deleteProject) {
|
||||
navigate(`/${orgSlug}`);
|
||||
} else {
|
||||
toast.error('Project not deleted');
|
||||
toast({
|
||||
id: 'project_not_deleted',
|
||||
title: 'Project not deleted',
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
|
||||
handleOpen();
|
||||
}, [client, project, handleOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<Dialog open={open} handler={handleOpen}>
|
||||
<DialogHeader className="flex justify-between">
|
||||
<div>Delete project?</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(deleteProjectHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2" placeholder={''}>
|
||||
<Typography variant="paragraph" placeholder={''}>
|
||||
<DialogBody className="flex flex-col gap-2">
|
||||
<Typography variant="paragraph">
|
||||
Deleting your project is irreversible. Enter your project’s
|
||||
name
|
||||
<span className="bg-blue-100 text-blue-700">({project.name})</span>
|
||||
@@ -75,23 +81,17 @@ const DeleteProjectDialog = ({
|
||||
</Typography>
|
||||
<Input
|
||||
id="input"
|
||||
crossOrigin={undefined}
|
||||
{...register('projectName', {
|
||||
required: 'Project name is required',
|
||||
validate: (value) => value === project.name,
|
||||
})}
|
||||
/>
|
||||
<Typography variant="small" color="red" placeholder={''}>
|
||||
<Typography variant="small" color="red">
|
||||
^ Deleting your project is irreversible.
|
||||
</Typography>
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
<DialogFooter className="flex justify-start">
|
||||
<Button variant="outlined" onClick={handleOpen} className="mr-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -99,7 +99,6 @@ const DeleteProjectDialog = ({
|
||||
color="red"
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
placeholder={''}
|
||||
>
|
||||
Yes, Delete project
|
||||
</Button>
|
||||
|
||||
+12
-12
@@ -1,8 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Collapse, Typography } from '@material-tailwind/react';
|
||||
import { useState } from 'react';
|
||||
import { Collapse } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
import {
|
||||
ChevronDownSmallIcon,
|
||||
ChevronUpSmallIcon,
|
||||
} from 'components/shared/CustomIcon';
|
||||
|
||||
interface DisplayEnvironmentVariablesProps {
|
||||
environment: Environment;
|
||||
@@ -20,23 +24,19 @@ const DisplayEnvironmentVariables = ({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex gap-4 p-2 "
|
||||
className="flex gap-4 p-2"
|
||||
onClick={() => setOpenCollapse((cur) => !cur)}
|
||||
>
|
||||
<div>^</div>
|
||||
{openCollapse ? <ChevronUpSmallIcon /> : <ChevronDownSmallIcon />}
|
||||
<div className="grow capitalize">{environment}</div>
|
||||
<div>{variables.length} variables</div>
|
||||
</div>
|
||||
<Collapse open={openCollapse}>
|
||||
{variables.length === 0 ? (
|
||||
<Card className="bg-gray-300 flex items-center p-4" placeholder={''}>
|
||||
<Typography variant="small" className="text-black" placeholder={''}>
|
||||
No environment variables added yet.
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Once you add them, they’ll show up here.
|
||||
</Typography>
|
||||
</Card>
|
||||
<div className="bg-slate-100 rounded-xl flex-col p-4">
|
||||
No environment variables added yet. Once you add them, they'll show
|
||||
up here.
|
||||
</div>
|
||||
) : (
|
||||
variables.map((variable: EnvironmentVariable) => {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useState } from 'react';
|
||||
import { Domain, DomainStatus, Project } from 'gql-client';
|
||||
|
||||
import {
|
||||
@@ -10,11 +9,12 @@ import {
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Card,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import EditDomainDialog from './EditDomainDialog';
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { DeleteDomainDialog } from 'components/projects/Dialog/DeleteDomainDialog';
|
||||
import { useToast } from 'components/shared/Toast';
|
||||
|
||||
enum RefreshStatus {
|
||||
IDLE,
|
||||
@@ -47,6 +47,7 @@ const DomainCard = ({
|
||||
project,
|
||||
onUpdate,
|
||||
}: DomainCardProps) => {
|
||||
const { toast, dismiss } = useToast();
|
||||
const [refreshStatus, SetRefreshStatus] = useState(RefreshStatus.IDLE);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
@@ -58,9 +59,19 @@ const DomainCard = ({
|
||||
|
||||
if (deleteDomain) {
|
||||
onUpdate();
|
||||
toast.success(`Domain ${domain.name} deleted successfully`);
|
||||
toast({
|
||||
id: 'domain_deleted_success',
|
||||
title: 'Domain deleted',
|
||||
variant: 'success',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
} else {
|
||||
toast.error(`Error deleting domain ${domain.name}`);
|
||||
toast({
|
||||
id: 'domain_deleted_error',
|
||||
title: `Error deleting domain ${domain.name}`,
|
||||
variant: 'error',
|
||||
onDismiss: dismiss,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,7 +79,7 @@ const DomainCard = ({
|
||||
<>
|
||||
<div className="flex justify-between py-3">
|
||||
<div className="flex justify-start gap-1">
|
||||
<Typography variant="h6" placeholder={''}>
|
||||
<Typography variant="h6">
|
||||
<i>^</i> {domain.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
@@ -97,22 +108,20 @@ const DomainCard = ({
|
||||
<MenuHandler>
|
||||
<button className="border-2 rounded-full w-8 h-8">...</button>
|
||||
</MenuHandler>
|
||||
<MenuList placeholder={''}>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
className="text-black"
|
||||
onClick={() => {
|
||||
setEditDialogOpen((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Edit domain
|
||||
Edit Domain
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="text-red-500"
|
||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
placeholder={''}
|
||||
>
|
||||
^ Delete domain
|
||||
Delete domain
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
@@ -130,26 +139,19 @@ const DomainCard = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Production
|
||||
</Typography>
|
||||
<Typography variant="small">Production</Typography>
|
||||
{domain.status === DomainStatus.Pending && (
|
||||
<Card className="bg-gray-200 p-4 text-sm" placeholder={''}>
|
||||
<Card className="bg-slate-100 p-4 text-sm">
|
||||
{refreshStatus === RefreshStatus.IDLE ? (
|
||||
<Typography variant="small" placeholder={''}>
|
||||
<Typography variant="small">
|
||||
^ Add these records to your domain and refresh to check
|
||||
</Typography>
|
||||
) : refreshStatus === RefreshStatus.CHECKING ? (
|
||||
<Typography
|
||||
variant="small"
|
||||
className="text-blue-500"
|
||||
placeholder={''}
|
||||
>
|
||||
<Typography variant="small" className="text-blue-500">
|
||||
^ Checking records for {domain.name}
|
||||
</Typography>
|
||||
) : (
|
||||
<div className="flex gap-2 text-red-500 mb-2">
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
Failed to verify records. DNS propagation can take up to 48
|
||||
hours. Please ensure you added the correct records and refresh.
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Controller, useForm, SubmitHandler } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Domain } from 'gql-client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
Input,
|
||||
Typography,
|
||||
Select,
|
||||
Option,
|
||||
} from '@material-tailwind/react';
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { useGQLClient } from '../../../../context/GQLClientContext';
|
||||
import { Modal } from 'components/shared/Modal';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
const DEFAULT_REDIRECT_OPTIONS = ['none'];
|
||||
|
||||
@@ -122,95 +119,67 @@ const EditDomainDialog = ({
|
||||
}, [domain]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} handler={handleOpen} placeholder={''}>
|
||||
<DialogHeader className="flex justify-between" placeholder={''}>
|
||||
<div>Edit domain</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1 rounded-3xl"
|
||||
placeholder={''}
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(updateDomainHandler)}>
|
||||
<DialogBody className="flex flex-col gap-2 p-4" placeholder={''}>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain name
|
||||
</Typography>
|
||||
<Input crossOrigin={undefined} {...register('name')} />
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Redirect to
|
||||
</Typography>
|
||||
<Controller
|
||||
name="redirectedTo"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select {...field} disabled={isDisableDropdown} placeholder={''}>
|
||||
{redirectOptions.map((option, key) => (
|
||||
<Option key={key} value={option}>
|
||||
^ {option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Modal open={open} onOpenChange={handleOpen}>
|
||||
<Modal.Content>
|
||||
<Modal.Header>Edit domain</Modal.Header>
|
||||
<form onSubmit={handleSubmit(updateDomainHandler)}>
|
||||
<Modal.Body className="flex flex-col gap-2">
|
||||
<Typography variant="small">Domain name</Typography>
|
||||
<Input {...register('name')} />
|
||||
<Typography variant="small">Redirect to</Typography>
|
||||
<Controller
|
||||
name="redirectedTo"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select {...field} disabled={isDisableDropdown}>
|
||||
{redirectOptions.map((option, key) => (
|
||||
<Option key={key} value={option}>
|
||||
^ {option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
{isDisableDropdown && (
|
||||
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
|
||||
<div>^</div>
|
||||
<Typography variant="small">
|
||||
Domain “
|
||||
{domainRedirectedFrom ? domainRedirectedFrom.name : ''}”
|
||||
redirects to this domain so you can not redirect this doman
|
||||
further.
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{isDisableDropdown && (
|
||||
<div className="flex p-2 gap-2 text-black bg-gray-300 rounded-lg">
|
||||
<div>^</div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Domain “{domainRedirectedFrom ? domainRedirectedFrom.name : ''}”
|
||||
redirects to this domain so you can not redirect this doman
|
||||
further.
|
||||
<Typography variant="small">Git branch</Typography>
|
||||
<Input
|
||||
{...register('branch', {
|
||||
validate: (value) =>
|
||||
Boolean(branches.length) ? branches.includes(value) : true,
|
||||
})}
|
||||
disabled={
|
||||
!Boolean(branches.length) ||
|
||||
watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]
|
||||
}
|
||||
/>
|
||||
{!isValid && (
|
||||
<Typography variant="small" className="text-red-500">
|
||||
We couldn't find this branch in the connected Git
|
||||
repository.
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Git branch
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
{...register('branch', {
|
||||
validate: (value) =>
|
||||
Boolean(branches.length) ? branches.includes(value) : true,
|
||||
})}
|
||||
disabled={
|
||||
!Boolean(branches.length) ||
|
||||
watch('redirectedTo') !== DEFAULT_REDIRECT_OPTIONS[0]
|
||||
}
|
||||
/>
|
||||
{!isValid && (
|
||||
<Typography
|
||||
variant="small"
|
||||
className="text-red-500"
|
||||
placeholder={''}
|
||||
>
|
||||
We couldn't find this branch in the connected Git repository.
|
||||
</Typography>
|
||||
)}
|
||||
</DialogBody>
|
||||
<DialogFooter className="flex justify-start" placeholder={''}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
className="mr-1"
|
||||
placeholder={''}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="gradient"
|
||||
color="blue"
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
placeholder={''}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={handleOpen} className="mr-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!isDirty}>
|
||||
Save changes
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+10
-19
@@ -1,12 +1,16 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import { EnvironmentVariable } from 'gql-client';
|
||||
|
||||
import { IconButton, Input, Typography } from '@material-tailwind/react';
|
||||
import {
|
||||
IconButton,
|
||||
Typography,
|
||||
} from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { useGQLClient } from 'context/GQLClientContext';
|
||||
import { DeleteVariableDialog } from 'components/projects/Dialog/DeleteVariableDialog';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
const ShowPasswordIcon = ({
|
||||
handler,
|
||||
@@ -84,24 +88,15 @@ const EditEnvironmentVariableRow = ({
|
||||
<>
|
||||
<div className="flex gap-1 p-2">
|
||||
<div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Key
|
||||
</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
disabled={!edit}
|
||||
{...register(`key`)}
|
||||
/>
|
||||
<Typography variant="small">Key</Typography>
|
||||
<Input disabled={!edit} {...register(`key`)} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Value
|
||||
</Typography>
|
||||
<Typography variant="small">Value</Typography>
|
||||
<Input
|
||||
crossOrigin={undefined}
|
||||
disabled={!edit}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
icon={
|
||||
leftIcon={
|
||||
<ShowPasswordIcon
|
||||
handler={() => {
|
||||
setShowPassword((prevShowPassword) => !prevShowPassword);
|
||||
@@ -118,7 +113,6 @@ const EditEnvironmentVariableRow = ({
|
||||
<IconButton
|
||||
onClick={handleSubmit(updateEnvironmentVariableHandler)}
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
{'S'}
|
||||
</IconButton>
|
||||
@@ -130,7 +124,6 @@ const EditEnvironmentVariableRow = ({
|
||||
reset();
|
||||
setEdit((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
{'C'}
|
||||
</IconButton>
|
||||
@@ -144,7 +137,6 @@ const EditEnvironmentVariableRow = ({
|
||||
onClick={() => {
|
||||
setEdit((preVal) => !preVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
{'E'}
|
||||
</IconButton>
|
||||
@@ -153,7 +145,6 @@ const EditEnvironmentVariableRow = ({
|
||||
<IconButton
|
||||
size="sm"
|
||||
onClick={() => setDeleteDialogOpen((preVal) => !preVal)}
|
||||
placeholder={''}
|
||||
>
|
||||
{'D'}
|
||||
</IconButton>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import { GitSelect } from '../../../../types';
|
||||
|
||||
const GitSelectionSection = ({
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Permission, User } from 'gql-client';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import {
|
||||
Select,
|
||||
Option,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import { formatAddress } from 'utils/format';
|
||||
import { RemoveMemberDialog } from 'components/projects/Dialog/RemoveMemberDialog';
|
||||
import { Select, SelectOption } from 'components/shared/Select';
|
||||
import { LoaderIcon } from 'components/shared/CustomIcon';
|
||||
import { Tooltip } from 'components/shared/Tooltip';
|
||||
import { Button } from 'components/shared/Button';
|
||||
import { Permission, User } from 'gql-client';
|
||||
import { formatAddress } from 'utils/format';
|
||||
import { Tag } from 'components/shared/Tag';
|
||||
import { Input } from 'components/shared/Input';
|
||||
|
||||
const PERMISSION_OPTIONS = [
|
||||
const PERMISSION_OPTIONS: SelectOption[] = [
|
||||
{
|
||||
label: 'View only',
|
||||
value: 'View',
|
||||
@@ -23,7 +21,7 @@ const PERMISSION_OPTIONS = [
|
||||
},
|
||||
];
|
||||
|
||||
const DROPDOWN_OPTIONS = [
|
||||
const DROPDOWN_OPTIONS: SelectOption[] = [
|
||||
...PERMISSION_OPTIONS,
|
||||
{ label: 'Remove member', value: 'remove' },
|
||||
];
|
||||
@@ -50,16 +48,21 @@ const MemberCard = ({
|
||||
onUpdateProjectMember,
|
||||
}: MemberCardProps) => {
|
||||
const [ethAddress, emailDomain] = member.email.split('@');
|
||||
const [selectedPermission, setSelectedPermission] = useState(
|
||||
permissions.join('+'),
|
||||
const [selectedPermission, setSelectedPermission] = useState<SelectOption>(
|
||||
PERMISSION_OPTIONS.map((value) => {
|
||||
permissions.join('+') === value.value;
|
||||
}).pop() ?? {
|
||||
label: 'View only',
|
||||
value: 'View',
|
||||
},
|
||||
);
|
||||
const [removeMemberDialogOpen, setRemoveMemberDialogOpen] = useState(false);
|
||||
|
||||
const handlePermissionChange = useCallback(
|
||||
async (value: string) => {
|
||||
async (value: SelectOption) => {
|
||||
setSelectedPermission(value);
|
||||
|
||||
if (value === 'remove') {
|
||||
if (value.value === 'remove') {
|
||||
setRemoveMemberDialogOpen((prevVal) => !prevVal);
|
||||
// To display updated label in next render
|
||||
setTimeout(() => {
|
||||
@@ -67,7 +70,7 @@ const MemberCard = ({
|
||||
});
|
||||
} else {
|
||||
if (onUpdateProjectMember) {
|
||||
const permissions = value.split('+') as Permission[];
|
||||
const permissions = value.value.split('+') as Permission[];
|
||||
await onUpdateProjectMember({ permissions });
|
||||
}
|
||||
}
|
||||
@@ -77,9 +80,8 @@ const MemberCard = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex p-1 ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
|
||||
className={`flex p-1 items-center ${!isFirstCard && 'mt-1 border-t border-gray-300'}`}
|
||||
>
|
||||
<div>^</div>
|
||||
<div className="basis-1/2">
|
||||
{member.name && (
|
||||
<Tooltip content={member.name}>
|
||||
@@ -94,48 +96,36 @@ const MemberCard = ({
|
||||
</div>
|
||||
<div className="basis-1/2">
|
||||
{!isPending ? (
|
||||
<Select
|
||||
size="lg"
|
||||
label={isOwner ? 'Owner' : ''}
|
||||
disabled={isOwner}
|
||||
value={selectedPermission}
|
||||
onChange={(value) => handlePermissionChange(value!)}
|
||||
selected={(_, index) => (
|
||||
<span>{DROPDOWN_OPTIONS[index!]?.label}</span>
|
||||
)}
|
||||
placeholder={''}
|
||||
>
|
||||
{DROPDOWN_OPTIONS.map((permission, key) => (
|
||||
<Option key={key} value={permission.value}>
|
||||
^ {permission.label}
|
||||
{permission.value === selectedPermission && (
|
||||
<p className="float-right">^</p>
|
||||
)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
isOwner ? (
|
||||
<Input size="md" value="Owner" disabled />
|
||||
) : (
|
||||
<Select
|
||||
options={DROPDOWN_OPTIONS}
|
||||
size="md"
|
||||
placeholder="Select permission"
|
||||
value={selectedPermission}
|
||||
onChange={(value) =>
|
||||
handlePermissionChange(value as SelectOption)
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="flex justify-end gap-2">
|
||||
<div>
|
||||
<Chip
|
||||
value="Pending"
|
||||
variant="outlined"
|
||||
color="orange"
|
||||
size="sm"
|
||||
icon={'^'}
|
||||
/>
|
||||
<Tag type="positive" size="sm" leftIcon={<LoaderIcon />}>
|
||||
Pending
|
||||
</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<IconButton
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
<Button
|
||||
size="md"
|
||||
iconOnly
|
||||
onClick={() => {
|
||||
setRemoveMemberDialogOpen((prevVal) => !prevVal);
|
||||
}}
|
||||
placeholder={''}
|
||||
>
|
||||
D
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { ProjectSettingHeader } from './ProjectSettingHeader';
|
||||
|
||||
export interface ProjectSettingContainerProps extends PropsWithChildren {
|
||||
headingText: string;
|
||||
className?: string;
|
||||
button?: React.ReactNode;
|
||||
badge?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ProjectSettingContainer: React.FC<ProjectSettingContainerProps> = ({
|
||||
headingText,
|
||||
className,
|
||||
button,
|
||||
children,
|
||||
badge,
|
||||
...props
|
||||
}: ProjectSettingContainerProps) => {
|
||||
return (
|
||||
<div
|
||||
className={'flex-col justify-start gap-8 space-y-3 px-2 pb-6'}
|
||||
{...props}
|
||||
>
|
||||
<ProjectSettingHeader
|
||||
headingText={headingText}
|
||||
button={button}
|
||||
badge={badge}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProjectSettingContainer };
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Heading } from 'components/shared/Heading';
|
||||
|
||||
export interface ProjectSettingHeaderProps extends PropsWithChildren {
|
||||
headingText: string;
|
||||
button?: React.ReactNode;
|
||||
badge?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ProjectSettingHeader: React.FC<ProjectSettingHeaderProps> = ({
|
||||
headingText,
|
||||
button,
|
||||
badge,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center" {...props}>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<Heading className="text-lg font-medium leading-normal">
|
||||
{headingText}
|
||||
</Heading>
|
||||
{badge}
|
||||
</div>
|
||||
{button ?? button}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProjectSettingHeader };
|
||||
+8
-16
@@ -1,9 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button, Typography } from '@material-tailwind/react';
|
||||
import { Typography } from '@snowballtools/material-tailwind-react-fork';
|
||||
|
||||
import { GitRepositoryDetails } from 'types';
|
||||
import { GitRepositoryDetails } from '../../../../types';
|
||||
import { DisconnectRepositoryDialog } from 'components/projects/Dialog/DisconnectRepositoryDialog';
|
||||
import { Button } from 'components/shared/Button';
|
||||
|
||||
const RepoConnectedSection = ({
|
||||
linkedRepo,
|
||||
@@ -17,21 +18,12 @@ const RepoConnectedSection = ({
|
||||
<div className="flex gap-4">
|
||||
<div>^</div>
|
||||
<div className="grow">
|
||||
<Typography variant="small" placeholder={''}>
|
||||
{linkedRepo.full_name}
|
||||
</Typography>
|
||||
<Typography variant="small" placeholder={''}>
|
||||
Connected just now
|
||||
</Typography>
|
||||
<Typography variant="small">{linkedRepo.full_name}</Typography>
|
||||
<Typography variant="small">Connected just now</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => setDisconnectRepoDialogOpen(true)}
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
placeholder={''}
|
||||
>
|
||||
^ Disconnect
|
||||
<Button onClick={() => setDisconnectRepoDialogOpen(true)} size="sm">
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
<DisconnectRepositoryDialog
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user