diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7363dc7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +README.md +.DS_Store \ No newline at end of file diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..4883d8e --- /dev/null +++ b/.env.template @@ -0,0 +1,2 @@ +REACT_APP_AUTH_SERVICE_URL = https://dev-auth.fiskerdps.com +REACT_APP_UPLOAD_SERVICE_URL = http://localhost:8080/api/upload \ No newline at end of file diff --git a/.eslintcache b/.eslintcache deleted file mode 100644 index 79c070b..0000000 --- a/.eslintcache +++ /dev/null @@ -1 +0,0 @@ -[{"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/index.js":"1","/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/reportWebVitals.js":"2","/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/components/ErrorBoundary.jsx":"3","/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/components/App/index.js":"4"},{"size":610,"mtime":1609866784202,"results":"5","hashOfConfig":"6"},{"size":362,"mtime":1609865788231,"results":"7","hashOfConfig":"6"},{"size":682,"mtime":1609866806365,"results":"8","hashOfConfig":"6"},{"size":406,"mtime":1609866869723,"results":"9","hashOfConfig":"6"},{"filePath":"10","messages":"11","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1qss98p",{"filePath":"12","messages":"13","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"14","messages":"15","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"16","messages":"17","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/index.js",[],"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/reportWebVitals.js",[],"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/components/ErrorBoundary.jsx",[],"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/components/App/index.js",[]] \ No newline at end of file diff --git a/.github/workflows/test.workflow.yml b/.github/workflows/test.workflow.yml new file mode 100644 index 0000000..420e71f --- /dev/null +++ b/.github/workflows/test.workflow.yml @@ -0,0 +1,24 @@ +name: Node.js CI + +on: [pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [12.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run build --if-present + - run: npm test + env: + CI: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4d29575..00ec607 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ .env.development.local .env.test.local .env.production.local +.eslintcache npm-debug.log* yarn-debug.log* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..52b51e3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:12-alpine as builder + +COPY package*.json ./ +RUN npm install +COPY . . +COPY .env.template .env +RUN npm run build + +FROM nginx:alpine + +COPY --from=builder build /usr/share/nginx/html diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..f597a6d --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,81 @@ +@Library('fisker') _ + +pipeline { + agent none + options { + ansiColor('xterm') + } + environment { + PROJECT = getProject() + ENV = getEnv() + } + stages { + stage('Build') { + when { + beforeAgent true + allOf { + not { + changeRequest() + } + anyOf { + branch 'development' + branch 'main' + } + } + } + agent { + kubernetes { + cloud 'dev' + inheritFrom 'fisker' + } + } + steps { + slack("Build Started - ${env.JOB_NAME} (${env.BUILD_URL})", 'info', '#team-eng-compute-jenkins') + slack(getChanges(), 'info', '#team-eng-compute-jenkins') + container('awscli') { + ecr() + } + container('kaniko') { + buildImage() + } + } + post { + failure { + slack("${env.JOB_NAME} build failed!", 'error', '#team-eng-compute-jenkins') + } + } + } + stage('Deploy') { + when { + beforeAgent true + allOf { + not { + changeRequest() + } + anyOf { + branch 'development' + branch 'main' + } + } + } + agent { + kubernetes { + cloud getEnv() + inheritFrom 'fisker' + } + } + steps { + slack("Deploying ${PROJECT} to ${ENV}... :partydeploy: ", 'info', '#team-eng-compute-jenkins') + container('helm') { + deploy(getEnv()) + } + slack("Successfully deployed ${PROJECT} to ${ENV}! :tada: ", 'info', '#team-eng-compute-jenkins') + } + post { + failure { + slack("${PROJECT} deploy to ${ENV} failed!", 'error', '#team-eng-compute-jenkins') + } + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 0c83cde..8ee3054 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ -# Getting Started with Create React App +# Fisker OTA Admin Portal -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +Front-end web application for administarting OTA services + +# Setup + +1. Install Node 12 +2. Run `npm install` +3. Setup environment variables listed in .env.template +4. Or copy .env.template to .env +5. Edit .env with the service urls for authentication and api services ## Available Scripts @@ -39,24 +47,6 @@ Instead, it will copy all the configuration files and the transitive dependencie 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/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) - -### Analyzing the Bundle Size - -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) - -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - ### Advanced Configuration This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) diff --git a/k8s/Chart.yaml b/k8s/Chart.yaml new file mode 100644 index 0000000..6a1b177 --- /dev/null +++ b/k8s/Chart.yaml @@ -0,0 +1,2 @@ +name: ota-admin-portal +version: 1.0.0 \ No newline at end of file diff --git a/k8s/templates/deployment.yaml b/k8s/templates/deployment.yaml new file mode 100644 index 0000000..d807e32 --- /dev/null +++ b/k8s/templates/deployment.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Chart.Name }} + labels: + app: {{ .Chart.Name }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + app: {{ .Chart.Name }} + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + + template: + metadata: + labels: + app: {{ .Chart.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.registry }}/{{ .Values.image.name }}:{{ .Values.image.tag}}" + resources: + requests: + cpu: {{ .Values.resources.requests.cpu }} + memory: {{ .Values.resources.requests.memory }} + limits: + cpu: {{ .Values.resources.limits.cpu }} + memory: {{ .Values.resources.limits.memory }} + env: + # non-secret env vars + {{- range $name, $value := $.Values.env }} + {{- if not (empty $value) }} + - name: {{ $name | quote }} + value: {{ $value | quote }} + {{- end }} + {{- end }} + # Params for env vars populated from k8s secrets + {{- range $.Values.secrets }} + - name: {{ . }} + valueFrom: + secretKeyRef: + name: {{ $.Chart.Name }} + key: {{ . }} + {{- end }} \ No newline at end of file diff --git a/k8s/templates/ingress.yaml b/k8s/templates/ingress.yaml new file mode 100644 index 0000000..b1a92d8 --- /dev/null +++ b/k8s/templates/ingress.yaml @@ -0,0 +1,21 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: nginx + labels: + app: {{ .Chart.Name }} + name: {{ .Chart.Name }} +spec: + rules: + - host: {{ .Values.ingress.hostname }} + http: + paths: + - backend: + serviceName: {{ .Chart.Name }} + servicePort: 80 + path: / + tls: + - hosts: + - {{ .Values.ingress.hostname }} + secretName: fiskerdps-cert \ No newline at end of file diff --git a/k8s/templates/service.yaml b/k8s/templates/service.yaml new file mode 100644 index 0000000..4f9a2bd --- /dev/null +++ b/k8s/templates/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} +spec: + selector: + app: {{ .Chart.Name }} + ports: + - protocol: TCP + port: 80 + targetPort: 80 + type: ClusterIP \ No newline at end of file diff --git a/k8s/values-dev.yaml b/k8s/values-dev.yaml new file mode 100644 index 0000000..1e69f1c --- /dev/null +++ b/k8s/values-dev.yaml @@ -0,0 +1,12 @@ +ingress: + hostname: dev-ota-admin.fiskerdps.com + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + +replicas: 1 \ No newline at end of file diff --git a/k8s/values-prd.yaml b/k8s/values-prd.yaml new file mode 100644 index 0000000..139cf6c --- /dev/null +++ b/k8s/values-prd.yaml @@ -0,0 +1,12 @@ +ingress: + hostname: ota-admin.fiskerdps.com + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + +replicas: 1 \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..f38bf38 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,13 @@ +events { worker_connections 1024; } + +http { + server { + listen 80; + root /usr/share/nginx/html; + include /etc/nginx/mime.types; + + location / { + try_files $uri /index.html; + } + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 61ce7d9..4b4854b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1764,6 +1764,14 @@ "react-transition-group": "^4.4.0" } }, + "@material-ui/icons": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz", + "integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==", + "requires": { + "@babel/runtime": "^7.4.4" + } + }, "@material-ui/styles": { "version": "4.11.2", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz", @@ -3067,6 +3075,11 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" + }, "autoprefixer": { "version": "9.8.6", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", @@ -3096,6 +3109,14 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz", "integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==" }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -6501,6 +6522,21 @@ } } }, + "file-selector": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz", + "integrity": "sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==", + "requires": { + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -7140,6 +7176,19 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -8078,81 +8127,6 @@ "istanbul-lib-report": "^3.0.0" } }, - "jest": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.0.tgz", - "integrity": "sha512-jxTmrvuecVISvKFFhOkjsWRZV7sFqdSUAd1ajOKY+/QE/aLBVstsJ/dX8GczLzwiT6ZEwwmZqtCUHLHHQVzcfA==", - "requires": { - "@jest/core": "^26.6.0", - "import-local": "^3.0.2", - "jest-cli": "^26.6.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "jest-cli": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", - "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", - "requires": { - "@jest/core": "^26.6.3", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^26.6.3", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "prompts": "^2.0.1", - "yargs": "^15.4.1" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "jest-changed-files": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", @@ -9987,6 +9961,16 @@ "object-visit": "^1.0.0" } }, + "material-ui-dropzone": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/material-ui-dropzone/-/material-ui-dropzone-3.5.0.tgz", + "integrity": "sha512-3BC6mz/4OEM4ZpbqMfuMN065JQyqfEbifT6/VzIua7Zj4b0DaR5YPCgpN+fL/e8yBgTs9MGBZJQY06p5pfKwvw==", + "requires": { + "@babel/runtime": "^7.4.4", + "clsx": "^1.0.2", + "react-dropzone": "^10.2.1" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -10118,6 +10102,15 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" }, + "mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + } + }, "mini-css-extract-plugin": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz", @@ -12541,6 +12534,16 @@ "scheduler": "^0.20.1" } }, + "react-dropzone": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.2.tgz", + "integrity": "sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==", + "requires": { + "attr-accept": "^2.0.0", + "file-selector": "^0.1.12", + "prop-types": "^15.7.2" + } + }, "react-error-overlay": { "version": "6.0.8", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz", @@ -12556,6 +12559,52 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" }, + "react-router": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", + "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-dom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", + "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + } + }, "react-scripts": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.1.tgz", @@ -12620,6 +12669,113 @@ "webpack-dev-server": "3.11.0", "webpack-manifest-plugin": "2.2.0", "workbox-webpack-plugin": "5.1.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest": { + "version": "26.6.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.0.tgz", + "integrity": "sha512-jxTmrvuecVISvKFFhOkjsWRZV7sFqdSUAd1ajOKY+/QE/aLBVstsJ/dX8GczLzwiT6ZEwwmZqtCUHLHHQVzcfA==", + "requires": { + "@jest/core": "^26.6.0", + "import-local": "^3.0.2", + "jest-cli": "^26.6.0" + }, + "dependencies": { + "jest-cli": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", + "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", + "requires": { + "@jest/core": "^26.6.3", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^26.6.3", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "prompts": "^2.0.1", + "yargs": "^15.4.1" + } + } + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "react-shallow-renderer": { + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", + "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0" + } + }, + "react-test-renderer": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.1.tgz", + "integrity": "sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "react-is": "^17.0.1", + "react-shallow-renderer": "^16.13.1", + "scheduler": "^0.20.1" + }, + "dependencies": { + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + } } }, "react-transition-group": { @@ -13023,6 +13179,11 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -14702,6 +14863,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, "tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -15177,6 +15343,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 651a2d3..789b9ca 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,18 @@ { "name": "client", - "version": "0.1.0", + "version": "0.1.1", "private": true, "dependencies": { "@material-ui/core": "^4.11.2", + "@material-ui/icons": "^4.11.2", "@testing-library/jest-dom": "^5.11.8", "@testing-library/react": "^11.2.2", "@testing-library/user-event": "^12.6.0", + "axios": "^0.21.1", + "material-ui-dropzone": "^3.5.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-router-dom": "^5.2.0", "react-scripts": "4.0.1", "web-vitals": "^0.2.4" }, @@ -16,6 +20,7 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", + "test:debug": "react-scripts --inspect-brk test --runInBand --no-cache", "eject": "react-scripts eject" }, "eslintConfig": { @@ -35,5 +40,11 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "engines": { + "node": "12.20.1" + }, + "devDependencies": { + "react-test-renderer": "^17.0.1" } } diff --git a/public/favicon.ico b/public/favicon.ico index a11777c..bd53ce5 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index aa069f2..d3d4b18 100644 --- a/public/index.html +++ b/public/index.html @@ -10,34 +10,13 @@ content="Web site created using create-react-app" /> - - - React App + + + File Upload App
- diff --git a/public/logo-192.png b/public/logo-192.png new file mode 100644 index 0000000..89a7f7d Binary files /dev/null and b/public/logo-192.png differ diff --git a/public/logo-512.png b/public/logo-512.png new file mode 100644 index 0000000..302133b Binary files /dev/null and b/public/logo-512.png differ diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fc44b0a..0000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index a4e47a6..0000000 Binary files a/public/logo512.png and /dev/null differ diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..36225dd 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "OTA Admin Portal", + "name": "Fisker OTA Admin Portal", "icons": [ { "src": "favicon.ico", @@ -8,12 +8,12 @@ "type": "image/x-icon" }, { - "src": "logo192.png", + "src": "logo-192.png", "type": "image/png", "sizes": "192x192" }, { - "src": "logo512.png", + "src": "logo-512.png", "type": "image/png", "sizes": "512x512" } diff --git a/src/components/404/index.jsx b/src/components/404/index.jsx new file mode 100644 index 0000000..9d266a0 --- /dev/null +++ b/src/components/404/index.jsx @@ -0,0 +1,17 @@ +import { Typography } from "@material-ui/core"; +import React from "react"; +import useStyles from '../Styles'; + +const PageNotFound = () => { + const classes = useStyles(); + + return ( +
+ + Page Not Found + +
+ ); +} + +export default PageNotFound; diff --git a/src/components/App/App.css b/src/components/App/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/src/components/App/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/components/App/App.test.js b/src/components/App/App.test.js index 7892e7b..d741738 100644 --- a/src/components/App/App.test.js +++ b/src/components/App/App.test.js @@ -1,8 +1,79 @@ -import { render, screen } from '@testing-library/react'; -import App from '.'; +jest.mock("../Contexts/UserContext"); +jest.mock("../Contexts/FileUploadContext"); -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); +import { render, screen, cleanup, waitForElementToBeRemoved, waitFor } from "@testing-library/react" +import { setToken } from "../Contexts/UserContext"; +import App from "."; + +const TEST_TOKEN = { accessToken: { jwtToken: "TEST" }}; +const LOADING_STATUS = "Loading..."; + +const renderRoute = async (route) => { + window.history.pushState({}, "", route); + const { container } = render(); + if (screen.queryByText(LOADING_STATUS)) { + await waitForElementToBeRemoved(() => screen.getByText(LOADING_STATUS)); + } + return container; +}; + +describe("App", () => { + + afterEach(() => { + setToken(null); + cleanup(); + }); + + it("Route / unauthenticated", async () => { + const container = await renderRoute("/"); + expect(container.querySelector("h1").innerHTML).toEqual("Sign in"); + expect(container).toMatchSnapshot(); + }); + + it("Route /signup unauthenticated", async () => { + const container = await renderRoute("/signup"); + expect(container.querySelector("h1").innerHTML).toEqual("Sign up"); + expect(container).toMatchSnapshot(); + }); + + it("Route /home unauthenticated", async () => { + const container = await renderRoute("/home"); + expect(container.querySelector("h1").innerHTML).toEqual("Sign in"); + expect(container).toMatchSnapshot(); + }); + + it("Route / authenticated", async () => { + setToken(TEST_TOKEN); + const container = await renderRoute("/"); + expect(container.querySelector("h1").innerHTML).toEqual("Upload file"); + expect(container).toMatchSnapshot(); + }); + + it("Route /signup authenticated", async () => { + setToken(TEST_TOKEN); + const container = await renderRoute("/signup"); + expect(container.querySelector("h1").innerHTML).toEqual("Upload file"); + expect(container).toMatchSnapshot(); + }); + + it("Route /home authenticated", async () => { + setToken(TEST_TOKEN); + const container = await renderRoute("/home"); + expect(container.querySelector("h1").innerHTML).toEqual("Upload file"); + expect(container).toMatchSnapshot(); + }); + + it("Route /page-not-found unauthenticated", async () => { + const container = await renderRoute("/page-not-found"); + expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found"); + expect(container).toMatchSnapshot(); + }); + + it("Route /page-not-found authenticated", async () => { + setToken(TEST_TOKEN); + const container = await renderRoute("/page-not-found"); + expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found"); + expect(container).toMatchSnapshot(); + }); + +}) \ No newline at end of file diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap new file mode 100644 index 0000000..156f1dc --- /dev/null +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -0,0 +1,780 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`App Route / authenticated 1`] = ` +
+
+
+
+

+ Upload file +

+
+
+
+ +
+

+ Drag and drop a file here or click +

+ +
+
+
+
+
+
+ +
+
+
+
+
+
+`; + +exports[`App Route / unauthenticated 1`] = ` +
+
+
+
+

+ Sign in +

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+
+
+`; + +exports[`App Route /home authenticated 1`] = ` +
+
+
+
+

+ Upload file +

+
+
+
+ +
+

+ Drag and drop a file here or click +

+ +
+
+
+
+
+
+ +
+
+
+
+
+
+`; + +exports[`App Route /home unauthenticated 1`] = ` +
+
+
+
+

+ Sign in +

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+
+
+`; + +exports[`App Route /page-not-found authenticated 1`] = ` +
+
+
+

+ Page Not Found +

+
+
+
+`; + +exports[`App Route /page-not-found unauthenticated 1`] = ` +
+
+
+

+ Page Not Found +

+
+
+
+`; + +exports[`App Route /signup authenticated 1`] = ` +
+
+
+
+

+ Upload file +

+
+
+
+ +
+

+ Drag and drop a file here or click +

+ +
+
+
+
+
+
+ +
+
+
+
+
+
+`; + +exports[`App Route /signup unauthenticated 1`] = ` +
+
+
+
+

+ Sign up +

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+
+
+`; diff --git a/src/components/App/index.js b/src/components/App/index.js deleted file mode 100644 index 8e7c207..0000000 --- a/src/components/App/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import './App.css'; - -function App() { - return ( -
-
-

- File upload demo -

- - Learn React - -
-
- ); -} - -export default App; diff --git a/src/components/App/index.jsx b/src/components/App/index.jsx new file mode 100644 index 0000000..e914bae --- /dev/null +++ b/src/components/App/index.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { UserProvider } from '../Contexts/UserContext'; +import SiteRoutes from '../Routes/SiteRoutes'; + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/src/components/Contexts/FileUploadContext.jsx b/src/components/Contexts/FileUploadContext.jsx new file mode 100644 index 0000000..142a50a --- /dev/null +++ b/src/components/Contexts/FileUploadContext.jsx @@ -0,0 +1,62 @@ +import React, { useContext, useState } from "react"; +import { uploadFile, getCancelToken } from "../../services/uploadFile"; + +const FileUploadContext = React.createContext(); + +export const FileUploadProvider = ({ children }) => { + const [uploading, setUploading] = useState(false); + const [progress, setProgress] = useState(0); + const [status, setStatus] = useState(null); + const [cancelUpload, setCancelUpload] = useState(null); + + const done = () => { + setCancelUpload(null); + setUploading(false); + setProgress(0); + }; + + const cancel = async () => { + if (cancelUpload && progress < 100) { + cancelUpload.cancel(); + setStatus("Upload cancelled"); + } + done(); + }; + + const upload = async (files) => { + try { + if (!files || files.length === 0) throw new Error("No file provided"); + + const file = files[0].file; + const filename = file.name; + + setUploading(true); + setProgress(0); + setStatus(`Uploading ${filename}`); + setCancelUpload(getCancelToken()); + + const result = await uploadFile(file, setProgress, cancelUpload); + const url = ((result && result.url) ? result.url : "No URL available"); + setStatus(`Uploaded ${filename}\n${url}`); + setCancelUpload(null); + setProgress(100); + } + catch (e) { + setStatus(`Error occured: ${e.message}`); + } + }; + + return ( + + {children} + + ); +}; + +export const useFileUploadContext = () => useContext(FileUploadContext); diff --git a/src/components/Contexts/FileUploadContext.test.jsx b/src/components/Contexts/FileUploadContext.test.jsx new file mode 100644 index 0000000..3e8e8ef --- /dev/null +++ b/src/components/Contexts/FileUploadContext.test.jsx @@ -0,0 +1,61 @@ +jest.mock("../../services/uploadFile"); + +import {uploadFile, getCancelToken, setUploadFileResponse, setUploadFileDelay, getIssuedCancelToken } from "../../services/uploadFile" +import { FileUploadProvider, useFileUploadContext } from "../Contexts/FileUploadContext"; +import {render, cleanup, screen, fireEvent, waitFor} from "@testing-library/react" + +describe("FileUploadContext", () => { + + beforeEach(() => { + const TestComp = () => { + const { progress, uploading, status, upload, cancel } = useFileUploadContext(); + return ( + <> +
{uploading.toString()}
+
{progress.toString()}
+
{status}
+ + + + + + +`; diff --git a/src/components/FileUploadForm/index.jsx b/src/components/FileUploadForm/index.jsx new file mode 100644 index 0000000..cb97a70 --- /dev/null +++ b/src/components/FileUploadForm/index.jsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Button, Container, CssBaseline, Grid, Typography } from "@material-ui/core"; +import { DropzoneAreaBase } from "material-ui-dropzone"; +import { useUserContext } from "../Contexts/UserContext"; +import { useFileUploadContext, FileUploadProvider } from "../Contexts/FileUploadContext"; +import ModalProgressBar from "../ModalProgressBar"; +import useStyles from "../Styles"; + +const FileUploadZone = ({ classes }) => { + const { uploading, progress, status, upload, cancel } = useFileUploadContext(); + + return ( +
+ + + + ); +}; + +export default function FileUploadForm() { + const { signOut } = useUserContext(); + const classes = useStyles(); + + return ( + + +
+ + Upload file + + + + + + + + + +
+
+ ); + } \ No newline at end of file diff --git a/src/components/MessageBar.jsx b/src/components/MessageBar.jsx new file mode 100644 index 0000000..665b29f --- /dev/null +++ b/src/components/MessageBar.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Snackbar } from "@material-ui/core"; +import { useUserContext } from './Contexts/UserContext'; + +export const MessageBar = () => { + const { error, setError } = useUserContext(); + const open = (error !== null); + + return ( setError(null)}/>) +} diff --git a/src/components/ModalProgressBar/index.jsx b/src/components/ModalProgressBar/index.jsx new file mode 100644 index 0000000..3b861ea --- /dev/null +++ b/src/components/ModalProgressBar/index.jsx @@ -0,0 +1,44 @@ +import React from "react"; +import Modal from '@material-ui/core/Modal'; + +import { Button, LinearProgress } from "@material-ui/core"; + +const getModalStyle = () => { + const top = 30; + const left = 50; + + return { + width: `350px`, + top: `${top}%`, + left: `${left}%`, + transform: `translate(-${left}%, -${top}%)`, + backgroundColor: `white`, + border: `none`, + position: `absolute`, + margin: `1em`, + padding: `1em`, + textAlign: `center`, + }; +}; + +const ModalProgressBar = ({ onCancel, uploading, progress, status }) => { + const modalStyle = getModalStyle(); + const onClickCancel = () => { + if (onCancel) onCancel(); + } + + return ( + +
+ {status &&

{status}

} + + +
+
+ ); + +} + +export default ModalProgressBar; diff --git a/src/components/Routes/AuthRoute.jsx b/src/components/Routes/AuthRoute.jsx new file mode 100644 index 0000000..7775630 --- /dev/null +++ b/src/components/Routes/AuthRoute.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Redirect, Route } from 'react-router-dom'; + +export const TYPES = { + PUBLIC: 0, + GUEST: 1, + PROTECTED: 2, +}; + +export const AuthRoute = ({ token, type, ...others }) => { + if (!token && type === TYPES.PROTECTED) { + return ; + } + else if (token && type === TYPES.GUEST) { + return ; + } + return ; +} \ No newline at end of file diff --git a/src/components/Routes/ProtectedRoute.jsx b/src/components/Routes/ProtectedRoute.jsx new file mode 100644 index 0000000..e2cded1 --- /dev/null +++ b/src/components/Routes/ProtectedRoute.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Redirect, Route } from 'react-router-dom'; +import { useUserContext } from '../Contexts/UserContext'; + +export const ProtectedRoute = ({ render, ...others }) => { + const context = useUserContext(); + const { token, setError } = context; + if (!token) { + setError('Please sign in to access'); + return ; + } + return ; +} \ No newline at end of file diff --git a/src/components/Routes/SiteRoutes.jsx b/src/components/Routes/SiteRoutes.jsx new file mode 100644 index 0000000..728f33e --- /dev/null +++ b/src/components/Routes/SiteRoutes.jsx @@ -0,0 +1,34 @@ +import React, { Suspense } from 'react'; +import { + BrowserRouter, + Switch, +} from 'react-router-dom'; + +import { AuthRoute, TYPES } from '../Routes/AuthRoute' +import { MessageBar } from '../MessageBar'; +import { useUserContext } from '../Contexts/UserContext'; + +const SignInForm = React.lazy(() => import('../SignInForm')); +const SignUpForm = React.lazy(() => import('../SignUpForm')); +const FileUploadForm = React.lazy(() => import('../FileUploadForm')); +const PageNotFound = React.lazy(() => import('../404')); + +const SiteRoutes = () => { + const { token } = useUserContext(); + return ( + + + + + } type={TYPES.GUEST} token={token} /> + } type={TYPES.GUEST} token={token} /> + } type={TYPES.PROTECTED} token={token} /> + + + + + ); +}; + + +export default SiteRoutes; \ No newline at end of file diff --git a/src/components/SignInForm/SignInForm.test.jsx b/src/components/SignInForm/SignInForm.test.jsx new file mode 100644 index 0000000..45c15d3 --- /dev/null +++ b/src/components/SignInForm/SignInForm.test.jsx @@ -0,0 +1,15 @@ +jest.mock("../Contexts/UserContext"); + +import React from "react"; +import { BrowserRouter } from 'react-router-dom'; +import { render, cleanup } from "@testing-library/react" +import SignInForm from './index'; + +describe("Sign In Form", () => { + + it("Should render", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + cleanup(); + }) +}) \ No newline at end of file diff --git a/src/components/SignInForm/__snapshots__/SignInForm.test.jsx.snap b/src/components/SignInForm/__snapshots__/SignInForm.test.jsx.snap new file mode 100644 index 0000000..e99b269 --- /dev/null +++ b/src/components/SignInForm/__snapshots__/SignInForm.test.jsx.snap @@ -0,0 +1,145 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sign In Form Should render 1`] = ` +
+
+
+

+ Sign in +

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+
+`; diff --git a/src/components/SignInForm/index.jsx b/src/components/SignInForm/index.jsx index e69de29..ec5fd54 100644 --- a/src/components/SignInForm/index.jsx +++ b/src/components/SignInForm/index.jsx @@ -0,0 +1,78 @@ +import React, { useRef } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { Button, Container, CssBaseline, Grid, Link, TextField, Typography } from '@material-ui/core'; +import { useUserContext } from '../Contexts/UserContext'; +import useStyles from '../Styles'; + +export default function SignInForm() { + const classes = useStyles(); + const emailEl = useRef(null); + const passwordEl = useRef(null); + const { fetching, signIn, setError } = useUserContext(); + const onSubmit = async (event) => { + try { + event.preventDefault(); + const username = emailEl.current.value; + const password = passwordEl.current.value; + await signIn(username, password); + } + catch (e) { + setError(e.message); + } + }; + + return ( + + +
+ + Sign in + +
+ + + + + + + {"Don't have an account? Sign Up"} + + + + +
+
+ ); + } \ No newline at end of file diff --git a/src/components/SignUpForm/SignUpForm.test.jsx b/src/components/SignUpForm/SignUpForm.test.jsx new file mode 100644 index 0000000..3e69fdc --- /dev/null +++ b/src/components/SignUpForm/SignUpForm.test.jsx @@ -0,0 +1,14 @@ +jest.mock("../Contexts/UserContext"); + +import { BrowserRouter } from 'react-router-dom'; +import { render, cleanup } from "@testing-library/react" +import SignUpForm from './index'; + +describe("Sign Up Form", () => { + + it("Should render", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + cleanup(); + }) +}) \ No newline at end of file diff --git a/src/components/SignUpForm/__snapshots__/SignUpForm.test.jsx.snap b/src/components/SignUpForm/__snapshots__/SignUpForm.test.jsx.snap new file mode 100644 index 0000000..1e1b0d8 --- /dev/null +++ b/src/components/SignUpForm/__snapshots__/SignUpForm.test.jsx.snap @@ -0,0 +1,189 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sign Up Form Should render 1`] = ` +
+
+
+

+ Sign up +

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+
+`; diff --git a/src/components/SignUpForm/index.jsx b/src/components/SignUpForm/index.jsx new file mode 100644 index 0000000..1e27a75 --- /dev/null +++ b/src/components/SignUpForm/index.jsx @@ -0,0 +1,91 @@ +import React, { useRef } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { Button, Container, CssBaseline, Grid, Link, TextField, Typography } from '@material-ui/core'; +import useStyles from '../Styles'; +import { useUserContext } from '../Contexts/UserContext'; + +export default function SignInForm() { + const { signUp, signIn, fetching, setError } = useUserContext(); + const classes = useStyles(); + const emailEl = useRef(null); + const passwordEl = useRef(null); + const confirmEl = useRef(null); + const onSubmit = async (event) => { + try { + event.preventDefault(); + const email = emailEl.current.value; + const password = passwordEl.current.value; + const confirm = confirmEl.current.value; + await signUp(email, password, confirm); + await signIn(email, password); + } + catch (e) { + setError(e.message); + } + }; + + return ( + + +
+ + Sign up + +
+ + + + + + + + {"Already have an account? Sign In"} + + + + +
+
+ ); + } \ No newline at end of file diff --git a/src/components/Styles.jsx b/src/components/Styles.jsx new file mode 100644 index 0000000..d2beb00 --- /dev/null +++ b/src/components/Styles.jsx @@ -0,0 +1,23 @@ +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles((theme) => ({ + paper: { + marginTop: theme.spacing(8), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.primary.main, + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing(1), + }, + submit: { + margin: theme.spacing(3, 0, 2), + }, +})); + +export default useStyles; \ No newline at end of file diff --git a/src/components/contexts/UserContext.jsx b/src/components/contexts/UserContext.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/index.js b/src/index.js index 3e3fa7f..29ba346 100644 --- a/src/index.js +++ b/src/index.js @@ -2,14 +2,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './components/App'; -import ErrorBoundary from './components/ErrorBoundary'; +// import ErrorBoundary from './components/ErrorBoundary'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( - - - + , document.getElementById('root') ); diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 9dfc1c0..0000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/services/__mocks__/auth.js b/src/services/__mocks__/auth.js new file mode 100644 index 0000000..dd50c68 --- /dev/null +++ b/src/services/__mocks__/auth.js @@ -0,0 +1,16 @@ +let signInResponse = {}; +let signUpResponse = {}; +let verifyResponse = {}; + +const logResponse = (response) => { + return response; +}; + +export default { + signIn: async (username, password) => logResponse(signInResponse), + signUp: async (username, password) => logResponse(signUpResponse), + verify: async (accessToken) => logResponse(verifyResponse), + setSignInResponse: (value) => { signInResponse = value; }, + setSignUpResponse: (value) => { signUpResponse = value; }, + setVerifyResponse: (value) => { verifyResponse = value; }, +} \ No newline at end of file diff --git a/src/services/__mocks__/uploadFile.js b/src/services/__mocks__/uploadFile.js new file mode 100644 index 0000000..a636ec5 --- /dev/null +++ b/src/services/__mocks__/uploadFile.js @@ -0,0 +1,29 @@ +import delay from "../../utils/delay"; + +let uploadFileResponse = { url: "CLOUDFRONT_URL" }; +let uploadFileDelay = false; +let issuedCancelToken = null; + +export const getCancelToken = () => { + issuedCancelToken = { + cancel: jest.fn() + } + return issuedCancelToken; +} + +export const uploadFile = async (file, onProgress, cancelToken) => { + if (!uploadFileDelay) return uploadFileResponse; + onProgress(50); + await delay(10000); + return {}; +}; + +export const setUploadFileResponse = (value) => { + uploadFileResponse = value; +} + +export const setUploadFileDelay = (value) => { + uploadFileDelay = value; +} + +export const getIssuedCancelToken = () => issuedCancelToken; diff --git a/src/services/auth.js b/src/services/auth.js new file mode 100644 index 0000000..f3c9b09 --- /dev/null +++ b/src/services/auth.js @@ -0,0 +1,35 @@ +const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL; + +const auth = { + signIn: (username, password) => fetch(`${AUTH_URL}/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + username, + password, + }) + }).then((response) => response.json()), + + signUp: (username, password) => fetch(`${AUTH_URL}/auth/register`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + username, + password, + }) + }).then((response) => response.json()), + + verify: (accessToken) => fetch(`${AUTH_URL}/auth/verify`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ token: accessToken }) + }).then((response) => response.json()), +}; + +export default auth; diff --git a/src/services/uploadFile.js b/src/services/uploadFile.js new file mode 100644 index 0000000..07a9680 --- /dev/null +++ b/src/services/uploadFile.js @@ -0,0 +1,29 @@ +import axios from 'axios'; + +const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL; + +export const getCancelToken = () => { + const token = axios.CancelToken; + return token.source(); +} + +export const uploadFile = (file, onProgress, cancelToken) => { + const form = new FormData(); + let options = { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + cancelToken, + }; + if (onProgress) { + options = { + ...options, + onUploadProgress: (event) => { + onProgress(Math.min(99, Math.floor((event.loaded / event.total) * 100))); + } + } + } + form.append('file', file); + return axios.post(UPLOAD_ENDPOINT, form, options); +}; diff --git a/src/utils/delay.js b/src/utils/delay.js new file mode 100644 index 0000000..a57e8fd --- /dev/null +++ b/src/utils/delay.js @@ -0,0 +1,8 @@ +const delay = (duration) => new Promise((resolve) => { + const timer = setTimeout(() => { + clearTimeout(timer); + resolve(); + }, duration); +}) + +export default delay; \ No newline at end of file