diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e86fe25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# dotenv environment variables file +.env + +# gatsby files +.cache/ +public + +# Mac files +.DS_Store + +# Yarn +yarn-error.log +.pnp/ +.pnp.js +# Yarn Integrity file +.yarn-integrity diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e43a17b --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + Copyright ©2022. Element 84, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..314d9b7 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Cirrus Dashboard + +Dashboard for Cirrus processing pipeline. + +## Getting Started + +### Requirements +* node +* yarn* +* Environment files + +*If you don't have yarn installed, you should be okay running the project with npm, but keep in mind the dependencies should be updated using yarn. + +#### Environment Files +For local development, you should include an `.env.development` file with the proper configuration. + +For production builds, you should include an `.env.production` file with the proper configuration. + +``` +CIRRUS_API_ENDPOINT="[Endpoint]" +``` + +### Installing Depdencies +``` +yarn install +``` + +### Development +``` +yarn develop +``` + +### Production Builds +``` +yarn build +``` + +### Deploying cirrus-dashboard into AWS +``` +export ENVIRONMENT=Development +export AWS_REGION=us-east-1 +export AWS_DEFAULT_REGION=us-east-1 +TARGET_ENVIRONMENT=$(echo $ENVIRONMENT | tr '[:upper:]' '[:lower:]') +TARGET_BUCKET="cirrus-dashboard-${TARGET_ENVIRONMENT}" +S3_BUCKET="http://${target_bucket}.s3-website-${AWS_REGION}.amazonaws.com" + +rm -rf public +cd build-deploy && sh ./build-environment.sh $ENVIRONMENT && \ + source ./.env.production && npm run build && \ + sh ./create-bucket.sh "${TARGET_ENVIRONMENT}" && \ + echo "Syncing deployment to S3 ..." && \ + aws s3 rm s3://${TARGET_BUCKET} --recursive && \ + aws s3 cp ./public s3://${TARGET_BUCKET}/ --recursive && \ + echo "Updating metadata ..." && \ + sh ./update-metadata.sh "${TARGET_ENVIRONMENT}" && \ + echo "Done" +``` diff --git a/build-deploy/build-environment.sh b/build-deploy/build-environment.sh new file mode 100644 index 0000000..0827c97 --- /dev/null +++ b/build-deploy/build-environment.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -o pipefail + +if [[ "$#" -lt 1 ]]; then + echo "Requires environment name as a parameter" + exit 1 +fi + +ENVIRONMENT=$1 +ENVIRONMENT_CONFIG_PATH="../.env.production" +SECRET_ENV="${ENVIRONMENT}" + +shift +ARGS=$@ + +rm -f "${ENVIRONMENT_CONFIG_PATH}" +echo "Setting up .env.production configuration" + +CIRRUS_API_ENDPOINT=$(aws cloudformation $ARGS describe-stacks --stack-name cirrus-$ENVIRONMENT| jq -c -r '.Stacks[0].Outputs[] | select(.OutputKey | test("^ServiceEndpoint$")) | .OutputValue') + +if [[ "$CIRRUS_API_ENDPOINT" == "" ]]; then + echo "Unable to determine CIRRUS_API_ENDPOINT" + exit 1 +fi + +echo "Using CIRRUS_API_ENDPOINT=${CIRRUS_API_ENDPOINT}" +echo "Building .env.production at ${ENVIRONMENT_CONFIG_PATH}" +echo CIRRUS_API_ENDPOINT=${CIRRUS_API_ENDPOINT} >> "${ENVIRONMENT_CONFIG_PATH}" +cat "${ENVIRONMENT_CONFIG_PATH}" diff --git a/build-deploy/create-bucket.sh b/build-deploy/create-bucket.sh new file mode 100644 index 0000000..e2d82fb --- /dev/null +++ b/build-deploy/create-bucket.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +set -o pipefail + +if [[ "$#" -lt 1 ]]; then + echo "Requires environment name as a parameter" + exit 1 +fi + +ENVIRONMENT=$1 +shift + +ARGS=$@ + +BUCKET="cirrus-dashboard-${ENVIRONMENT}" +BUCKET_ARN="arn:aws:s3:::$BUCKET/*" +echo "Checking to see if ${BUCKET} exists" + +aws s3api head-bucket --bucket "$BUCKET" +if [[ $? -eq 0 ]]; then + echo "Bucket exists" +else + echo "Bucket does not exist" + echo "Creating ${BUCKET}" + echo "$(aws s3 mb s3://"$BUCKET" --region ${AWS_REGION})" + echo "$(aws s3 website s3://"$BUCKET" --index-document index.html --error-document index.html)" +fi + +# ensure default encryption-at-rest is enabled +echo "S3 Bucket: $BUCKET - ApplyServerSideEncryptionByDefault ..." +aws s3api put-bucket-encryption --bucket $BUCKET --server-side-encryption-configuration \ + '{"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]}' + +# If you ever want to put cloudfront in front of the s3 bucket you can set a +# s3 bucket policy to restrict access by using this version of RESTRICT_CF +#RESTRICT_CF=', "Condition": { "StringLike": { "aws:UserAgent": "*Amazon CloudFront*" }}' +RESTRICT_CF='' + +echo "Checking to see if ${BUCKET} has a policy" +aws s3api get-bucket-policy-status --bucket "$BUCKET" +if [[ $? -eq 0 ]]; then + echo "Bucket has a policy" +else + echo "Bucket policy not found, adding bucket policy" + touch policy.json + echo '{"Version": "2012-10-17","Statement": [{"Sid": "PublicReadGetObject","Effect": "Allow","Principal": "*","Action": "s3:GetObject","Resource": "'"${BUCKET_ARN}"'"'"$RESTRICT_CF"'}]}' > policy.json + cat policy.json + echo "$(aws s3api put-bucket-policy --bucket "$BUCKET" --policy file://policy.json)" +fi + +aws s3api get-bucket-website --bucket "$BUCKET" |jq -e '.ErrorDocument.Key' +if [[ $? -eq 0 ]]; then + echo "Bucket has error page" +else + echo "Updating bucket website" + echo "$(aws s3 website s3://"$BUCKET" --index-document index.html --error-document index.html)" +fi diff --git a/build-deploy/update-metadata.sh b/build-deploy/update-metadata.sh new file mode 100644 index 0000000..cbd73a8 --- /dev/null +++ b/build-deploy/update-metadata.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# +# Example usage: +# ./update-metadata.sh Development --profile my-dev --region $AWS_REGION +# + +set -o pipefail + +if [ "$#" -lt 1 ]; then + echo "Requires environment name as a parameter" + exit 1 +fi + +ENVIRONMENT=$1 +shift + +ARGS=$@ + +BUCKET="cirrus-dashboard-${ENVIRONMENT}" +echo "Checking to see if ${BUCKET} exists" + +aws s3api head-bucket --bucket "$BUCKET" $ARGS +if [ $? -eq 0 ]; then + echo "Bucket exists" +else + echo "Bucket does not exist" + break +fi + +# Caching policy recommendations per https://www.gatsbyjs.org/docs/caching/ + +cacheControlNever="public, max-age=0, must-revalidate" +cacheControlForever="public, max-age=31536000, immutable" + +# page-data.json files require the content-type to function appropriately +# due to a bug / restriction in Gatsby's page lookup engine +aws s3 cp "s3://${BUCKET}/page-data" "s3://${BUCKET}/page-data" \ + --recursive \ + --content-type "application/json" \ + --metadata-directive REPLACE --exclude "*" --include "*.json" $ARGS +if [ $? -eq 0 ]; then + echo "Updated page-data JSON files to content type application/json" +else + echo "Failed to update page-data JSON files to content type application/json" +fi + +# Page data controls Gatby's lookups and page management, it should +# not get cached ever +aws s3 cp "s3://${BUCKET}/page-data" "s3://${BUCKET}/page-data" \ + --recursive \ + --cache-control "${cacheControlNever}" \ + --metadata-directive REPLACE $ARGS +if [ $? -eq 0 ]; then + echo "Updated page-data directory metadata" +else + echo "Failed to update page-data directory metadata" +fi + +# Update all and any files in the static directory to cache forever +aws s3 cp "s3://${BUCKET}/static" "s3://${BUCKET}/static" \ + --recursive \ + --cache-control "${cacheControlForever}" \ + --metadata-directive REPLACE $ARGS +if [ $? -eq 0 ]; then + echo "Updated static directory metadata" +else + echo "Failed to update static directory metadata" +fi + +# Update all CSS and JS files to cache forever +# We specifically exclude sw.js, which is used in some +# Gatsby functionality for service workers +aws s3 cp "s3://${BUCKET}/" "s3://${BUCKET}/" \ + --recursive \ + --cache-control "${cacheControlForever}" \ + --metadata-directive REPLACE \ + --exclude "*" --include "*.css" --include "*.js" --exclude "sw.js" $ARGS +if [ $? -eq 0 ]; then + echo "Updated all CSS and JS file metadata" +else + echo "Failed to update all CSS and JS file metadata" +fi diff --git a/gatsby-config.js b/gatsby-config.js new file mode 100644 index 0000000..e83475d --- /dev/null +++ b/gatsby-config.js @@ -0,0 +1,22 @@ +module.exports = { + plugins: [ + 'gatsby-plugin-resolve-src', + 'gatsby-plugin-sass', + 'gatsby-plugin-react-helmet', + { + resolve: `gatsby-source-filesystem`, + options: { + name: `images`, + path: `${__dirname}/src/assets/images`, + }, + }, + { + resolve: 'gatsby-plugin-create-client-paths', + options: { + prefixes: [ + '/collections/*' + ] + } + } + ], +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3036388 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "cirrus-dashboard", + "description": "a dashboard for cirrus", + "license": "Apache-2.0", + "version": "0.4.0", + "author": "Element 84, Inc. ", + "dependencies": { + "@material-ui/core": "^4.10.2", + "axios": "^0.19.2", + "gatsby": "^2.23.4", + "gatsby-plugin-create-client-paths": "^2.3.4", + "gatsby-plugin-react-helmet": "^3.3.4", + "gatsby-plugin-resolve-src": "^2.1.0", + "gatsby-plugin-sass": "^2.3.4", + "gatsby-source-filesystem": "^2.3.11", + "node-sass": "^4.14.1", + "numeral": "^2.0.6", + "prop-types": "^15.7.2", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-helmet": "^6.1.0", + "react-icons": "^3.10.0" + }, + "devDependencies": {}, + "keywords": [ + "gatsby", + "sass", + "scss" + ], + "scripts": { + "build": "gatsby build", + "sonar": "sonar-scanner -Dsonar.projectVersion=$npm_package_version", + "develop": "gatsby develop", + "lint": "Get started linting your code with Eslint and stylelint", + "format": "Get started formatting your code with Eslint, Prettier, and stylelint", + "start": "yarn develop", + "serve": "gatsby serve", + "test": "echo 'Write tests! -> https://gatsby.dev/unit-testing'" + }, + "repository": { + "type": "git", + "url": "https://github.com/cirrus-geo/cirrus-dashboard" + }, + "bugs": { + "url": "https://github.com/cirrus-geo/cirrus-dashboard/issues" + } +} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..d12a2a7 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,8 @@ +# Config Parameters Can Be Found Here: +# https://docs.sonarqube.org/latest/analysis/analysis-parameters/ +sonar.host.url=https://sonar.example.com/ +sonar.projectKey=cirrus-geo:cirrus-dashboard +sonar.projectName=cirrus-dashboard + +sonar.sources=. +sonar.sourceEncoding=UTF-8 diff --git a/src/assets/images/.gitkeep b/src/assets/images/.gitkeep new file mode 100644 index 0000000..aa41166 --- /dev/null +++ b/src/assets/images/.gitkeep @@ -0,0 +1 @@ +# Remove when images exist in this directory \ No newline at end of file diff --git a/src/assets/stylesheets/application.scss b/src/assets/stylesheets/application.scss new file mode 100644 index 0000000..8177717 --- /dev/null +++ b/src/assets/stylesheets/application.scss @@ -0,0 +1,28 @@ +@import "settings/__settings"; +@import "util/__util"; +@import "components/__components"; +@import "pages/__pages"; + +* { + box-sizing: border-box; +} + +body { + font-family: Helvetica, Arial, sans-serif; + color: $color-gray9; + background-color: lighten($color-gray0, 3); + padding: 0; + margin: 0; +} + +.wrapper { + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr auto; +} + +main { + display: flex; + flex-direction: column; + align-content: center; +} \ No newline at end of file diff --git a/src/assets/stylesheets/components/__components.scss b/src/assets/stylesheets/components/__components.scss new file mode 100644 index 0000000..4824000 --- /dev/null +++ b/src/assets/stylesheets/components/__components.scss @@ -0,0 +1,9 @@ +@import "breadcrumbs"; +@import "collection"; +@import "container"; +@import "card"; +@import "dashboard"; +@import "dashboard-controls"; +@import "data-table"; +@import "footer"; +@import "header"; \ No newline at end of file diff --git a/src/assets/stylesheets/components/_breadcrumbs.scss b/src/assets/stylesheets/components/_breadcrumbs.scss new file mode 100644 index 0000000..53659d4 --- /dev/null +++ b/src/assets/stylesheets/components/_breadcrumbs.scss @@ -0,0 +1,28 @@ +.breadcrumbs { + + list-style: none; + padding: 0; + margin: 0; + + &, + li, + a { + display: flex; + align-items: center; + } + + svg { + + margin: 0 .4em; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + + } + +} \ No newline at end of file diff --git a/src/assets/stylesheets/components/_card.scss b/src/assets/stylesheets/components/_card.scss new file mode 100644 index 0000000..a0223ef --- /dev/null +++ b/src/assets/stylesheets/components/_card.scss @@ -0,0 +1,23 @@ +.card { + height: 25vh; + width: 25vw; + border: black solid .2em; + margin-bottom: 1vh; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + a { + margin-top: 10vh; + } + + transform: translateY(0px); + transition: .5s; + + &:hover { + transform: translateY(-10px); + transition: .5s; + } +} \ No newline at end of file diff --git a/src/assets/stylesheets/components/_collection.scss b/src/assets/stylesheets/components/_collection.scss new file mode 100644 index 0000000..2bb7d40 --- /dev/null +++ b/src/assets/stylesheets/components/_collection.scss @@ -0,0 +1,114 @@ +.collection { + + width: 100%; + margin: 2em auto 2em; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + +} + +.collection-title { + + text-align: left; + + &, + a { + color: $color-gray9; + } + + a { + text-decoration: none; + } + +} + +.collection-dashboard { + background-color: white; +} + +.collection-meta { + + display: flex; + justify-content: flex-end; + list-style: none; + padding: .6em 1em; + border-top: solid 1px $color-gray0; + margin: 0; + + li { + margin: 0; + } + + p { + color: $color-gray5; + font-size: .9em; + font-style: italic; + margin: 0; + } + +} + +.collection-cells { + display: flex; + flex-direction: row; + flex-wrap: wrap; + list-style: none; + text-align: center; + padding: 0; + margin: 0; +} + +.collection-cell { + flex-shrink: 0; + flex-grow: 1; + padding: 2em; + margin: 1px; +} + +.collection-cell-header { + display: flex; + align-items: center; + justify-content: center; + height: 2em; + font-size: 1.2vw; + margin: 0 0 1em; +} + +.collection-cell-value { + font-size: 3vw; + font-weight: bold; + margin: 0; +} + +.collection-not-active { + + padding: 2em 0; + + p { + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + + } + +} + +.collection-is-loading { + + .collection-cell-header, + .collection-cell-value { + @extend .loading; + } + +} \ No newline at end of file diff --git a/src/assets/stylesheets/components/_container.scss b/src/assets/stylesheets/components/_container.scss new file mode 100644 index 0000000..1e338db --- /dev/null +++ b/src/assets/stylesheets/components/_container.scss @@ -0,0 +1,5 @@ +.container { + width: 100%; + max-width: 50em; + margin: 0 auto; +} \ No newline at end of file diff --git a/src/assets/stylesheets/components/_dashboard-controls.scss b/src/assets/stylesheets/components/_dashboard-controls.scss new file mode 100644 index 0000000..7213056 --- /dev/null +++ b/src/assets/stylesheets/components/_dashboard-controls.scss @@ -0,0 +1,45 @@ +.dashboard-controls { + + display: flex; + justify-content: flex-end; + margin-bottom: 2em; + + ul { + display: flex; + list-style: none; + padding: 0; + margin: 0; + } + + label { + display: inline-block; + margin-right: .5em; + } + + select { + font-size: 1em; + } + + .MuiSelect-select { + padding-left: 8px; + } + +} + +.dashboard-controls-control { + + margin: 0 1.4em; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + + select { + margin-left: 1em; + } + +} \ No newline at end of file diff --git a/src/assets/stylesheets/components/_dashboard.scss b/src/assets/stylesheets/components/_dashboard.scss new file mode 100644 index 0000000..5187985 --- /dev/null +++ b/src/assets/stylesheets/components/_dashboard.scss @@ -0,0 +1,3 @@ +.dashboard { + padding: 2em; +} \ No newline at end of file diff --git a/src/assets/stylesheets/components/_data-table.scss b/src/assets/stylesheets/components/_data-table.scss new file mode 100644 index 0000000..b616442 --- /dev/null +++ b/src/assets/stylesheets/components/_data-table.scss @@ -0,0 +1,22 @@ +.data-table { + width: 100%; + margin: 0 auto; +} + +.data-table-row-headers { + + &, + .MuiTableRow-head { + background: $color-gray7; + } + +} + +.data-table-row-header { + + &, + &.MuiTableCell-head { + color: $color-gray0; + } + +} \ No newline at end of file diff --git a/src/assets/stylesheets/components/_footer.scss b/src/assets/stylesheets/components/_footer.scss new file mode 100644 index 0000000..66fb5f9 --- /dev/null +++ b/src/assets/stylesheets/components/_footer.scss @@ -0,0 +1,15 @@ +footer { + + width: 100%; + height: 2em; + line-height: 2em; + color: white; + text-align: center; + background-color: $color-gray7; + + p { + font-size: 1em; + margin: 0; + } + +} \ No newline at end of file diff --git a/src/assets/stylesheets/components/_header.scss b/src/assets/stylesheets/components/_header.scss new file mode 100644 index 0000000..4064770 --- /dev/null +++ b/src/assets/stylesheets/components/_header.scss @@ -0,0 +1,38 @@ +header { + + width: 100%; + color: white; + background-color: black; + + .container { + display: flex; + justify-content: center; + height: 4em; + line-height: 4em; + max-width: 100em; + } + + ul { + list-style: none; + margin: 0; + padding: 0; + } + + li { + display: inline-block; + } + + a { + display: block; + color: white; + text-decoration: none; + padding: 0 .5em; + } + + p { + font-size: 2em; + font-weight: bold; + margin: 0; + } + +} \ No newline at end of file diff --git a/src/assets/stylesheets/pages/__pages.scss b/src/assets/stylesheets/pages/__pages.scss new file mode 100644 index 0000000..44873eb --- /dev/null +++ b/src/assets/stylesheets/pages/__pages.scss @@ -0,0 +1,2 @@ +@import "collections-details"; +@import "home"; \ No newline at end of file diff --git a/src/assets/stylesheets/pages/_collections-details.scss b/src/assets/stylesheets/pages/_collections-details.scss new file mode 100644 index 0000000..14288ab --- /dev/null +++ b/src/assets/stylesheets/pages/_collections-details.scss @@ -0,0 +1,23 @@ +.page-collections-details { + + table { + width: 100%; + background-color: white; + } + +} + +.collections-details-header { + + margin-top: 2em; + + h1 { + margin: 0; + } + + p { + color: $color-gray6; + font-weight: bold; + } + +} \ No newline at end of file diff --git a/src/assets/stylesheets/pages/_home.scss b/src/assets/stylesheets/pages/_home.scss new file mode 100644 index 0000000..3707b26 --- /dev/null +++ b/src/assets/stylesheets/pages/_home.scss @@ -0,0 +1,12 @@ +.page-home { + + h1 { + color: black; + } + + .container { + display: flex; + justify-content: space-around; + } + +} \ No newline at end of file diff --git a/src/assets/stylesheets/settings/__settings.scss b/src/assets/stylesheets/settings/__settings.scss new file mode 100644 index 0000000..e8b0203 --- /dev/null +++ b/src/assets/stylesheets/settings/__settings.scss @@ -0,0 +1,3 @@ +@import "functions/__functions"; + +@import "colors"; \ No newline at end of file diff --git a/src/assets/stylesheets/settings/_colors.scss b/src/assets/stylesheets/settings/_colors.scss new file mode 100644 index 0000000..a7d92b7 --- /dev/null +++ b/src/assets/stylesheets/settings/_colors.scss @@ -0,0 +1,230 @@ +/** + * Color Map + * @description Colors from https://www.materialui.co/colors + */ + +$color-map: ( + + white: white, + black: black, + + red0: #ffebee, + red1: #ffcdd2, + red2: #ef9a9a, + red3: #e57373, + red4: #ef5350, + red5: #f44336, + red6: #e53935, + red7: #d32f2f, + red8: #c62828, + red9: #b71c1c, + red: red5, + + orange0: #FFF3E0, + orange1: #FFE0B2, + orange2: #FFCC80, + orange3: #FFB74D, + orange4: #FF5722, + orange5: #FF9800, + orange6: #FB8C00, + orange7: #F57C00, + orange8: #EF6C00, + orange9: #E65100, + orange: orange5, + + yellow0: #FFFDE7, + yellow1: #FFF9C4, + yellow2: #FFF59D, + yellow3: #FFF176, + yellow4: #FFEE58, + yellow5: #FFEB3B, + yellow6: #FDD835, + yellow7: #FBC02D, + yellow8: #F9A825, + yellow9: #F57F17, + yellow: yellow5, + + green0: #E8F5E9, + green1: #C8E6C9, + green2: #A5D6A7, + green3: #81C784, + green4: #66BB6A, + green5: #4CAF50, + green6: #43A047, + green7: #388E3C, + green8: #2E7D32, + green9: #1B5E20, + green: green5, + + blue0: #E3F2FD, + blue1: #BBDEFB, + blue2: #90CAF9, + blue3: #64B5F6, + blue4: #42A5F5, + blue5: #2196F3, + blue6: #1E88E5, + blue7: #1976D2, + blue8: #1565C0, + blue9: #0D47A1, + blue: blue5, + + purple0: #ede7f7, + purple1: #D1C4E9, + purple2: #B39DDB, + purple3: #9575CD, + purple4: #7E57C2, + purple5: #673AB7, + purple6: #5E35B1, + purple7: #512DA8, + purple8: #4527A0, + purple9: #311B92, + purple: purple5, + + gray0: #ECEFF1, + gray1: #CFD8DC, + gray2: #B0BEC5, + gray3: #90A4AE, + gray4: #78909C, + gray5: #607D8B, + gray6: #546E7A, + gray7: #455A64, + gray8: #37474F, + gray9: #263238, + gray: gray5, + + alert: red7, + alert_text: red9, + alert_background: red1, + + warning: yellow, + warning_text: yellow9, + warning_background: yellow0, + + error: red, + error_text: red8, + error_background: red0, + + success: green, + success_text: green8, + success_background: green0, + + info: gray, + info_text: gray8, + info_background: gray0, + +); + + +// Define the colors in scss accessible values + +$color-red0: colorByKey(red0); +$color-red1: colorByKey(red1); +$color-red2: colorByKey(red2); +$color-red3: colorByKey(red3); +$color-red4: colorByKey(red4); +$color-red5: colorByKey(red5); +$color-red6: colorByKey(red6); +$color-red7: colorByKey(red7); +$color-red8: colorByKey(red8); +$color-red9: colorByKey(red9); +$color-red: colorByKey(red); + +$color-orange0: colorByKey(orange0); +$color-orange1: colorByKey(orange1); +$color-orange2: colorByKey(orange2); +$color-orange3: colorByKey(orange3); +$color-orange4: colorByKey(orange4); +$color-orange5: colorByKey(orange5); +$color-orange6: colorByKey(orange6); +$color-orange7: colorByKey(orange7); +$color-orange8: colorByKey(orange8); +$color-orange9: colorByKey(orange9); +$color-orange: colorByKey(orange); + +$color-yellow0: colorByKey(yellow0); +$color-yellow1: colorByKey(yellow1); +$color-yellow2: colorByKey(yellow2); +$color-yellow3: colorByKey(yellow3); +$color-yellow4: colorByKey(yellow4); +$color-yellow5: colorByKey(yellow5); +$color-yellow6: colorByKey(yellow6); +$color-yellow7: colorByKey(yellow7); +$color-yellow8: colorByKey(yellow8); +$color-yellow9: colorByKey(yellow9); +$color-yellow: colorByKey(yellow); + +$color-green0: colorByKey(green0); +$color-green1: colorByKey(green1); +$color-green2: colorByKey(green2); +$color-green3: colorByKey(green3); +$color-green4: colorByKey(green4); +$color-green5: colorByKey(green5); +$color-green6: colorByKey(green6); +$color-green7: colorByKey(green7); +$color-green8: colorByKey(green8); +$color-green9: colorByKey(green9); +$color-green: colorByKey(green); + +$color-blue0: colorByKey(blue0); +$color-blue1: colorByKey(blue1); +$color-blue2: colorByKey(blue2); +$color-blue3: colorByKey(blue3); +$color-blue4: colorByKey(blue4); +$color-blue5: colorByKey(blue5); +$color-blue6: colorByKey(blue6); +$color-blue7: colorByKey(blue7); +$color-blue8: colorByKey(blue8); +$color-blue9: colorByKey(blue9); +$color-blue: colorByKey(blue); + +$color-purple0: colorByKey(purple0); +$color-purple1: colorByKey(purple1); +$color-purple2: colorByKey(purple2); +$color-purple3: colorByKey(purple3); +$color-purple4: colorByKey(purple4); +$color-purple5: colorByKey(purple5); +$color-purple6: colorByKey(purple6); +$color-purple7: colorByKey(purple7); +$color-purple8: colorByKey(purple8); +$color-purple9: colorByKey(purple9); +$color-purple: colorByKey(purple); + +$color-gray0: colorByKey(gray0); +$color-gray1: colorByKey(gray1); +$color-gray2: colorByKey(gray2); +$color-gray3: colorByKey(gray3); +$color-gray4: colorByKey(gray4); +$color-gray5: colorByKey(gray5); +$color-gray6: colorByKey(gray6); +$color-gray7: colorByKey(gray7); +$color-gray8: colorByKey(gray8); +$color-gray9: colorByKey(gray9); +$color-gray: colorByKey(gray); + +$color-alert: colorByKey(alert); +$color-alert-text: colorByKey(alert_text); +$color-alert-background: colorByKey(alert_background); + +$color-warning: colorByKey(warning); +$color-warning-text: colorByKey(warning_text); +$color-warning-background: colorByKey(warning_background); + +$color-error: colorByKey(error); +$color-error-text: colorByKey(error_text); +$color-error-background: colorByKey(error_background); + +$color-success: colorByKey(success); +$color-success-text: colorByKey(success_text); +$color-success-background: colorByKey(success_background); + +$color-info: colorByKey(info); +$color-info-text: colorByKey(info_text); +$color-info-background: colorByKey(info_background); + + +// Brand colors + +$color-facebook: #3b5998; +$color-twitter: #1da1f2; +$color-googleplus: #dd4b39; +$color-pinterest: #bd081c; \ No newline at end of file diff --git a/src/assets/stylesheets/settings/functions/__functions.scss b/src/assets/stylesheets/settings/functions/__functions.scss new file mode 100644 index 0000000..004edd8 --- /dev/null +++ b/src/assets/stylesheets/settings/functions/__functions.scss @@ -0,0 +1 @@ +@import "color-by-key"; \ No newline at end of file diff --git a/src/assets/stylesheets/settings/functions/_color-by-key.scss b/src/assets/stylesheets/settings/functions/_color-by-key.scss new file mode 100644 index 0000000..fe0004a --- /dev/null +++ b/src/assets/stylesheets/settings/functions/_color-by-key.scss @@ -0,0 +1,21 @@ +@function colorByKey($key) { + + @if map-has-key( $color-map, $key ) { + + @if map-has-key( $color-map, map-get( $color-map, $key ) ) { + + @if map-has-key( $color-map, map-get( $color-map, map-get( $color-map, $key ) ) ) { + @return map-get( $color-map, map-get( $color-map, map-get( $color-map, $key ) ) ); + } @else { + @return map-get( $color-map, map-get($color-map, $key) ); + } + + } @else { + @return map-get( $color-map, $key ); + } + + } @else { + @return $key; + } + +} diff --git a/src/assets/stylesheets/util/__util.scss b/src/assets/stylesheets/util/__util.scss new file mode 100644 index 0000000..7c8cff3 --- /dev/null +++ b/src/assets/stylesheets/util/__util.scss @@ -0,0 +1,2 @@ +@import "loading"; +@import "typography"; \ No newline at end of file diff --git a/src/assets/stylesheets/util/_loading.scss b/src/assets/stylesheets/util/_loading.scss new file mode 100644 index 0000000..5f7100b --- /dev/null +++ b/src/assets/stylesheets/util/_loading.scss @@ -0,0 +1,60 @@ +.loading { + + &, + svg { + color: transparent; + } + + background: linear-gradient(90deg, $color-gray0 30%, lighten($color-gray0, 3) 50%, $color-gray0 70%); + background-size: 400%; + animation: loading 1.2s ease-in-out infinite; + +} + +.loading-0 { + + &, + svg { + color: transparent; + } + + background: linear-gradient(90deg, $color-gray0 30%, lighten($color-gray0, 3) 50%, $color-gray0 70%); + background-size: 400%; + animation: loading 1.2s ease-in-out infinite; + +} + +.loading-1 { + + &, + svg { + color: transparent; + } + + background: linear-gradient(90deg, $color-gray1 30%, lighten($color-gray1, 5) 50%, $color-gray1 70%); + background-size: 400%; + animation: loading 1.2s ease-in-out infinite; + +} + +.loading-2 { + + &, + svg { + color: transparent; + } + + background: linear-gradient(90deg, $color-gray2 30%, lighten($color-gray2, 5) 50%, $color-gray2 70%); + background-size: 400%; + animation: loading 1.2s ease-in-out infinite; + +} + +@keyframes loading { + 0% { + background-position: 100% 50%; + } + 100% { + background-position: 0 50%; + } +} diff --git a/src/assets/stylesheets/util/_typography.scss b/src/assets/stylesheets/util/_typography.scss new file mode 100644 index 0000000..ef96e1a --- /dev/null +++ b/src/assets/stylesheets/util/_typography.scss @@ -0,0 +1,3 @@ +.text-center { + text-align: center; +} \ No newline at end of file diff --git a/src/components/Breadcrumbs.js b/src/components/Breadcrumbs.js new file mode 100644 index 0000000..4dbca18 --- /dev/null +++ b/src/components/Breadcrumbs.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { Link } from 'gatsby'; + +const Breadcrumbs = ({ crumbs }) => { + const hasCrumbs = Array.isArray(crumbs) && crumbs.length > 0; + + if ( !hasCrumbs ) return null; + + return ( + + ) +} + +export default Breadcrumbs; \ No newline at end of file diff --git a/src/components/Card.js b/src/components/Card.js new file mode 100644 index 0000000..9c0b5ce --- /dev/null +++ b/src/components/Card.js @@ -0,0 +1,18 @@ +import React from 'react' +import PropTypes from 'prop-types' + +// TODO: Create a way to take in as many lines as needed +const Card = ({className, header, children}) => ( +
+

