Init
19
packages/admin/.eslintrc.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
extends: [
|
||||
'../../.eslintrc.js',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'error',
|
||||
{ props: 'never', children: 'never' },
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
};
|
8
packages/admin/.prettierrc.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
tabWidth: 2,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'always',
|
||||
printWidth: 80,
|
||||
};
|
20
packages/admin/.stylelintrc.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": [
|
||||
"stylelint-config-sass-guidelines",
|
||||
"stylelint-config-css-modules"
|
||||
],
|
||||
"plugins": ["stylelint-order"],
|
||||
"configBaseDir": "./",
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"order/properties-alphabetical-order": null,
|
||||
"max-nesting-depth": null,
|
||||
"selector-no-qualifying-type": null,
|
||||
"selector-class-pattern": "(^[a-z]+([A-Z]|-)+\\w+$)+|(^([a-z]|-)+\\w+$)",
|
||||
"scss/at-mixin-pattern": "(^[a-z]+([A-Z]|-)+\\w+$)+|(^([a-z]|-)+\\w+$)",
|
||||
"scss/dollar-variable-pattern": "(^[a-z]+([A-Z]|-)+\\w+$)+|(^([a-z]|-)+\\w+$)",
|
||||
"selector-max-compound-selectors": 8,
|
||||
"scss/at-extend-no-missing-placeholder": null,
|
||||
"at-rule-no-unknown": null
|
||||
}
|
||||
}
|
30
packages/admin/index.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Baseline Core</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Baseline Core Admin portal" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<script>
|
||||
var global = global || window;
|
||||
var Buffer = Buffer || [];
|
||||
var process = process || { env: { DEBUG: undefined }, version: [] };
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
72
packages/admin/package.json
Normal file
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"name": "@baseline/admin",
|
||||
"version": "1.0.0",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"deploy:staging": "./scripts/deploy.sh staging",
|
||||
"remove:staging": ". ../../scripts/project-variables.sh && npx serverless remove --stage staging --region $REGION",
|
||||
"deploy:prod": "./scripts/deploy.sh prod",
|
||||
"remove:prod": ". ../../scripts/project-variables.sh && npx serverless remove --stage prod --region $REGION",
|
||||
"aws:profile": "../../scripts/setup-aws-profile.sh",
|
||||
"start": "npx vite",
|
||||
"build": "pnpm run generate:env:staging && npx vite build",
|
||||
"build:deploy": "npx vite build",
|
||||
"preview": "npx vite preview",
|
||||
"generate:env:local": "pnpm -w run generate:env:local",
|
||||
"generate:env:staging": "pnpm -w run generate:env:staging",
|
||||
"generate:env:prod": "pnpm -w run generate:env:prod",
|
||||
"lint": "npx stylelint --config '.stylelintrc.json' 'src/**/*.scss' && npx eslint --config '.eslintrc.js' 'src/**/*.{ts,tsx,js}'",
|
||||
"pretty": "npx prettier --write 'src/**/*.{ts,tsx,js,json,css,scss,md,yml,yaml,html}' && npx prettier --write 'public/**/*.{ts,tsx,js,json,css,scss,md,yml,yaml,html}' && npx prettier --write '*.{ts,tsx,js,json,css,scss,md,yml,yaml,html}'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-amplify/ui-react": "6.1.6",
|
||||
"@baseline/client-api": "workspace:1.0.0",
|
||||
"@baseline/types": "workspace:1.0.0",
|
||||
"@vitejs/plugin-react": "4.2.1",
|
||||
"aws-amplify": "6.0.20",
|
||||
"axios": "1.7.7",
|
||||
"formik": "^2.4.6",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-router-dom": "6.22.3",
|
||||
"react-select": "5.8.0",
|
||||
"reactstrap": "9.2.2",
|
||||
"swr": "^2.2.5",
|
||||
"typescript": "5.4.2",
|
||||
"vite": "5.2.6",
|
||||
"vite-plugin-environment": "1.1.3",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.2.67",
|
||||
"@types/react-dom": "18.2.22",
|
||||
"eslint-plugin-react": "7.34.0",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"postcss": "8.4.35",
|
||||
"postcss-scss": "4.0.9",
|
||||
"prettier": "2.4.1",
|
||||
"sass": "1.43.4",
|
||||
"serverless": "3.38.0",
|
||||
"serverless-baseline-invalidate-cloudfront": "0.1.1",
|
||||
"serverless-s3-sync": "3.1.0",
|
||||
"stylelint": "16.2.1",
|
||||
"stylelint-config-css-modules": "4.4.0",
|
||||
"stylelint-config-sass-guidelines": "11.1.0",
|
||||
"stylelint-config-standard": "36.0.0",
|
||||
"stylelint-config-standard-scss": "13.0.0",
|
||||
"stylelint-order": "6.0.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
packages/admin/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
packages/admin/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
packages/admin/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
packages/admin/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 390 B |
BIN
packages/admin/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 723 B |
BIN
packages/admin/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
7
packages/admin/public/icons/asset.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<svg id="Group_60" data-name="Group 60" xmlns="http://www.w3.org/2000/svg" width="24.748" height="21.723" viewBox="0 0 24.748 21.723">
|
||||
<g id="ic_photo_camera_24px">
|
||||
<circle cx="3.2" cy="3.2" r="3.2" transform="translate(5.299 5.216)" fill="#707070"/>
|
||||
<path d="M7.949,2,6.394,3.7H3.7A1.7,1.7,0,0,0,2,5.4V15.6a1.7,1.7,0,0,0,1.7,1.7H17.3A1.7,1.7,0,0,0,19,15.6V5.4a1.7,1.7,0,0,0-1.7-1.7H14.6L13.048,2ZM10.5,14.748A4.249,4.249,0,1,1,14.748,10.5,4.251,4.251,0,0,1,10.5,14.748Z" transform="translate(-2 -2)" fill="#707070"/>
|
||||
</g>
|
||||
<path id="ic_audiotrack_24px" d="M11.192,3v8.03a3.8,3.8,0,0,0-1.3-.242,3.894,3.894,0,1,0,3.85,4.326h.043V5.6h3.461V3Z" transform="translate(7 2.649)" fill="#707070" stroke="#fff" stroke-width="1"/>
|
||||
</svg>
|
After Width: | Height: | Size: 744 B |
3
packages/admin/public/icons/gear.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="19.4" height="20">
|
||||
<path d="M19.43,12.98A7.793,7.793,0,0,0,19.5,12a7.793,7.793,0,0,0-.07-.98l2.11-1.65a.5.5,0,0,0,.12-.64l-2-3.46a.5.5,0,0,0-.61-.22l-2.49,1a7.306,7.306,0,0,0-1.69-.98l-.38-2.65A.488.488,0,0,0,14,2H10a.488.488,0,0,0-.49.42L9.13,5.07a7.683,7.683,0,0,0-1.69.98l-2.49-1a.488.488,0,0,0-.61.22l-2,3.46a.493.493,0,0,0,.12.64l2.11,1.65A7.931,7.931,0,0,0,4.5,12a7.931,7.931,0,0,0,.07.98L2.46,14.63a.5.5,0,0,0-.12.64l2,3.46a.5.5,0,0,0,.61.22l2.49-1a7.306,7.306,0,0,0,1.69.98l.38,2.65A.488.488,0,0,0,10,22h4a.488.488,0,0,0,.49-.42l.38-2.65a7.683,7.683,0,0,0,1.69-.98l2.49,1a.488.488,0,0,0,.61-.22l2-3.46a.5.5,0,0,0-.12-.64ZM12,15.5A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" transform="translate(-2.271 -2)" fill="#707070"/>
|
||||
</svg>
|
After Width: | Height: | Size: 790 B |
3
packages/admin/public/icons/home.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="19" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path fill="#707070" stroke="none" d="M 8.8 19 L 8.8 12.294117 L 13.2 12.294117 L 13.2 19 L 18.700001 19 L 18.700001 10.058824 L 22 10.058824 L 11 0 L 0 10.058824 L 3.3 10.058824 L 3.3 19 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 309 B |
5
packages/admin/public/icons/post.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated by Pixelmator Pro 2.4.6 -->
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path id="iccreate24px-1" fill="#707070" stroke="none" d="M -0 11.086084 L -0 14 L 2.916162 14 L 11.520921 5.393497 L 8.604759 2.483365 Z M 13.772342 3.14162 C 13.918085 2.99629 14 2.798914 14 2.593074 C 14 2.387233 13.918085 2.189857 13.772342 2.044527 L 11.95305 0.227704 C 11.80775 0.081932 11.610414 0 11.404614 0 C 11.198815 0 11.00148 0.081932 10.856179 0.227704 L 9.433084 1.651088 L 12.349246 4.567841 L 13.772342 3.144457 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 656 B |
3
packages/admin/public/icons/screen.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="15">
|
||||
<path id="ic_video_label_24px" d="M17.316,3H2.632A1.628,1.628,0,0,0,1,4.615V15.922a1.628,1.628,0,0,0,1.632,1.615H17.316a1.628,1.628,0,0,0,1.632-1.615V4.615A1.628,1.628,0,0,0,17.316,3Zm0,10.5H2.632V4.615H17.316Z" transform="translate(-1 -3)" fill="#707070"/>
|
||||
</svg>
|
After Width: | Height: | Size: 331 B |
3
packages/admin/public/icons/tag.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#707070" d="M 16.684999 8.042999 L 8.949 0.308001 C 8.627602 -0.013262 8.191428 -0.193201 7.737 -0.191999 L 1.719 -0.191999 C 0.770762 -0.189255 0.002746 0.578762 0 1.527 L 0 7.545 C -0.000972 8.003402 0.181611 8.443114 0.507 8.766 L 8.244 16.503 C 8.565398 16.824263 9.001572 17.004202 9.456 17.003 C 9.912048 17.005405 10.349501 16.822411 10.668 16.496 L 16.684999 10.479 C 17.01141 10.160501 17.194405 9.723047 17.191999 9.267 C 17.191452 8.80802 17.009161 8.367934 16.684999 8.042999 Z M 3.009 4.108 C 2.297627 4.106895 1.721633 3.529708 1.722002 2.818335 C 1.72237 2.10696 2.298961 1.53037 3.010334 1.530002 C 3.721708 1.529634 4.298895 2.105627 4.3 2.816999 C 4.300799 3.159638 4.16504 3.488474 3.922757 3.730757 C 3.680474 3.97304 3.351639 4.108799 3.009 4.108 Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 880 B |
3
packages/admin/public/icons/users.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||
<path id="ic_account_circle_24px" d="M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm0,3A3,3,0,1,1,9,8,3,3,0,0,1,12,5Zm0,14.2a7.2,7.2,0,0,1-6-3.22c.03-1.99,4-3.08,6-3.08s5.97,1.09,6,3.08A7.2,7.2,0,0,1,12,19.2Z" transform="translate(-2 -2)" fill="#707070"/>
|
||||
</svg>
|
After Width: | Height: | Size: 344 B |
BIN
packages/admin/public/logo.png
Normal file
After Width: | Height: | Size: 14 KiB |
25
packages/admin/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "Baseline Core",
|
||||
"name": "Baseline Core",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
1
packages/admin/public/placeholder.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" id="svg839" version="1.1" height="1px" width="1px"><g><rect y="0" x="0" height="1px" width="1px" style="fill:#bababa;"/></g></svg>
|
After Width: | Height: | Size: 171 B |
2
packages/admin/public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow:
|
18
packages/admin/scripts/deploy.sh
Executable file
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
CURRENT_DIR="$(pwd -P)"
|
||||
PARENT_PATH="$(
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" || exit
|
||||
pwd -P
|
||||
)/.."
|
||||
cd "$PARENT_PATH" || exit
|
||||
|
||||
STAGE=$1
|
||||
|
||||
pnpm run generate:env:"$STAGE"
|
||||
. ../../scripts/project-variables.sh
|
||||
. ../../scripts/get-stack-outputs.sh "$STAGE" >/dev/null
|
||||
pnpm run build:deploy
|
||||
npx serverless deploy --verbose --stage "$STAGE" --region "$REGION"
|
||||
|
||||
cd "$CURRENT_DIR" || exit
|
177
packages/admin/serverless.yml
Normal file
|
@ -0,0 +1,177 @@
|
|||
service: ${env:APP_NAME}-admin
|
||||
|
||||
frameworkVersion: '>=2.0.0 <4.0.0'
|
||||
|
||||
plugins:
|
||||
- serverless-s3-sync
|
||||
- serverless-baseline-invalidate-cloudfront
|
||||
|
||||
custom:
|
||||
s3Sync:
|
||||
- bucketNameKey: S3Bucket
|
||||
localDir: .dist/
|
||||
cloudfrontInvalidate:
|
||||
- distributionIdKey: 'CDNDistributionId'
|
||||
items:
|
||||
- '/*'
|
||||
# domain:
|
||||
# local: "local-admin.baselinejs.com"
|
||||
# staging: "staging-admin.baselinejs.com"
|
||||
# prod: "admin.baselinejs.com"
|
||||
|
||||
provider:
|
||||
name: aws
|
||||
runtime: nodejs20.x
|
||||
profile: ${env:AWS_PROFILE}
|
||||
stage: ${opt:stage}
|
||||
deploymentMethod: direct
|
||||
deploymentPrefix: ${self:service}-${sls:stage}
|
||||
stackTags:
|
||||
AppName: ${env:APP_NAME}
|
||||
Stage: ${opt:stage}
|
||||
Region: ${opt:region}
|
||||
Product: Baseline
|
||||
|
||||
# The "Resources" your "Functions" use. Raw AWS CloudFormation goes in here.
|
||||
resources:
|
||||
Description: ${env:APP_NAME} ${opt:stage}
|
||||
Resources:
|
||||
## Specifying the S3 Bucket
|
||||
WebsiteS3Bucket:
|
||||
Type: AWS::S3::Bucket
|
||||
Properties:
|
||||
OwnershipControls:
|
||||
Rules:
|
||||
- ObjectOwnership: BucketOwnerPreferred
|
||||
PublicAccessBlockConfiguration:
|
||||
BlockPublicAcls: false
|
||||
BlockPublicPolicy: false
|
||||
IgnorePublicAcls: false
|
||||
RestrictPublicBuckets: false
|
||||
## Specifying the policies to make sure all files inside the Bucket are available to CloudFront
|
||||
WebsiteS3BucketPolicy:
|
||||
Type: AWS::S3::BucketPolicy
|
||||
Properties:
|
||||
Bucket:
|
||||
Ref: WebsiteS3Bucket
|
||||
PolicyDocument:
|
||||
Statement:
|
||||
- Sid: PublicReadGetObject
|
||||
Effect: Allow
|
||||
Principal:
|
||||
Service: cloudfront.amazonaws.com
|
||||
Action:
|
||||
- s3:GetObject
|
||||
Resource: !Join
|
||||
- ''
|
||||
- - 'arn:aws:s3:::'
|
||||
- !Ref WebsiteS3Bucket
|
||||
- /*
|
||||
Condition:
|
||||
StringEquals:
|
||||
AWS:SourceArn:
|
||||
!Join [
|
||||
'',
|
||||
[
|
||||
'arn:aws:cloudfront::',
|
||||
!Ref AWS::AccountId,
|
||||
':distribution/',
|
||||
!Ref WebsiteCloudFrontDistribution,
|
||||
],
|
||||
]
|
||||
CloudfrontResponsePolicy:
|
||||
Type: AWS::CloudFront::ResponseHeadersPolicy
|
||||
Properties:
|
||||
ResponseHeadersPolicyConfig:
|
||||
Name: ${self:service}-${sls:stage}-no-cache-headers
|
||||
CustomHeadersConfig:
|
||||
Items:
|
||||
- Header: 'Cache-Control'
|
||||
Override: true
|
||||
Value: 'no-cache'
|
||||
# OAC Role for the Cloudfront distribution to block direct S3 Access
|
||||
WebsiteCloudFrontDistributionOriginAccessControl:
|
||||
Type: AWS::CloudFront::OriginAccessControl
|
||||
Properties:
|
||||
OriginAccessControlConfig:
|
||||
Name: ${self:service}-${sls:stage}-cloudfront-oac
|
||||
OriginAccessControlOriginType: s3
|
||||
SigningBehavior: always
|
||||
SigningProtocol: sigv4
|
||||
## Specifying the CloudFront Distribution to serve your Web Application
|
||||
WebsiteCloudFrontDistribution:
|
||||
Type: AWS::CloudFront::Distribution
|
||||
Properties:
|
||||
DistributionConfig:
|
||||
HttpVersion: http2
|
||||
Origins:
|
||||
- DomainName: !GetAtt WebsiteS3Bucket.RegionalDomainName
|
||||
## An identifier for the origin which must be unique within the distribution
|
||||
Id: !GetAtt WebsiteS3Bucket.RegionalDomainName
|
||||
OriginAccessControlId: !Ref WebsiteCloudFrontDistributionOriginAccessControl
|
||||
S3OriginConfig:
|
||||
OriginAccessIdentity: ''
|
||||
Enabled: true
|
||||
## [Custom Domain] Add the domain alias
|
||||
# Aliases:
|
||||
# - ${self:custom.domain.${opt:stage}}
|
||||
DefaultRootObject: 'index.html'
|
||||
## Since the Single Page App is taking care of the routing we need to make sure ever path is served with index.html
|
||||
## The only exception are files that actually exist e.h. app.js, reset.css
|
||||
CustomErrorResponses:
|
||||
- ErrorCode: 403
|
||||
ResponseCode: 200
|
||||
ResponsePagePath: '/index.html'
|
||||
DefaultCacheBehavior:
|
||||
AllowedMethods:
|
||||
- GET
|
||||
- HEAD
|
||||
CachedMethods:
|
||||
- HEAD
|
||||
- GET
|
||||
Compress: true
|
||||
DefaultTTL: 1800
|
||||
MinTTL: 0
|
||||
## The origin id defined above
|
||||
TargetOriginId: !GetAtt WebsiteS3Bucket.RegionalDomainName
|
||||
## Defining if and how the QueryString and Cookies are forwarded to the origin which in this case is S3
|
||||
ForwardedValues:
|
||||
QueryString: false
|
||||
Cookies:
|
||||
Forward: none
|
||||
## The protocol that users can use to access the files in the origin. To allow HTTP use `allow-all`
|
||||
ViewerProtocolPolicy: redirect-to-https
|
||||
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
|
||||
ResponseHeadersPolicyId: !Ref CloudfrontResponsePolicy
|
||||
## The certificate to use when viewers use HTTPS to request objects.
|
||||
ViewerCertificate:
|
||||
CloudFrontDefaultCertificate: true
|
||||
## [Custom Domain] Stop using the cloudfront default certificate, uncomment below and add ACM Certificate ARN
|
||||
# MinimumProtocolVersion: TLSv1.2_2021
|
||||
# SslSupportMethod: sni-only
|
||||
# AcmCertificateArn: arn:aws:acm:us-east-1:xxxxxxxxxxxx:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # ARN of the AWS certificate
|
||||
|
||||
## Uncomment the following section in case you want to enable logging for CloudFront requests
|
||||
# Logging:
|
||||
# IncludeCookies: 'false'
|
||||
# Bucket: mylogs.s3.amazonaws.com
|
||||
# Prefix: myprefix
|
||||
|
||||
## In order to print out the hosted domain via `serverless info` we need to define the DomainName output for CloudFormation
|
||||
Outputs:
|
||||
AdminCloudFrontUrl:
|
||||
Description: The Admin URL
|
||||
Value:
|
||||
'Fn::GetAtt': [WebsiteCloudFrontDistribution, DomainName]
|
||||
AdminCloudFrontDistributionId:
|
||||
Description: CloudFront Distribution Id
|
||||
Value:
|
||||
Ref: WebsiteCloudFrontDistribution
|
||||
CDNDistributionId:
|
||||
Description: CloudFront Distribution Id for serverless-cloudfront-invalidate
|
||||
Value:
|
||||
Ref: WebsiteCloudFrontDistribution
|
||||
S3Bucket:
|
||||
Description: S3 Bucket
|
||||
Value:
|
||||
Ref: WebsiteS3Bucket
|
132
packages/admin/src/App.tsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Amplify } from 'aws-amplify';
|
||||
import { fetchAuthSession } from 'aws-amplify/auth';
|
||||
import { Hub } from 'aws-amplify/utils';
|
||||
import { checkAdmin } from '@baseline/client-api/admin';
|
||||
import {
|
||||
Outlet,
|
||||
RouterProvider,
|
||||
createBrowserRouter,
|
||||
redirect,
|
||||
} from 'react-router-dom';
|
||||
import '@aws-amplify/ui-react/styles.css';
|
||||
import Dashboard from './baseblocks/dashboard/pages/Dashboard';
|
||||
import User, { userLoader } from './baseblocks/user/pages/User';
|
||||
import Admins, { adminListLoader } from './baseblocks/admin/pages/Admins';
|
||||
import {
|
||||
createRequestHandler,
|
||||
getRequestHandler,
|
||||
} from '@baseline/client-api/request-handler';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import Home from './baseblocks/home/pages/Home';
|
||||
import Login from './baseblocks/login/pages/Login';
|
||||
import NotAdmin from './baseblocks/not-admin/pages/NotAdmin';
|
||||
import Layout from './components/layout/Layout';
|
||||
import Loader from './components/page-content/loader/Loader';
|
||||
import Charts from './baseblocks/chart/pages/charts';
|
||||
import Chart from './baseblocks/chart/pages/Chart';
|
||||
|
||||
Amplify.configure({
|
||||
Auth: {
|
||||
Cognito: {
|
||||
signUpVerificationMethod: 'code',
|
||||
identityPoolId: process.env.REACT_APP_COGNITO_IDENTITY_POOL_ID || '',
|
||||
userPoolId: process.env.REACT_APP_COGNITO_USER_POOL_ID || '',
|
||||
userPoolClientId:
|
||||
process.env.REACT_APP_COGNITO_USER_POOL_WEB_CLIENT_ID || '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
return Hub.listen('auth', (data) => {
|
||||
console.debug('auth event', data.payload.event);
|
||||
switch (data.payload.event) {
|
||||
case 'signedIn':
|
||||
router.navigate('/dashboard').catch((e) => console.error(e));
|
||||
break;
|
||||
case 'signedOut':
|
||||
router.navigate('/').catch((e) => console.error(e));
|
||||
break;
|
||||
case 'signInWithRedirect_failure':
|
||||
break;
|
||||
case 'tokenRefresh':
|
||||
break;
|
||||
default:
|
||||
console.debug(`Unhandled event: ${data.payload.event}`);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RouterProvider
|
||||
router={router}
|
||||
fallbackElement={<Loader hasStartedLoading={true} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function protectedLoader() {
|
||||
console.debug('protected loader');
|
||||
if (!getRequestHandler()) {
|
||||
console.debug('creating request handler');
|
||||
createRequestHandler(
|
||||
async (config: AxiosRequestConfig): Promise<AxiosRequestConfig> => {
|
||||
const authSession = await fetchAuthSession();
|
||||
if (!config.headers) config.headers = {};
|
||||
config.headers.Authorization = `Bearer ${authSession?.tokens?.idToken}`;
|
||||
return config;
|
||||
},
|
||||
);
|
||||
}
|
||||
const authSession = await fetchAuthSession();
|
||||
if (!authSession?.tokens?.idToken) {
|
||||
return redirect('/login');
|
||||
}
|
||||
const isAdmin = await checkAdmin(getRequestHandler());
|
||||
if (!isAdmin) {
|
||||
return redirect('/not-admin');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loginLoader() {
|
||||
console.debug('login loader');
|
||||
const authSession = await fetchAuthSession();
|
||||
if (authSession?.tokens?.idToken) {
|
||||
console.debug('redirecting to dashboard');
|
||||
return redirect('/dashboard');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
id: 'public',
|
||||
path: '/',
|
||||
Component: Outlet,
|
||||
children: [
|
||||
{ path: '/', Component: Home, index: true },
|
||||
{ path: '/not-admin', Component: NotAdmin },
|
||||
{ path: '/login', Component: Login, loader: loginLoader },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'protected',
|
||||
path: '/',
|
||||
Component: Layout,
|
||||
loader: protectedLoader,
|
||||
children: [
|
||||
{ path: '/dashboard', Component: Dashboard },
|
||||
{
|
||||
path: '/admins',
|
||||
Component: Admins,
|
||||
loader: adminListLoader,
|
||||
},
|
||||
{ path: '/settings', Component: User, loader: userLoader },
|
||||
{ path: '/chart', Component: Charts },
|
||||
{ path: '/chart/:chartId', Component: Chart },
|
||||
],
|
||||
},
|
||||
]);
|
|
@ -0,0 +1,12 @@
|
|||
@use '../../../../styles/global';
|
||||
|
||||
.addUser,
|
||||
.addUserModal {
|
||||
.addUserButton {
|
||||
@include global.smallFont();
|
||||
|
||||
padding: 6px 12px;
|
||||
background: unset;
|
||||
border: 1px solid #bababa;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { createAdmin } from '@baseline/client-api/admin';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FormGroup,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from 'reactstrap';
|
||||
import { getRequestHandler } from '@baseline/client-api/request-handler';
|
||||
import styles from './AddAdmin.module.scss';
|
||||
import { Admin } from '@baseline/types/admin';
|
||||
|
||||
interface Props {
|
||||
setAllAdmins: React.Dispatch<React.SetStateAction<Admin[]>>;
|
||||
}
|
||||
|
||||
const AddAdmin = (props: Props) => {
|
||||
const { setAllAdmins } = props;
|
||||
const [newEmail, setNewEmail] = useState('');
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const toggle = () => {
|
||||
setNewEmail('');
|
||||
setIsModalOpen((open) => !open);
|
||||
};
|
||||
|
||||
const addUser = async (): Promise<void> => {
|
||||
const newAdmin = await createAdmin(getRequestHandler(), {
|
||||
userEmail: newEmail,
|
||||
});
|
||||
setAllAdmins((admins) => [...admins, newAdmin]);
|
||||
toggle();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.addUser}>
|
||||
<button className={styles.addUserButton} onClick={toggle}>
|
||||
Invite
|
||||
</button>
|
||||
<Modal
|
||||
className={styles.addUserModal}
|
||||
isOpen={isModalOpen}
|
||||
toggle={toggle}
|
||||
centered
|
||||
>
|
||||
<ModalHeader toggle={toggle}>Add Admin</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
value={newEmail}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button
|
||||
disabled={!newEmail}
|
||||
className={styles.addUserButton}
|
||||
onClick={() => {
|
||||
void addUser();
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAdmin;
|
|
@ -0,0 +1,69 @@
|
|||
@use '../../../../styles/global';
|
||||
|
||||
.userList {
|
||||
.admin,
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 18px 48px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #bababa;
|
||||
|
||||
@media screen and (max-width: global.$lg) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
.userCount {
|
||||
@include global.smallFont();
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
.details {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.name,
|
||||
.data {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.name {
|
||||
@include global.largeFont();
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.data {
|
||||
@include global.mediumFont();
|
||||
color: #707070;
|
||||
}
|
||||
}
|
||||
|
||||
.pill {
|
||||
@include global.smallFont();
|
||||
width: min-content;
|
||||
padding: 6px 18px;
|
||||
border: 1px solid #bababa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex: 0 0 auto;
|
||||
min-width: min-content;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import React, { useState } from 'react';
|
||||
import { deleteAdmin } from '@baseline/client-api/admin';
|
||||
import ConfirmDelete from '../../../../components/confirm-delete/ConfirmDelete';
|
||||
import AddUser from '../add-admin/AddAdmin';
|
||||
import styles from './AdminList.module.scss';
|
||||
import { getRequestHandler } from '@baseline/client-api/request-handler';
|
||||
import { Admin } from '@baseline/types/admin';
|
||||
|
||||
interface Props {
|
||||
admins: Admin[];
|
||||
}
|
||||
|
||||
const AdminList = (props: Props): JSX.Element => {
|
||||
const [allAdmins, setAllAdmins] = useState<Admin[]>(props?.admins || []);
|
||||
|
||||
const handleDelete = async (adminSub: string): Promise<void> => {
|
||||
await deleteAdmin(getRequestHandler(), { adminId: adminSub });
|
||||
setAllAdmins((admins) =>
|
||||
admins.filter((admin) => admin.userSub !== adminSub),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.userList}>
|
||||
<div className={styles.list}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.userCount}>
|
||||
There are {allAdmins.length} people in your team
|
||||
</div>
|
||||
<AddUser setAllAdmins={setAllAdmins} />
|
||||
</div>
|
||||
{allAdmins.map((admin) => (
|
||||
<div key={admin.userSub} className={styles.admin}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.name}>{admin.userEmail}</div>
|
||||
<div className={styles.data}>{admin.userSub}</div>
|
||||
</div>
|
||||
<div className={styles.pill}>Admin</div>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<ConfirmDelete
|
||||
itemName={admin.userEmail}
|
||||
deleteFunction={async () => {
|
||||
await handleDelete(admin.userSub);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminList;
|
26
packages/admin/src/baseblocks/admin/pages/Admins.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { Admin } from '@baseline/types/admin';
|
||||
import AdminList from '../components/admin-list/AdminList';
|
||||
import { useLoaderData } from 'react-router-dom';
|
||||
import { getAllAdmins } from '@baseline/client-api/admin';
|
||||
import { getRequestHandler } from '@baseline/client-api/request-handler';
|
||||
import PageContent from '../../../components/page-content/PageContent';
|
||||
|
||||
export async function adminListLoader() {
|
||||
const admins = await getAllAdmins(getRequestHandler());
|
||||
return {
|
||||
admins: admins,
|
||||
};
|
||||
}
|
||||
|
||||
const Admins = (): JSX.Element => {
|
||||
const { admins } = useLoaderData() as { admins: Admin[] };
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<AdminList admins={admins} />
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Admins;
|
|
@ -0,0 +1,7 @@
|
|||
.chartList {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import styles from './ChartDetail.module.scss';
|
||||
import { useChart } from '../../../../swr/chart';
|
||||
import Loader from '../../../../components/page-content/loader/Loader';
|
||||
import EditChartModal from '../edit-chart-modal/EditChartModal';
|
||||
|
||||
interface Props {
|
||||
chartId: string;
|
||||
}
|
||||
|
||||
const ChartDetail = (props: Props): JSX.Element => {
|
||||
const { chartId } = props;
|
||||
const { chart, isLoading, error } = useChart(chartId);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader hasStartedLoading={true} isLoading={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>{JSON.stringify(error)}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.chartList}>
|
||||
<div className={styles.header}>
|
||||
<h1>Charts</h1>
|
||||
<EditChartModal chart={chart} />
|
||||
</div>
|
||||
<hr />
|
||||
<pre>{JSON.stringify(chart, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartDetail;
|
|
@ -0,0 +1,7 @@
|
|||
.chartList {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import styles from './ChartList.module.scss';
|
||||
import CreateChartModal from '../create-chart-modal/CreateChartModal';
|
||||
import { useCharts } from '../../../../swr/chart';
|
||||
import Loader from '../../../../components/page-content/loader/Loader';
|
||||
|
||||
const ChartList = (): JSX.Element => {
|
||||
const { charts, isLoading } = useCharts();
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader hasStartedLoading={true} isLoading={true} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.chartList}>
|
||||
<div className={styles.header}>
|
||||
<h1>Charts</h1>
|
||||
<CreateChartModal />
|
||||
</div>
|
||||
<hr />
|
||||
<ul>
|
||||
{charts.map((chart) => (
|
||||
<li key={chart.chartId}>
|
||||
<a href={`/chart/${chart.chartId}`}>{chart.name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartList;
|
|
@ -0,0 +1,183 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { Chart, chartSchema } from '@baseline/types/chart';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
Button,
|
||||
FormFeedback,
|
||||
FormGroup,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from 'reactstrap';
|
||||
import { createChart } from '@baseline/client-api/chart';
|
||||
import { onChartCreated } from '../../../../swr/chart';
|
||||
|
||||
const CreateChartModal = (): JSX.Element => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggle = () => setIsOpen(!isOpen);
|
||||
|
||||
const initialValues: Chart = useMemo(
|
||||
() => ({
|
||||
chartId: undefined,
|
||||
parentChart: 'NO_PARENT',
|
||||
name: '',
|
||||
md5: '',
|
||||
sha256: '',
|
||||
resourceUri: '',
|
||||
comment: '',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
enableReinitialize
|
||||
validationSchema={chartSchema}
|
||||
onSubmit={async (values, { setSubmitting, resetForm }) => {
|
||||
setSubmitting(true);
|
||||
const newChart = await createChart(values);
|
||||
await onChartCreated(newChart);
|
||||
|
||||
resetForm();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{({
|
||||
values,
|
||||
handleSubmit,
|
||||
isValid,
|
||||
isSubmitting,
|
||||
resetForm,
|
||||
errors,
|
||||
setFieldValue,
|
||||
handleBlur,
|
||||
}) => (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
Create Chart
|
||||
</Button>
|
||||
<Modal
|
||||
centered
|
||||
isOpen={isOpen}
|
||||
toggle={() => {
|
||||
toggle();
|
||||
resetForm();
|
||||
}}
|
||||
>
|
||||
<ModalHeader toggle={toggle}>Create Chart</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
name="name"
|
||||
id="name"
|
||||
value={values.name}
|
||||
onChange={(e) => void setFieldValue('name', e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className={errors.name ? 'is-invalid' : ''}
|
||||
/>
|
||||
<FormFeedback>{errors.name}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="parentChart">Parent Chart</Label>
|
||||
<Input
|
||||
name="parentChart"
|
||||
id="parentChart"
|
||||
value={values.parentChart}
|
||||
onChange={(e) =>
|
||||
void setFieldValue('parentChart', e.target.value)
|
||||
}
|
||||
onBlur={handleBlur}
|
||||
className={errors.parentChart ? 'is-invalid' : ''}
|
||||
disabled
|
||||
placeholder="This will be a dropdown at some point"
|
||||
/>
|
||||
<FormFeedback>{errors.parentChart}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="md5">MD5</Label>
|
||||
<Input
|
||||
name="md5"
|
||||
id="md5"
|
||||
value={values.md5}
|
||||
onChange={(e) => void setFieldValue('md5', e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className={errors.md5 ? 'is-invalid' : ''}
|
||||
/>
|
||||
<FormFeedback>{errors.md5}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="sha256">SHA256</Label>
|
||||
<Input
|
||||
name="sha256"
|
||||
id="sha256"
|
||||
value={values.sha256}
|
||||
onChange={(e) => void setFieldValue('sha256', e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className={errors.sha256 ? 'is-invalid' : ''}
|
||||
/>
|
||||
<FormFeedback>{errors.sha256}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="resourceUri">Resource URI</Label>
|
||||
<Input
|
||||
name="resourceUri"
|
||||
id="resourceUri"
|
||||
value={values.resourceUri}
|
||||
onChange={(e) =>
|
||||
void setFieldValue('resourceUri', e.target.value)
|
||||
}
|
||||
onBlur={handleBlur}
|
||||
className={errors.resourceUri ? 'is-invalid' : ''}
|
||||
/>
|
||||
<FormFeedback>{errors.resourceUri}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="comment">Comment</Label>
|
||||
<textarea
|
||||
name="comment"
|
||||
id="comment"
|
||||
value={values.comment}
|
||||
onChange={(e) =>
|
||||
void setFieldValue('comment', e.target.value)
|
||||
}
|
||||
onBlur={handleBlur}
|
||||
className={`${
|
||||
errors.comment ? 'is-invalid' : ''
|
||||
} form-control`}
|
||||
/>
|
||||
<FormFeedback>{errors.comment}</FormFeedback>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
void handleSubmit();
|
||||
}}
|
||||
disabled={!isValid || isSubmitting}
|
||||
color="primary"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateChartModal;
|
|
@ -0,0 +1,175 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Chart, chartSchema } from '@baseline/types/chart';
|
||||
import { Formik } from 'formik';
|
||||
import {
|
||||
Button,
|
||||
FormFeedback,
|
||||
FormGroup,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from 'reactstrap';
|
||||
import { updateChart } from '@baseline/client-api/chart';
|
||||
import { onChartUpdated } from '../../../../swr/chart';
|
||||
|
||||
interface Props {
|
||||
chart: Chart;
|
||||
}
|
||||
|
||||
const EditChartModal = (props: Props): JSX.Element => {
|
||||
const { chart } = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggle = () => setIsOpen(!isOpen);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={chart}
|
||||
enableReinitialize
|
||||
validationSchema={chartSchema}
|
||||
onSubmit={async (values, { setSubmitting, resetForm }) => {
|
||||
setSubmitting(true);
|
||||
const newChart = await updateChart(values);
|
||||
await onChartUpdated(newChart);
|
||||
|
||||
resetForm();
|
||||
setIsOpen(false);
|
||||
setSubmitting(false);
|
||||
}}
|
||||
>
|
||||
{({
|
||||
values,
|
||||
handleSubmit,
|
||||
isValid,
|
||||
isSubmitting,
|
||||
resetForm,
|
||||
errors,
|
||||
setFieldValue,
|
||||
handleBlur,
|
||||
}) => (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
Edit Chart
|
||||
</Button>
|
||||
<Modal
|
||||
centered
|
||||
isOpen={isOpen}
|
||||
toggle={() => {
|
||||
toggle();
|
||||
resetForm();
|
||||
}}
|
||||
>
|
||||
<ModalHeader toggle={toggle}>Create Chart</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
name="name"
|
||||
id="name"
|
||||
value={values.name}
|
||||
onChange={(e) => void setFieldValue('name', e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className={errors.name ? 'is-invalid' : ''}
|
||||
/>
|
||||
<FormFeedback>{errors.name}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="parentChart">Parent Chart</Label>
|
||||
<Input
|
||||
name="parentChart"
|
||||
id="parentChart"
|
||||
value={values.parentChart}
|
||||
onChange={(e) =>
|
||||
void setFieldValue('parentChart', e.target.value)
|
||||
}
|
||||
onBlur={handleBlur}
|
||||
className={errors.parentChart ? 'is-invalid' : ''}
|
||||
disabled
|
||||
placeholder={'This will be a dropdown at some point'}
|
||||
/>
|
||||
<FormFeedback>{errors.parentChart}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="md5">MD5</Label>
|
||||
<Input
|
||||
name="md5"
|
||||
id="md5"
|
||||
value={values.md5}
|
||||
onChange={(e) => void setFieldValue('md5', e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className={errors.md5 ? 'is-invalid' : ''}
|
||||
/>
|
||||
<FormFeedback>{errors.md5}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="sha256">SHA256</Label>
|
||||
<Input
|
||||
name="sha256"
|
||||
id="sha256"
|
||||
value={values.sha256}
|
||||
onChange={(e) => void setFieldValue('sha256', e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
className={errors.sha256 ? 'is-invalid' : ''}
|
||||
/>
|
||||
<FormFeedback>{errors.sha256}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="resourceUri">Resource URI</Label>
|
||||
<Input
|
||||
name="resourceUri"
|
||||
id="resourceUri"
|
||||
value={values.resourceUri}
|
||||
onChange={(e) =>
|
||||
void setFieldValue('resourceUri', e.target.value)
|
||||
}
|
||||
onBlur={handleBlur}
|
||||
className={errors.resourceUri ? 'is-invalid' : ''}
|
||||
/>
|
||||
<FormFeedback>{errors.resourceUri}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label htmlFor="comment">Comment</Label>
|
||||
<textarea
|
||||
name="comment"
|
||||
id="comment"
|
||||
value={values.comment}
|
||||
onChange={(e) =>
|
||||
void setFieldValue('comment', e.target.value)
|
||||
}
|
||||
onBlur={handleBlur}
|
||||
className={`${
|
||||
errors.comment ? 'is-invalid' : ''
|
||||
} form-control`}
|
||||
/>
|
||||
<FormFeedback>{errors.comment}</FormFeedback>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
void handleSubmit();
|
||||
}}
|
||||
disabled={!isValid || isSubmitting}
|
||||
color="primary"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditChartModal;
|
16
packages/admin/src/baseblocks/chart/pages/Chart.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import PageContent from '../../../components/page-content/PageContent';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ChartDetail from '../components/chart-detail/ChartDetail';
|
||||
|
||||
const Chart = (): JSX.Element => {
|
||||
const { chartId } = useParams();
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<ChartDetail chartId={chartId} />
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chart;
|
13
packages/admin/src/baseblocks/chart/pages/charts.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import PageContent from '../../../components/page-content/PageContent';
|
||||
import ChartList from '../components/chart-list/ChartList';
|
||||
|
||||
const charts = (): JSX.Element => {
|
||||
return (
|
||||
<PageContent>
|
||||
<ChartList />
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default charts;
|
|
@ -0,0 +1,55 @@
|
|||
@use '../../../../styles/global';
|
||||
|
||||
.dashboard {
|
||||
h1 {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
@media screen and (max-width: global.$xl) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.links {
|
||||
a {
|
||||
@include global.mediumFont();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 16px;
|
||||
padding: 32px;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
background: #fff;
|
||||
border: 1px solid #bababa;
|
||||
|
||||
&::before {
|
||||
@include global.mediumFont();
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 16px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
background: #000;
|
||||
border-radius: 10px;
|
||||
content: '+';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin-bottom: 16px;
|
||||
padding: 32px;
|
||||
background: #fff;
|
||||
border: 1px solid #bababa;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import styles from './DashboardContent.module.scss';
|
||||
|
||||
const DashboardContent = (): JSX.Element => {
|
||||
return (
|
||||
<div className={styles.dashboard}>
|
||||
<h1>Dashboard</h1>
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.preview}>
|
||||
<h2>My Site Preview</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardContent;
|
11
packages/admin/src/baseblocks/dashboard/pages/Dashboard.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import PageContent from '../../../components/page-content/PageContent';
|
||||
import DashboardContent from '../components/dashboard-content/DashboardContent';
|
||||
|
||||
const Dashboard = (): JSX.Element => (
|
||||
<PageContent>
|
||||
<DashboardContent />
|
||||
</PageContent>
|
||||
);
|
||||
|
||||
export default Dashboard;
|
22
packages/admin/src/baseblocks/home/pages/Home.module.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
@use '../../../styles/global';
|
||||
|
||||
.home {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
flex: 1 1 auto;
|
||||
|
||||
> div:last-of-type {
|
||||
flex: 1 1 auto;
|
||||
min-width: min(100vw, 390px);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
16
packages/admin/src/baseblocks/home/pages/Home.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styles from './Home.module.scss';
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<div className={styles.home}>
|
||||
<div className={styles.content}>
|
||||
<h1>Home</h1>
|
||||
<p>Welcome to the home page</p>
|
||||
<Link to="/dashboard">Dashboard</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Home;
|
22
packages/admin/src/baseblocks/login/pages/Login.module.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
@use '../../../styles/global';
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
flex: 1 1 auto;
|
||||
|
||||
> div:last-of-type {
|
||||
flex: 1 1 auto;
|
||||
min-width: min(100vw, 390px);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
14
packages/admin/src/baseblocks/login/pages/Login.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import { Authenticator } from '@aws-amplify/ui-react';
|
||||
import styles from './Login.module.scss';
|
||||
|
||||
function Login() {
|
||||
return (
|
||||
<div className={styles.login}>
|
||||
<div className={styles.content}>
|
||||
<Authenticator />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Login;
|
|
@ -0,0 +1,26 @@
|
|||
@use '../../../styles/global';
|
||||
|
||||
.notAdmin {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
flex: 1 1 auto;
|
||||
|
||||
> div:last-of-type {
|
||||
flex: 1 1 auto;
|
||||
min-width: min(100vw, 390px);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
@include global.genericButton();
|
||||
}
|
||||
}
|
||||
}
|
31
packages/admin/src/baseblocks/not-admin/pages/NotAdmin.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import styles from './NotAdmin.module.scss';
|
||||
import { signOut } from 'aws-amplify/auth';
|
||||
import { redirect } from 'react-router-dom';
|
||||
|
||||
async function signOutButton() {
|
||||
await signOut();
|
||||
redirect('/');
|
||||
}
|
||||
|
||||
function NotAdmin() {
|
||||
return (
|
||||
<div className={styles.notAdmin}>
|
||||
<div className={styles.content}>
|
||||
<div>
|
||||
<h1>Please contact your system administrator</h1>
|
||||
<p>You do not have permission to view this content</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
void signOutButton();
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotAdmin;
|
|
@ -0,0 +1,52 @@
|
|||
@use '../../../../styles/global';
|
||||
|
||||
.userSettings {
|
||||
h1 {
|
||||
@include global.largeFont();
|
||||
margin-bottom: 32px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.settings {
|
||||
@include global.smallFont();
|
||||
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 18px 48px;
|
||||
background: #fff;
|
||||
border: 1px solid #bababa;
|
||||
|
||||
@media screen and (max-width: global.$lg) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 16px;
|
||||
border: 1px solid #bababa;
|
||||
border-radius: unset;
|
||||
}
|
||||
|
||||
button {
|
||||
@include global.smallFont();
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border: 1px solid #bababa;
|
||||
}
|
||||
|
||||
.email {
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
border-left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.signOut {
|
||||
@include global.smallFont();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
import {
|
||||
signOut,
|
||||
updateUserAttributes,
|
||||
confirmUserAttribute,
|
||||
fetchUserAttributes,
|
||||
} from 'aws-amplify/auth';
|
||||
import React, { useState } from 'react';
|
||||
import { FormFeedback, FormGroup, Input, Label } from 'reactstrap';
|
||||
import styles from './UserSettings.module.scss';
|
||||
|
||||
interface Props {
|
||||
user: { email: string; email_verified: boolean };
|
||||
}
|
||||
|
||||
const UserSettings = (props: Props): JSX.Element => {
|
||||
const [email, setEmail] = useState<string>(props?.user?.email);
|
||||
const [isChangingEmail, setIsChangingEmail] = useState(false);
|
||||
const [isEmailVerified, setIsEmailVerified] = useState(
|
||||
props?.user?.email_verified,
|
||||
);
|
||||
const [isCodeInvalid, setIsCodeInvalid] = useState<undefined | boolean>();
|
||||
const [changingEmailCode, setChangingEmailCode] = useState('');
|
||||
|
||||
const handleEmailChange = async () => {
|
||||
const attributes = await fetchUserAttributes();
|
||||
setIsChangingEmail(false);
|
||||
if (attributes.email !== email) {
|
||||
setIsEmailVerified(false);
|
||||
await updateUserAttributes({
|
||||
userAttributes: {
|
||||
email: email,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const finalizeEmailChange = async () => {
|
||||
try {
|
||||
await confirmUserAttribute({
|
||||
userAttributeKey: 'email',
|
||||
confirmationCode: changingEmailCode,
|
||||
});
|
||||
setIsEmailVerified(true);
|
||||
setChangingEmailCode('');
|
||||
} catch {
|
||||
console.log('Invalid code');
|
||||
setIsCodeInvalid(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.userSettings}>
|
||||
<h1>Account settings</h1>
|
||||
<div className={styles.settings}>
|
||||
<FormGroup>
|
||||
<Label for="email">Email</Label>
|
||||
<div className={styles.email}>
|
||||
<Input
|
||||
name="email"
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
disabled={!isChangingEmail}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
disabled={!isEmailVerified}
|
||||
onClick={() => {
|
||||
isChangingEmail
|
||||
? void handleEmailChange()
|
||||
: setIsChangingEmail(true);
|
||||
}}
|
||||
>
|
||||
{isChangingEmail ? 'Update' : 'Edit'}
|
||||
</button>
|
||||
</div>
|
||||
</FormGroup>
|
||||
{!isEmailVerified ? (
|
||||
<FormGroup>
|
||||
<Label for="code">Check your email for a code</Label>
|
||||
<div className={styles.email}>
|
||||
<Input
|
||||
name="code"
|
||||
id="code"
|
||||
type="text"
|
||||
placeholder="Code"
|
||||
invalid={isCodeInvalid}
|
||||
value={changingEmailCode}
|
||||
onChange={(e) => setChangingEmailCode(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
void finalizeEmailChange();
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsEmailVerified(true);
|
||||
setIsChangingEmail(true);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<FormFeedback className={isCodeInvalid ? 'd-block' : ''}>
|
||||
Code is invalid
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<button
|
||||
className={styles.signOut}
|
||||
onClick={() => {
|
||||
void signOut();
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSettings;
|
34
packages/admin/src/baseblocks/user/pages/User.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import PageContent from '../../../components/page-content/PageContent';
|
||||
import UserSettings from '../components/user-settings/UserSettings';
|
||||
import { fetchUserAttributes } from 'aws-amplify/auth';
|
||||
import { useLoaderData } from 'react-router-dom';
|
||||
|
||||
export async function userLoader() {
|
||||
const { email, email_verified } = await fetchUserAttributes();
|
||||
return {
|
||||
user: {
|
||||
email,
|
||||
email_verified,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const User = (): JSX.Element => {
|
||||
const { user } = useLoaderData() as {
|
||||
user: { email: string; email_verified: boolean };
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<UserSettings
|
||||
user={{
|
||||
email: user.email,
|
||||
email_verified: user.email_verified,
|
||||
}}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default User;
|
|
@ -0,0 +1,19 @@
|
|||
.confirmDelete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.deleteModal,
|
||||
.confirmDelete {
|
||||
.deleteButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font: normal normal normal 15px/22px 'Montserrat', sans-serif;
|
||||
background: unset;
|
||||
border: unset;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
FormGroup,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from 'reactstrap';
|
||||
import styles from './ConfirmDelete.module.scss';
|
||||
|
||||
interface Props {
|
||||
itemName: string;
|
||||
deleteFunction(): Promise<void>;
|
||||
deleteString?: string;
|
||||
buttonProps?: React.DetailedHTMLProps<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
>;
|
||||
}
|
||||
|
||||
const ConfirmDelete = (props: Props): JSX.Element => {
|
||||
const {
|
||||
itemName,
|
||||
deleteFunction,
|
||||
deleteString = itemName,
|
||||
buttonProps,
|
||||
} = props;
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const toggle = () => setIsModalOpen((open) => !open);
|
||||
const [deleteType, setDeleteType] = useState('');
|
||||
|
||||
const handleDelete = async (): Promise<void> => {
|
||||
toggle();
|
||||
setDeleteType('');
|
||||
await deleteFunction();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.confirmDelete}>
|
||||
<button {...buttonProps} onClick={toggle} className={styles.deleteButton}>
|
||||
Delete
|
||||
</button>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
toggle={toggle}
|
||||
centered
|
||||
className={styles.deleteModal}
|
||||
>
|
||||
<ModalHeader toggle={toggle}>
|
||||
Delete "{itemName}"?
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<Label for="delete">
|
||||
Please type <b>{deleteString}</b> to confirm deletion
|
||||
</Label>
|
||||
<Input
|
||||
id="delete"
|
||||
name="delete"
|
||||
autoComplete="off"
|
||||
placeholder={deleteString}
|
||||
value={deleteType}
|
||||
onChange={(e) => {
|
||||
setDeleteType(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<button
|
||||
disabled={deleteString !== deleteType}
|
||||
onClick={() => {
|
||||
void handleDelete();
|
||||
}}
|
||||
className={styles.deleteButton}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmDelete;
|
25
packages/admin/src/components/layout/Layout.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import { Outlet, useNavigation } from 'react-router-dom';
|
||||
import Sidebar from '../sidebar/Sidebar';
|
||||
import PageContent from '../page-content/PageContent';
|
||||
import Loader from '../page-content/loader/Loader';
|
||||
|
||||
const Layout = () => {
|
||||
const navigation = useNavigation();
|
||||
const isLoading = navigation.state === 'loading';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar />
|
||||
{isLoading ? (
|
||||
<PageContent>
|
||||
<Loader hasStartedLoading={true} />
|
||||
</PageContent>
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
|
@ -0,0 +1,20 @@
|
|||
@keyframes fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.pageContent {
|
||||
position: relative;
|
||||
padding: 98px min(12vw, 144px);
|
||||
background: #efefef;
|
||||
width: 100%;
|
||||
|
||||
.children {
|
||||
animation: fade 500ms forwards ease-in-out;
|
||||
}
|
||||
}
|
20
packages/admin/src/components/page-content/PageContent.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import styles from './PageContent.module.scss';
|
||||
|
||||
interface Props {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
const PageContent = (props: Props) => {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.pageContent}>
|
||||
<div className={styles.children}>{children}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageContent;
|
|
@ -0,0 +1,51 @@
|
|||
@keyframes pulse {
|
||||
0% {
|
||||
filter: grayscale(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: grayscale(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #efefef;
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
pointer-events: none;
|
||||
|
||||
&.visible,
|
||||
&.textVisible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.textVisible {
|
||||
svg {
|
||||
animation: fade 600ms forwards ease-in-out,
|
||||
pulse 1.2s infinite ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
70
packages/admin/src/components/page-content/loader/Loader.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import styles from './Loader.module.scss';
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
hasStartedLoading?: boolean;
|
||||
}
|
||||
|
||||
const Loader = (props: Props): JSX.Element => {
|
||||
const [isLoadingTextShowing, setIsLoadingTextShowing] = useState(false);
|
||||
const [isLoaderShowing, setIsLoaderShowing] = useState(false);
|
||||
const [loadingTimeout, setLoadingTimeout] = useState<unknown>();
|
||||
const { isLoading, hasStartedLoading } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasStartedLoading) {
|
||||
setIsLoaderShowing(true);
|
||||
setLoadingTimeout(
|
||||
setTimeout(() => {
|
||||
setIsLoadingTextShowing(true);
|
||||
}, 250),
|
||||
);
|
||||
}
|
||||
}, [hasStartedLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasStartedLoading && !isLoading) {
|
||||
setIsLoadingTextShowing(false);
|
||||
setIsLoaderShowing(false);
|
||||
clearTimeout(loadingTimeout as NodeJS.Timeout);
|
||||
}
|
||||
}, [hasStartedLoading, isLoading, loadingTimeout]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.loader} ${
|
||||
isLoadingTextShowing ? styles.textVisible : ''
|
||||
} ${isLoaderShowing ? styles.visible : ''}`}
|
||||
>
|
||||
{isLoadingTextShowing ? (
|
||||
<>
|
||||
<svg width="77" height="81" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path
|
||||
fill="#ffd600"
|
||||
d="M2.791 25.101c6.527 3.225 12.977 6.45 19.427 9.675l13.9 6.911a3.78 3.78 0 0 0 1.305.461c.23.077.384.154.614.23l.23.077.23-.077c.23-.077.384-.154.614-.23.498-.145.986-.325 1.459-.538 5.068-2.534 10.136-5.068 15.2-7.525 6.066-2.995 12.209-6.066 18.275-9.061a4.186 4.186 0 0 0 2.615-3.689 3.961 3.961 0 0 0-2.611-3.839l-9.522-4.761C56.695 8.822 48.479 4.829 40.416.913a5.533 5.533 0 0 0-4.146 0c-7.909 3.839-15.972 7.832-23.728 11.672l-9.751 4.837a4.119 4.119 0 0 0-2.688 3.763 4.061 4.061 0 0 0 2.688 3.916m59.817-3.225c-1.613.768-3.148 1.613-4.684 2.38l-2.534 1.229a1063.588 1063.588 0 0 1-16.279 7.986 2.12 2.12 0 0 1-1.536 0c-6.757-3.3-13.515-6.68-20.272-9.982l-4.3-2.15 17.047-8.447c2.611-1.305 5.222-2.611 7.832-3.839.268-.108.556-.16.845-.154 8.063 3.993 16.048 7.986 24.111 11.979l.921.461Z"
|
||||
/>
|
||||
<path
|
||||
fill="#ffd600"
|
||||
d="M2.637 38.154a19132.396 19132.396 0 0 0 33.326 16.509c.495.224 1.01.403 1.537.535l.691.23.23.077.23-.077.691-.23c.55-.17 1.089-.376 1.613-.614 5.759-2.841 11.518-5.682 17.277-8.6l10.981-5.452c1.766-.845 3.532-1.689 5.222-2.611a3.939 3.939 0 0 0 2-4.838 3.882 3.882 0 0 0-4.454-2.611c-.7.162-1.372.42-2 .768l-3.611 1.846c-8.907 4.377-17.815 8.831-26.722 13.284a2.5 2.5 0 0 1-2.534 0A5805.147 5805.147 0 0 0 6.169 31.013c-2.3-1.152-4.607-.538-5.682 1.536a3.751 3.751 0 0 0-.307 2.995 5.054 5.054 0 0 0 2.457 2.611"
|
||||
/>
|
||||
<path
|
||||
fill="#f5931e"
|
||||
d="M70.67 56.046a5941.672 5941.672 0 0 0-31.406 15.588 1.662 1.662 0 0 1-1.689 0 8859.35 8859.35 0 0 0-31.56-15.588 4.608 4.608 0 0 0-2.457-.537 3.756 3.756 0 0 0-3.379 3.3 3.847 3.847 0 0 0 2.38 4.3 5055.77 5055.77 0 0 0 33.633 16.664c.447.212.909.392 1.382.538.23.077.384.154.614.23l.23.077.23-.077c.23-.077.384-.154.614-.23.461-.154.921-.307 1.305-.461 11.213-5.531 22.424-11.131 33.635-16.663a5.276 5.276 0 0 0 1.612-1.228 4.037 4.037 0 0 0 0-4.991c-1.229-1.613-3.148-1.92-5.145-.921"
|
||||
/>
|
||||
<path
|
||||
fill="#f5931e"
|
||||
d="m2.791 50.748 10.827 5.375c7.525 3.686 15.05 7.448 22.575 11.211.69.35 1.453.534 2.227.538.775.005 1.54-.18 2.227-.538 1.766-.921 3.532-1.766 5.3-2.688l7.6-3.763 20.5-10.136a4.1 4.1 0 0 0 2.611-3.686 3.884 3.884 0 0 0-1.689-3.379 4.583 4.583 0 0 0-4.454 0l-31.1 15.434c-.61.385-1.39.385-2 0-8.6-4.377-17.123-8.6-25.724-12.823l-5.368-2.686c-2.457-1.229-4.684-.61-5.759 1.459a3.6 3.6 0 0 0-.307 2.918 4.654 4.654 0 0 0 2.534 2.764"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
112
packages/admin/src/components/sidebar/Sidebar.module.scss
Normal file
|
@ -0,0 +1,112 @@
|
|||
$horizontalPadding: 44px;
|
||||
$width: 250px;
|
||||
$totalSidebarSize: calc((#{$horizontalPadding} * 2) + #{$width});
|
||||
$collapseSize: calc(((#{$horizontalPadding} * 2) + #{$width}) * -1);
|
||||
$animationSpeed: 250ms;
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
flex: 0 1 auto;
|
||||
width: $totalSidebarSize;
|
||||
height: 100vh;
|
||||
padding: 48px $horizontalPadding 32px;
|
||||
background: #fff;
|
||||
transform: translateX(0);
|
||||
transition: margin-left $animationSpeed linear;
|
||||
|
||||
.toggler {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: -42px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-top: 16px solid #b2b2b2;
|
||||
border-left: 16px solid #b2b2b2;
|
||||
transform: rotate(0deg);
|
||||
cursor: pointer;
|
||||
opacity: 0.25;
|
||||
transition: transform calc(#{$animationSpeed} - 100ms) ease-in-out,
|
||||
opacity calc(#{$animationSpeed} - 100ms) ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: rotate(45deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
margin-left: $collapseSize;
|
||||
|
||||
.toggler {
|
||||
transform: rotate(90deg);
|
||||
|
||||
&:hover {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 48px;
|
||||
height: 40px;
|
||||
max-height: 50px;
|
||||
max-width: 200px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-top: 48px;
|
||||
|
||||
img {
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.link,
|
||||
.spacer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
|
||||
img {
|
||||
flex: 0 0 20px;
|
||||
width: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 15px;
|
||||
color: #707070;
|
||||
font: normal normal 300 17px/20px Montserrat;
|
||||
border-bottom: 1px solid #fff0;
|
||||
transition: border-bottom 150ms ease-in-out;
|
||||
}
|
||||
|
||||
&.active {
|
||||
img {
|
||||
/** Since its not an svg in DOM we cant edit fill */
|
||||
filter: brightness(0);
|
||||
}
|
||||
|
||||
span {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
border-bottom: thin solid #707070;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
packages/admin/src/components/sidebar/Sidebar.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import styles from './Sidebar.module.scss';
|
||||
|
||||
const Sidebar = (): JSX.Element => {
|
||||
const location = useLocation();
|
||||
const [isToggled, setIsToggled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.innerWidth < 400) {
|
||||
setIsToggled(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`${styles.sidebar} ${isToggled ? styles.collapsed : ''}`}>
|
||||
<div
|
||||
className={styles.toggler}
|
||||
onClick={() => {
|
||||
setIsToggled((toggled) => !toggled);
|
||||
}}
|
||||
></div>
|
||||
<img className={styles.logo} src="/logo.png" alt="Baseline" />
|
||||
|
||||
<div className={styles.links}>
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className={`${styles.link} ${
|
||||
location.pathname === '/dashboard' ? styles.active : ''
|
||||
}`}
|
||||
>
|
||||
<img src="/icons/home.svg" alt="Home" />
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
|
||||
<div className={styles.spacer} />
|
||||
|
||||
<Link
|
||||
to="/chart"
|
||||
className={`${styles.link} ${
|
||||
location.pathname === '/chart' ? styles.active : ''
|
||||
}`}
|
||||
>
|
||||
<img src="/icons/users.svg" alt="Charts" />
|
||||
<span>Charts</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/admins"
|
||||
className={`${styles.link} ${
|
||||
location.pathname === '/admins' ? styles.active : ''
|
||||
}`}
|
||||
>
|
||||
<img src="/icons/users.svg" alt="Admins" />
|
||||
<span>Admins</span>
|
||||
</Link>
|
||||
|
||||
<div className={styles.spacer} />
|
||||
|
||||
<Link
|
||||
to="/settings"
|
||||
className={`${styles.link} ${
|
||||
location.pathname === '/settings' ? styles.active : ''
|
||||
}`}
|
||||
>
|
||||
<img src="/icons/gear.svg" alt="Settings" />
|
||||
<span>Account Settings</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
39
packages/admin/src/index.scss
Normal file
|
@ -0,0 +1,39 @@
|
|||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
.loader-container div {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #a0a0a0;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
flex: 1 1 auto;
|
||||
|
||||
> div:last-of-type {
|
||||
flex: 1 1 auto;
|
||||
min-width: min(100vw, 390px);
|
||||
}
|
||||
}
|
||||
|
||||
[id=root] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1 1 auto;
|
||||
}
|
11
packages/admin/src/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import * as ReactDOM from 'react-dom/client';
|
||||
import './styles/fonts.scss';
|
||||
import './index.scss';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
1
packages/admin/src/react-app-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="react-scripts" />
|
90
packages/admin/src/styles/_global.scss
Normal file
|
@ -0,0 +1,90 @@
|
|||
$xl: 1200px;
|
||||
$lg: 992px;
|
||||
$md: 768px;
|
||||
$sm: 576px;
|
||||
$animationSpeed: 150ms;
|
||||
$breakSize: $md;
|
||||
|
||||
@mixin genericButton {
|
||||
@include smallFont(normal, 600);
|
||||
|
||||
display: inline-block;
|
||||
padding: 12px 108px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background: #3a3838;
|
||||
border: 2px solid #3a3838;
|
||||
border-radius: 32px;
|
||||
transition: background-color $animationSpeed ease-in-out,
|
||||
color $animationSpeed ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: #3a3838;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin tinyFont(
|
||||
$style: normal,
|
||||
$weight: normal,
|
||||
$fontFace: 'Montserrat',
|
||||
$fallback: sans-serif
|
||||
) {
|
||||
font: $style normal $weight 12px/20px $fontFace, $fallback;
|
||||
|
||||
@media screen and (max-width: $breakSize) {
|
||||
font: $style normal $weight 8px/16px $fontFace, $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin smallFont(
|
||||
$style: normal,
|
||||
$weight: normal,
|
||||
$fontFace: 'Montserrat',
|
||||
$fallback: sans-serif
|
||||
) {
|
||||
font: $style normal $weight 16px/24px $fontFace, $fallback;
|
||||
|
||||
@media screen and (max-width: $breakSize) {
|
||||
font: $style normal $weight 12px/20px $fontFace, $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mediumFont(
|
||||
$style: normal,
|
||||
$weight: normal,
|
||||
$fontFace: 'Montserrat',
|
||||
$fallback: sans-serif
|
||||
) {
|
||||
font: $style normal $weight 24px/32px $fontFace, $fallback;
|
||||
|
||||
@media screen and (max-width: $breakSize) {
|
||||
font: $style normal $weight 16px/24px $fontFace, $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin largeFont(
|
||||
$style: normal,
|
||||
$weight: normal,
|
||||
$fontFace: 'Montserrat',
|
||||
$fallback: sans-serif
|
||||
) {
|
||||
font: $style normal $weight 40px/49px $fontFace, $fallback;
|
||||
|
||||
@media screen and (max-width: $breakSize) {
|
||||
font: $style normal $weight 24px/32px $fontFace, $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin hugeFont(
|
||||
$style: normal,
|
||||
$weight: normal,
|
||||
$fontFace: 'Montserrat',
|
||||
$fallback: sans-serif
|
||||
) {
|
||||
font: $style normal $weight 72px/88px $fontFace, $fallback;
|
||||
|
||||
@media screen and (max-width: $breakSize) {
|
||||
font: $style normal $weight 40px/49px $fontFace, $fallback;
|
||||
}
|
||||
}
|
2
packages/admin/src/styles/fonts.scss
Normal file
|
@ -0,0 +1,2 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;800;900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;600&display=swap');
|
75
packages/admin/src/swr/chart.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import useSWR, { mutate } from 'swr';
|
||||
import { getAllCharts, getChart } from '@baseline/client-api/chart';
|
||||
import { Chart } from '@baseline/types/chart';
|
||||
|
||||
export const useCharts = () => {
|
||||
const { data, error, isLoading, isValidating } = useSWR<Chart[], unknown>(
|
||||
`chart/list`,
|
||||
() => getAllCharts(),
|
||||
);
|
||||
|
||||
return {
|
||||
charts: data,
|
||||
isLoading,
|
||||
error,
|
||||
isValidating,
|
||||
};
|
||||
};
|
||||
|
||||
export const useChart = (chartId: string) => {
|
||||
const { data, error, isLoading, isValidating } = useSWR<Chart, unknown>(
|
||||
chartId ? `chart/${chartId}` : null,
|
||||
() => getChart(chartId),
|
||||
);
|
||||
|
||||
return {
|
||||
chart: data,
|
||||
isLoading,
|
||||
error,
|
||||
isValidating,
|
||||
};
|
||||
};
|
||||
|
||||
export const onChartCreated = async (chart: Chart) => {
|
||||
await Promise.all([
|
||||
mutate<Chart[]>(
|
||||
`chart/list`,
|
||||
(existingCharts) => [...(existingCharts || []), chart],
|
||||
{
|
||||
revalidate: false,
|
||||
},
|
||||
),
|
||||
mutate<Chart>(`chart/${chart.chartId}`, chart),
|
||||
]);
|
||||
};
|
||||
|
||||
export const onChartUpdated = async (chart: Chart) => {
|
||||
await Promise.all([
|
||||
mutate<Chart[]>(
|
||||
`chart/list`,
|
||||
(existingCharts) =>
|
||||
(existingCharts || []).map((chartExisting) =>
|
||||
chartExisting.chartId === chart.chartId ? chart : chartExisting,
|
||||
),
|
||||
{
|
||||
revalidate: false,
|
||||
},
|
||||
),
|
||||
mutate<Chart>(`chart/${chart.chartId}`, chart),
|
||||
]);
|
||||
};
|
||||
|
||||
export const onChartDeleted = async (chart: Chart) => {
|
||||
await Promise.all([
|
||||
mutate<Chart[]>(
|
||||
`chart/list`,
|
||||
(existingCharts) =>
|
||||
(existingCharts || []).filter(
|
||||
(chartExisting) => chartExisting.chartId !== chart.chartId,
|
||||
),
|
||||
{
|
||||
revalidate: false,
|
||||
},
|
||||
),
|
||||
]);
|
||||
};
|
5
packages/admin/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
27
packages/admin/tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"../../shared/types",
|
||||
"../../shared/client-api",
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
41
packages/admin/vite.config.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import EnvironmentPlugin from 'vite-plugin-environment';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
EnvironmentPlugin([
|
||||
'REACT_APP_APP_NAME',
|
||||
'REACT_APP_AWS_PROFILE',
|
||||
'REACT_APP_API_URL',
|
||||
'REACT_APP_COGNITO_IDENTITY_POOL_ID',
|
||||
'REACT_APP_COGNITO_USER_POOL_ID',
|
||||
'REACT_APP_COGNITO_USER_POOL_WEB_CLIENT_ID',
|
||||
]),
|
||||
],
|
||||
envPrefix: 'REACT_APP_',
|
||||
define: {},
|
||||
resolve: {
|
||||
alias: {
|
||||
'./runtimeConfig': './runtimeConfig.browser',
|
||||
},
|
||||
},
|
||||
esbuild: {
|
||||
minifyWhitespace: true,
|
||||
treeShaking: true,
|
||||
},
|
||||
build: {
|
||||
outDir: '.dist',
|
||||
minify: 'esbuild',
|
||||
chunkSizeWarningLimit: 1500,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
amplify: ['@aws-amplify/ui-react', 'aws-amplify'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|