{header}

+

{children}

+
+) + +Card.propTypes = { + className: PropTypes.string, + header: PropTypes.string, + children: PropTypes.string, +} + +export default Card \ No newline at end of file diff --git a/src/components/Collection.js b/src/components/Collection.js new file mode 100644 index 0000000..d8f597f --- /dev/null +++ b/src/components/Collection.js @@ -0,0 +1,111 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Link } from 'gatsby'; +import numeral from 'numeral'; + +import { useFetchCollection } from 'hooks'; + +import { COLLECTION_STATES } from 'data/collections'; + +// This creates a block of cells that allow us to fill the content +// to create a loading state + +const loadingCells = [...new Array(COLLECTION_STATES.length)].map((item, i) => { + return { + id: `loading-${i}`, + label: 'Loading', + value: 0 + } +}); + +const ContentNotActive = () => ( +
+

+ Data Not Available +

+
+); + +const Collection = ({ collection, since, type }) => { + let collectionClass = 'collection'; + + const { title, href } = collection; + + const { data = {}, state = {} } = useFetchCollection({ + href, + since, + type + }); + + const { counts, collections, workflow } = data; + const { loading = true } = state; + + let cells; + + if ( loading ) { + cells = loadingCells; + } else { + cells = Object.keys(counts).map(key => { + const { id, label, count } = counts[key]; + return { + id, + label, + value: count + } + }); + } + + const hasCells = Array.isArray(cells) && cells.length > 0; + + if ( loading ) { + collectionClass = `${collectionClass} collection-is-loading`; + } + + return ( +
+

+ + { title } + +

+
+ { !hasCells && } + { hasCells && ( +
    + { cells.map(({ id, label, value } = {}, i) => { + let number = value; + + if ( value > 999 ) { + number = numeral(value).format('0,0'); + } + + return ( +
  • +

    + { label } +

    +

    + { number } +

    +
  • + ); + }) } +
+ )} +
    +
  • +

    + Results from {since && since !== 'all' ? `the last ${since}` : 'all time' } +

    +
  • +
+
+
+ ); +} + +Collection.propTypes = { + cells: PropTypes.array +} + +export default Collection \ No newline at end of file diff --git a/src/components/Container.js b/src/components/Container.js new file mode 100644 index 0000000..5f73643 --- /dev/null +++ b/src/components/Container.js @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Container = ({children}) => { + return ( +
+ { children } +
+ ); +}; + +Container.propTypes = { + children: PropTypes.node, +}; + +export default Container; \ No newline at end of file diff --git a/src/components/Dashboard.js b/src/components/Dashboard.js new file mode 100644 index 0000000..0a333b8 --- /dev/null +++ b/src/components/Dashboard.js @@ -0,0 +1,11 @@ +import React from 'react'; + +const Dashboard = ({ children }) => { + return ( +
+ { children } +
+ ) +} + +export default Dashboard; \ No newline at end of file diff --git a/src/components/DashboardControls.js b/src/components/DashboardControls.js new file mode 100644 index 0000000..03c3670 --- /dev/null +++ b/src/components/DashboardControls.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { Select, MenuItem, InputLabel } from '@material-ui/core'; + + +import { DASHBOARD_FILTERS_SINCE, DASHBOARD_DEFAULT_FILTERS } from 'data/dashboard'; + +const DashboardControls = ({ additionalControls = [], filters = DASHBOARD_DEFAULT_FILTERS, onFilter }) => { + const { since } = filters; + + /** + * handleOnSinceChange + */ + + function handleOnSinceChange(event) { + const { target = {} } = event; + const value = target.value; + + if ( typeof onFilter === 'function' ) { + onFilter({ + filters: { + since: value + }, + event + }); + } + } + + return ( +
+ +
+ ); +} + +export default DashboardControls; \ No newline at end of file diff --git a/src/components/DataTable.js b/src/components/DataTable.js new file mode 100644 index 0000000..fe4eef1 --- /dev/null +++ b/src/components/DataTable.js @@ -0,0 +1,51 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow +} from '@material-ui/core' + +// Table being made based off of https://material-ui.com/components/tables/ + +const DataTable = ({ rows = [], columns = [] }) => { + return ( + + + + + { columns.map(column => { + const { key, Header } = column; + return ( + { Header } + ) + })} + + + + {rows.map((row, rowIndex) => { + return ( + + {row.map((cell, cellIndex) => { + return ( + { cell } + ); + })} + + ); + })} + +
+
+ ); +} + +DataTable.propTypes = { + rows: PropTypes.array +} + +export default DataTable diff --git a/src/components/Footer.js b/src/components/Footer.js new file mode 100644 index 0000000..5848b0a --- /dev/null +++ b/src/components/Footer.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import Container from 'components/Container'; + +const Footer = () => { + return ( + + ); +}; + +export default Footer; \ No newline at end of file diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 0000000..d415f69 --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { Link } from 'gatsby'; + +import Container from 'components/Container'; + +const Header = () => { + return ( +
+ +

+ Cirrus +

+
+
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/src/components/Layout.js b/src/components/Layout.js new file mode 100644 index 0000000..2b03e9a --- /dev/null +++ b/src/components/Layout.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Helmet from 'react-helmet'; + +import 'assets/stylesheets/application.scss'; + +import Header from 'components/Header'; +import Footer from 'components/Footer'; + +const Layout = ({ children, pageName }) => { + + let className = ''; + + if ( pageName ) { + className = `${className} page-${pageName}`; + } + + return ( + <> + + Cirrus + +
+
+
{ children }
+
+
+ + ); + +}; + +Layout.propTypes = { + children: PropTypes.node.isRequired, +} + +export default Layout; \ No newline at end of file diff --git a/src/data/collections.js b/src/data/collections.js new file mode 100644 index 0000000..19f637c --- /dev/null +++ b/src/data/collections.js @@ -0,0 +1,22 @@ +export const COLLECTION_STATES = [ + { + id: 'COMPLETED', + label: 'Completed' + }, + { + id: 'FAILED', + label: 'Failed' + }, + { + id: 'INVALID', + label: 'Invalid' + }, + { + id: 'PROCESSING', + label: 'Processing' + }, + { + id: 'ABORTED', + label: 'Aborted' + } +]; diff --git a/src/data/dashboard.js b/src/data/dashboard.js new file mode 100644 index 0000000..f34f958 --- /dev/null +++ b/src/data/dashboard.js @@ -0,0 +1,46 @@ +import { COLLECTION_STATES } from 'data/collections'; + +export const DASHBOARD_FILTERS_SINCE = [ + { + label: 'All Time', + value: 'all', + isDefault: true + }, + { + label: 'Last Hour', + value: '1h' + }, + { + label: 'Last Day', + value: '1d' + }, + { + label: 'Last Week', + value: '7d' + }, + { + label: 'Last Month', + value: '30d' + }, + { + label: 'Last Year', + value: '365d' + } +]; + +export const DASHBOARD_FILTERS_SINCE_DEFAULT = DASHBOARD_FILTERS_SINCE.find(({ isDefault }) => isDefault); + +export const DASHBOARD_DEFAULT_FILTERS = { + since: DASHBOARD_FILTERS_SINCE_DEFAULT?.value +} + +export const DASHBOARD_COLLECTION_STATES = [ + { + id: 'ALL', + label: 'All', + isDefault: true + }, + ...COLLECTION_STATES +]; + +export const DASHBOARD_COLLECTION_STATES_DEFAULT = DASHBOARD_COLLECTION_STATES.find(({ isDefault }) => isDefault); \ No newline at end of file diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..89dec6a --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,4 @@ +export { default as useFetchCollection } from './useFetchCollection'; +export { default as useFetchCollections } from './useFetchCollections'; +export { default as useFetchCollectionItems } from './useFetchCollectionItems'; +export { default as useRequest } from './useRequest'; \ No newline at end of file diff --git a/src/hooks/useFetchCollection.js b/src/hooks/useFetchCollection.js new file mode 100644 index 0000000..7d383e1 --- /dev/null +++ b/src/hooks/useFetchCollection.js @@ -0,0 +1,49 @@ +import { useRequest } from 'hooks'; + +import { COLLECTION_STATES } from 'data/collections'; + +const errorBase = 'Failed to fetch collection'; + +export default function useFetchCollection({ href, since = 'all' }) { + const params = []; + let url; + + if ( href ) { + url = href; + } else { + throw new Error(`${errorBase}: Unknown collection origin`); + } + + if ( since !== 'all' ) { + params.push(`since=${since}`); + } + + if ( params.length > 0 ) { + url = `${url}?${params.join('&')}`; + } + + const { state, data = {} } = useRequest({ + url + }); + + const { counts = {} } = data; + + const collectionData = { + ...data, + counts: {} + }; + + Object.keys(counts).forEach(key => { + const mapping = COLLECTION_STATES.find(({ id } = {}) => id === key); + + collectionData.counts[key] = { + ...mapping, + count: counts[key] + } + }); + + return { + state, + data: collectionData + } +} diff --git a/src/hooks/useFetchCollectionItems.js b/src/hooks/useFetchCollectionItems.js new file mode 100644 index 0000000..f1da56f --- /dev/null +++ b/src/hooks/useFetchCollectionItems.js @@ -0,0 +1,130 @@ +import { useEffect, useState } from 'react'; + +import { useRequest } from 'hooks'; +import Item from 'models/item'; + +import { DASHBOARD_COLLECTION_STATES_DEFAULT, DASHBOARD_FILTERS_SINCE_DEFAULT } from 'data/dashboard'; + +const DEFAULT_PARAMS = { + since: undefined, + state: undefined, + nextkey: undefined +} + +export default function useFetchCollectionItems({ + collectionsId, + workflowId, + since = DASHBOARD_FILTERS_SINCE_DEFAULT.value, + state = DASHBOARD_COLLECTION_STATES_DEFAULT.value +}) { + const [params, updateParams] = useState({ + ...DEFAULT_PARAMS, + since, + state, + }); + + // Construct the endpoint we'll use for requests + + const paramsToAdd = []; + let url = `${process.env.CIRRUS_API_ENDPOINT}/${collectionsId}/workflow-${workflowId}/items`; + + if ( params?.nextkey ) { + paramsToAdd.push(`nextkey=${params.nextkey}`); + } + + if ( params?.since !== 'all' ) { + paramsToAdd.push(`since=${params.since}`); + } + + if ( params?.state !== 'ALL' ) { + paramsToAdd.push(`state=${params.state}`); + } + + if ( paramsToAdd.length > 0 ) { + url = `${url}?${paramsToAdd.join('&')}`; + } + + // Make the request + + const { state: requestState, data: requestData = {} } = useRequest({ + url + }); + + const { nextkey: requestNextKey, items: requestItems } = requestData; + const hasRequestItems = Array.isArray(requestItems) && requestItems.length > 0; + + // Store and collect the data from the request + + const [collectionData, updateCollectionData] = useState({}); + + let { items } = collectionData || {}; + + items = items && items.map(item => new Item(item)); + + // If any of the configurable params change, we need to refresh our state so that + // we're not using stale data + + useEffect(() => { + updateCollectionData({}); + updateParams({ + ...DEFAULT_PARAMS, + since, + state + }); + }, [since, state]); + + // If our nextkey or items change at any time, we want to update our state to include + // those new items + + useEffect(() => { + updateCollectionData((prev = {}) => { + const isNewKey = requestNextKey !== prev.nextkey; + let newItems = [ + ...(prev.items || []) + ]; + + // If we don't have the same key, that means we have a new set of data. Append + // any new items to our item state + + if ( isNewKey && hasRequestItems ) { + newItems = [ + ...newItems, + ...requestItems + ] + } else if ( !requestNextKey && hasRequestItems ) { + newItems = [ + ...requestItems + ] + } + + return { + ...prev, + nextkey: requestNextKey, + items: newItems + } + }); + }, [requestNextKey, requestItems, hasRequestItems]) + + /** + * handleLoadMore + * @description Updates the params to use a nextkey to fetch the next page + */ + + function handleLoadMore() { + updateParams(prev => { + return { + ...prev, + nextkey: requestData?.nextkey + } + }) + } + + return { + state: requestState, + data: { + ...requestData, + items + }, + loadMore: requestData?.nextkey && handleLoadMore + } +} diff --git a/src/hooks/useFetchCollections.js b/src/hooks/useFetchCollections.js new file mode 100644 index 0000000..f01390c --- /dev/null +++ b/src/hooks/useFetchCollections.js @@ -0,0 +1,16 @@ +import { useRequest } from 'hooks'; + +const CIRRUS_ENDPOINT = process.env.CIRRUS_API_ENDPOINT; + +export default function useFetchCollections() { + const { state, data = {} } = useRequest({ + url: CIRRUS_ENDPOINT + }); + + const collections = data?.links?.filter(({ rel }) => rel === 'child'); + + return { + state, + collections + } +} diff --git a/src/hooks/useRequest.js b/src/hooks/useRequest.js new file mode 100644 index 0000000..b382766 --- /dev/null +++ b/src/hooks/useRequest.js @@ -0,0 +1,66 @@ +import { useCallback, useState, useEffect } from 'react'; +import axios from 'axios'; + +const defaultState = { + loading: false, + error: false +} + +const defaultMethod = 'get'; + +export default function useRequest({ url, method = defaultMethod }) { + const [response, updateResponse] = useState(); + const [requestState, updateRequestState] = useState(defaultState); + + async function request(options) { + let response; + + updateRequestState(prev => { + return { + ...prev, + loading: true + } + }); + + try { + response = await axios(options); + } catch(e) { + updateRequestState(prev => { + return { + ...prev, + loading: false, + error: true + } + }); + throw e; + } + + updateRequestState(prev => { + return { + ...prev, + loading: false + } + }); + + return response; + } + + const memoizedRequest = useCallback(async () => { + const response = await request({ + url, + method + }); + updateResponse(response); + }, [url, method]); + + useEffect(() => { + memoizedRequest(); + }, [memoizedRequest]); + + return { + response, + state: requestState, + data: response?.data, + request: memoizedRequest + } +} \ No newline at end of file diff --git a/src/lib/datetime.js b/src/lib/datetime.js new file mode 100644 index 0000000..f5efab9 --- /dev/null +++ b/src/lib/datetime.js @@ -0,0 +1,15 @@ +/** + * friendlyDate + * @description Takes in a date value and returns a friendly version + */ + +export function friendlyDate(value) { + const date = new Date(value); + return new Intl.DateTimeFormat('en', { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: 'numeric', + minute: 'numeric' + }).format(date); +} \ No newline at end of file diff --git a/src/models/item.js b/src/models/item.js new file mode 100644 index 0000000..3b4690a --- /dev/null +++ b/src/models/item.js @@ -0,0 +1,31 @@ +import { friendlyDate } from 'lib/datetime'; + +class Item { + constructor(data) { + this.catid = data.catid; + this.created = data.created; + this.updated = data.updated; + this.state = data.state; + this.executions = data.executions; + } + + get itemId() { + const split = this.catid && this.catid.split('/'); + return split[split.length - 1]; + } + + get createdFriendly() { + return friendlyDate(this.created); + } + + get updatedFriendly() { + return friendlyDate(this.updated); + } + + get lastExecution() { + return Array.isArray(this.executions) && this.executions[this.executions.length - 1]; + } + +} + +export default Item; \ No newline at end of file diff --git a/src/pages-client/collections/details.js b/src/pages-client/collections/details.js new file mode 100644 index 0000000..ce96412 --- /dev/null +++ b/src/pages-client/collections/details.js @@ -0,0 +1,164 @@ +import React, { useState } from 'react'; +import Helmet from 'react-helmet'; +import { Link } from 'gatsby'; +import { Select, MenuItem, InputLabel, Button } from '@material-ui/core'; +import { FaChevronLeft } from 'react-icons/fa'; + +import { useFetchCollectionItems } from 'hooks'; + +import Layout from 'components/Layout'; +import Dashboard from 'components/Dashboard'; +import DashboardControls from 'components/DashboardControls'; +import DataTable from 'components/DataTable'; +import Breadcrumbs from 'components/Breadcrumbs'; + +import { DASHBOARD_DEFAULT_FILTERS, DASHBOARD_COLLECTION_STATES, DASHBOARD_COLLECTION_STATES_DEFAULT } from 'data/dashboard'; + +const DEFAULT_COLLECTION_FILTERS = { + ...DASHBOARD_DEFAULT_FILTERS, + state: DASHBOARD_COLLECTION_STATES_DEFAULT.id +} + +const CollectionsDetailsPage = ({ collectionsId, workflowId }) => { + + const [filters, updateFilters] = useState(DEFAULT_COLLECTION_FILTERS); + const { state, since } = filters; + + const { data = {}, state: requestState, loadMore } = useFetchCollectionItems({ + collectionsId, + workflowId, + since, + state + }); + const { items } = data; + const { loading } = requestState; + + const hasItems = Array.isArray(items) && items.length > 0; + const canLoadMore = typeof loadMore === 'function'; + + const columns = [ + { + key: 'itemId', + Header: 'Item ID' + }, + { + key: 'createdFriendly', + Header: 'Date Created' + }, + { + key: 'updatedFriendly', + Header: 'Date Updated' + }, + { + key: 'state', + Header: 'State' + }, + { + key: 'actions', + Header: ' ', + Cell: (item) => { + const { lastExecution } = item; + return Last Execution + } + } + ] + + const rows = Array.isArray(items) && items.map(item => { + return columns.map(({ key, Cell }) => { + if ( Cell ) return Cell(item); + return item[key]; + }) + }); + + function handleOnFilter({ filters: updatedFilters }) { + updateFilters(prev => { + return { + ...prev, + ...updatedFilters + } + }); + } + + function handleOnChangeState({ target = {} }) { + const { value } = target; + handleOnFilter({ + filters: { + state: value + } + }); + } + + return ( + + + { collectionsId } - {workflowId} + + + { + return ( + + Back to All Collections + + ); + } + } + ]} /> +
+

{ collectionsId }

+

+ Workflow: {workflowId} +

+
+ { + return ( + <> + State: + + + ) + } + } + ]} /> + + + + { loading && !hasItems && ( +

+ Loading +

+ ) } + + { !loading && !hasItems && ( +

+ No Items +

+ ) } + + {canLoadMore && ( +

+ +

+ )} +
+ +
+ ); +}; + +export default CollectionsDetailsPage; diff --git a/src/pages/404.js b/src/pages/404.js new file mode 100644 index 0000000..dd1eaba --- /dev/null +++ b/src/pages/404.js @@ -0,0 +1,14 @@ +import React from 'react'; + +import Layout from 'components/Layout'; + +const NotFoundPage = () => { + return ( + +

Page Not Found

+

You just hit a route that doesn't exist... the sadness.

+
+ ); +;} + +export default NotFoundPage diff --git a/src/pages/collections.js b/src/pages/collections.js new file mode 100644 index 0000000..fb2140b --- /dev/null +++ b/src/pages/collections.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { Router } from '@reach/router'; + +import CollectionsDetailsPage from 'pages-client/collections/details'; + +const CollectionsIndex = () => { + return ( + <> + + + + + ); +}; + +export default CollectionsIndex; diff --git a/src/pages/index.js b/src/pages/index.js new file mode 100644 index 0000000..daeef45 --- /dev/null +++ b/src/pages/index.js @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; + +import { useFetchCollections } from 'hooks'; + +import Dashboard from 'components/Dashboard' +import DashboardControls from 'components/DashboardControls' +import Collection from 'components/Collection' +import Layout from 'components/Layout'; + +import { DASHBOARD_DEFAULT_FILTERS } from 'data/dashboard'; + +const IndexPage = () => { + const [filters, updateFilters] = useState(DASHBOARD_DEFAULT_FILTERS); + + const { collections = [] } = useFetchCollections(); + + function handleOnFilter({ filters: updatedFilters }) { + updateFilters(updatedFilters); + } + + return ( + + + + { collections && collections.map((collection = {}) => { + return + })} + + + ); +}; + +export default IndexPage;