فهرست منبع

Merge branch 'beta.2.integration-ui' into dev

Jo Chuang 5 سال پیش
والد
کامیت
0ed1c481a4
90فایلهای تغییر یافته به همراه1996 افزوده شده و 12275 حذف شده
  1. 8 9
      .github/ISSUE_TEMPLATE/bug.md
  2. 3 4
      .github/ISSUE_TEMPLATE/change.md
  3. 2 3
      .github/ISSUE_TEMPLATE/feature.md
  4. 8 5
      .github/PULL_REQUEST_TEMPLATE.md
  5. 44 44
      .github/workflows/release.yaml
  6. 1 2
      .github/workflows/staging.yaml
  7. 16 7
      README.md
  8. 76 0
      cli/cmd/api/registry.go
  9. 3 0
      cli/cmd/auth.go
  10. 52 0
      cli/cmd/connect.go
  11. 76 0
      cli/cmd/connect/dockerhub.go
  12. 76 0
      cli/cmd/connect/registry.go
  13. 50 1
      cli/cmd/docker.go
  14. 1 1
      cli/cmd/version.go
  15. 1 1
      cmd/docker-credential-porter/main.go
  16. 3 0
      dashboard/.prettierignore
  17. 1 0
      dashboard/.prettierrc.json
  18. 1 10251
      dashboard/package-lock.json
  19. 1 0
      dashboard/package.json
  20. 0 1
      dashboard/src/App.tsx
  21. BIN
      dashboard/src/assets/DaGsIs8VwAAGHM1.jpg
  22. 11 11
      dashboard/src/assets/GithubIcon.tsx
  23. 1 0
      dashboard/src/assets/discord.svg
  24. 1 0
      dashboard/src/components/ResourceTab.tsx
  25. 1 1
      dashboard/src/components/TabRegion.tsx
  26. 2 2
      dashboard/src/components/TabSelector.tsx
  27. 9 3
      dashboard/src/components/image-selector/ImageList.tsx
  28. 75 77
      dashboard/src/components/image-selector/ImageSelector.tsx
  29. 1 1
      dashboard/src/components/repo-selector/ActionConfEditor.tsx
  30. 9 26
      dashboard/src/components/repo-selector/ActionDetails.tsx
  31. 156 0
      dashboard/src/components/values-form/InputArray.tsx
  32. 1 1
      dashboard/src/components/values-form/InputRow.tsx
  33. 185 0
      dashboard/src/components/values-form/KeyValueArray.tsx
  34. 6 11
      dashboard/src/components/values-form/RangeSlider.tsx
  35. 35 7
      dashboard/src/components/values-form/ValuesForm.tsx
  36. 9 1
      dashboard/src/components/values-form/ValuesWrapper.tsx
  37. 40 26
      dashboard/src/index.html
  38. 2 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  39. 8 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx
  40. 3 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx
  41. 13 3
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx
  42. 5 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx
  43. 89 74
      dashboard/src/main/home/integrations/IntegrationList.tsx
  44. 2 2
      dashboard/src/main/home/launch/Launch.tsx
  45. 1 1
      dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx
  46. 160 97
      dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx
  47. 1 1
      dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx
  48. 4 17
      dashboard/src/main/home/modals/Modal.tsx
  49. 1 0
      dashboard/src/main/home/project-settings/InviteList.tsx
  50. 18 12
      dashboard/src/main/home/provisioner/InfraStatuses.tsx
  51. 91 73
      dashboard/src/main/home/provisioner/ProvisionerLogs.tsx
  52. 39 0
      dashboard/src/main/home/sidebar/Sidebar.tsx
  53. 2 2
      dashboard/src/shared/Context.tsx
  54. 35 23
      dashboard/src/shared/api.tsx
  55. 18 17
      dashboard/src/shared/common.tsx
  56. 1 0
      dashboard/src/shared/types.tsx
  57. 1 1
      dashboard/tsconfig.json
  58. 4 4
      docker-compose.dev.yaml
  59. 2 2
      docs/GCR.md
  60. 17 18
      docs/GETTING_STARTED.md
  61. 5 7
      helm/templates/service.yaml
  62. 9 5
      helm/values.yaml
  63. 1 1
      internal/config/config.go
  64. 14 12
      internal/forms/registry.go
  65. 0 4
      internal/forms/release.go
  66. 9 11
      internal/helm/grapher/test_yaml/cassandra.yaml
  67. 41 41
      internal/helm/grapher/test_yaml/ingress.yaml
  68. 32 30
      internal/helm/grapher/test_yaml/kafka.yaml
  69. 1 1
      internal/helm/grapher/test_yaml/volumes.yaml
  70. 1 1
      internal/integrations/ci/actions/actions.go
  71. 6 3
      internal/integrations/ci/actions/steps.go
  72. 0 2
      internal/kubernetes/agent.go
  73. 13 4
      internal/kubernetes/config.go
  74. 0 4
      internal/kubernetes/provisioner/global_stream.go
  75. 0 2
      internal/kubernetes/provisioner/resource_stream.go
  76. 13 12
      internal/models/integrations/integration.go
  77. 8 3
      internal/models/registry.go
  78. 252 0
      internal/registry/registry.go
  79. 24 12
      internal/repository/gorm/cluster.go
  80. 5 1213
      package-lock.json
  81. 0 3
      server/api/cluster_handler.go
  82. 2 2
      server/api/git_action_handler.go
  83. 0 2
      server/api/git_repo_handler.go
  84. 3 9
      server/api/integration_handler.go
  85. 0 5
      server/api/integration_handler_test.go
  86. 45 28
      server/api/registry_handler.go
  87. 0 3
      server/api/release_handler_test.go
  88. 19 0
      server/api/user_handler.go
  89. 1 3
      server/router/middleware/auth.go
  90. 10 0
      server/router/router.go

+ 8 - 9
.github/ISSUE_TEMPLATE/bug.md

@@ -1,23 +1,22 @@
 ---
 ---
 name: Bug Report
 name: Bug Report
-about: 🐛 Found a bug? Let us know! 
-
+about: 🐛 Found a bug? Let us know!
 ---
 ---
 
 
 # Description
 # Description
 
 
-<!-- Please provide a high-level description of what you were trying to accomplish and what went wrong. --> 
+<!-- Please provide a high-level description of what you were trying to accomplish and what went wrong. -->
 
 
 # Location
 # Location
 
 
-- [ ] Browser 
-- [ ] CLI 
+- [ ] Browser
+- [ ] CLI
 - [ ] API
 - [ ] API
 
 
 # Steps to reproduce
 # Steps to reproduce
 
 
-1. 
-2. 
-3. 
+1.
+2.
+3.
 
 
-# Additional Details
+# Additional Details

+ 3 - 4
.github/ISSUE_TEMPLATE/change.md

@@ -1,13 +1,12 @@
 ---
 ---
 name: Change
 name: Change
-about: 🛠️ Update functionality that already exists. 
-
+about: 🛠️ Update functionality that already exists.
 ---
 ---
 
 
 # Location
 # Location
 
 
-- [ ] Browser 
-- [ ] CLI 
+- [ ] Browser
+- [ ] CLI
 - [ ] API
 - [ ] API
 
 
 # Motivation
 # Motivation

+ 2 - 3
.github/ISSUE_TEMPLATE/feature.md

@@ -1,13 +1,12 @@
 ---
 ---
 name: Feature
 name: Feature
 about: ✨ Add new functionality to the project.
 about: ✨ Add new functionality to the project.
-
 ---
 ---
 
 
 # Location
 # Location
 
 
-- [ ] Browser 
-- [ ] CLI 
+- [ ] Browser
+- [ ] CLI
 - [ ] API
 - [ ] API
 
 
 # Requirements
 # Requirements

+ 8 - 5
.github/PULL_REQUEST_TEMPLATE.md

@@ -1,28 +1,31 @@
 ## Pull request type
 ## Pull request type
 
 
-<!-- Please try to limit your pull request to one type, submit multiple pull requests if needed. --> 
+<!-- Please try to limit your pull request to one type, submit multiple pull requests if needed. -->
 
 
 Please check the type of change your PR introduces:
 Please check the type of change your PR introduces:
+
 - [ ] Bugfix
 - [ ] Bugfix
 - [ ] Feature
 - [ ] Feature
-- [ ] Other (please describe): 
+- [ ] Other (please describe):
 
 
 ## Pull request checklist
 ## Pull request checklist
 
 
 Please check if your PR fulfills the following requirements:
 Please check if your PR fulfills the following requirements:
-- [ ] If it's a backend change, tests for the changes have been added and `go test ./...` runs successfully from the root folder. 
+
+- [ ] If it's a backend change, tests for the changes have been added and `go test ./...` runs successfully from the root folder.
 - [ ] If it's a frontend change, Prettier has been run
 - [ ] If it's a frontend change, Prettier has been run
 - [ ] Docs have been reviewed and added / updated if needed
 - [ ] Docs have been reviewed and added / updated if needed
 
 
 ## What is the current behavior?
 ## What is the current behavior?
-<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. 
+
+<!-- Please describe the current behavior that you are modifying, or link to a relevant issue.
 
 
 Issue Number: N/A
 Issue Number: N/A
 
 
 -->
 -->
 
 
-
 ## What is the new behavior?
 ## What is the new behavior?
+
 <!-- Please describe the behavior or changes that are being added by this PR. -->
 <!-- Please describe the behavior or changes that are being added by this PR. -->
 
 
 <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->
 <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->

+ 44 - 44
.github/workflows/release.yaml

@@ -1,7 +1,7 @@
 on:
 on:
   push:
   push:
     tags:
     tags:
-    - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
+      - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
 
 
 name: Create release w/ binaries and docker image
 name: Create release w/ binaries and docker image
 
 
@@ -9,38 +9,38 @@ jobs:
   docker-build-push:
   docker-build-push:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-    - name: Get tag name
-      id: tag_name
-      run: |
-        tag=${GITHUB_TAG/refs\/tags\//}
-        echo ::set-output name=tag::$tag
-      env:
-        GITHUB_TAG: ${{ github.ref }}
-    - name: Checkout
-      uses: actions/checkout@v2.3.4
-    - name: Setup docker
-      uses: docker/login-action@v1
-      with:
-        username: ${{ secrets.DOCKERHUB_USERNAME }}
-        password: ${{ secrets.DOCKERHUB_TOKEN }}
-    - name: Write Dashboard Environment Variables
-      run: |
-        cat >./dashboard/.env <<EOL
-        NODE_ENV=production
-        API_SERVER=dashboard.getporter.dev
-        FULLSTORY_ORG_ID=${{secrets.FULLSTORY_ORG_ID}}
-        DISCORD_KEY=${{secrets.DISCORD_KEY}}
-        DISCORD_CID=${{secrets.DISCORD_CID}}
-        FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
-        EOL
+      - name: Get tag name
+        id: tag_name
+        run: |
+          tag=${GITHUB_TAG/refs\/tags\//}
+          echo ::set-output name=tag::$tag
+        env:
+          GITHUB_TAG: ${{ github.ref }}
+      - name: Checkout
+        uses: actions/checkout@v2.3.4
+      - name: Setup docker
+        uses: docker/login-action@v1
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_TOKEN }}
+      - name: Write Dashboard Environment Variables
+        run: |
+          cat >./dashboard/.env <<EOL
+          NODE_ENV=production
+          API_SERVER=dashboard.getporter.dev
+          FULLSTORY_ORG_ID=${{secrets.FULLSTORY_ORG_ID}}
+          DISCORD_KEY=${{secrets.DISCORD_KEY}}
+          DISCORD_CID=${{secrets.DISCORD_CID}}
+          FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}
+          EOL
 
 
-        cat ./dashboard/.env
-    - name: Build
-      run: |
-        DOCKER_BUILDKIT=1 docker build . -t porter1/porter:${{steps.tag_name.outputs.tag}} -f ./docker/Dockerfile
-    - name: Push
-      run: |
-        docker push porter1/porter:${{steps.tag_name.outputs.tag}}
+          cat ./dashboard/.env
+      - name: Build
+        run: |
+          DOCKER_BUILDKIT=1 docker build . -t porter1/porter:${{steps.tag_name.outputs.tag}} -f ./docker/Dockerfile
+      - name: Push
+        run: |
+          docker push porter1/porter:${{steps.tag_name.outputs.tag}}
   build:
   build:
     name: Build binaries
     name: Build binaries
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
@@ -93,7 +93,7 @@ jobs:
       # Note: we have to zip all binaries before uploading them as artifacts --
       # Note: we have to zip all binaries before uploading them as artifacts --
       # without this step, the binaries will be uploaded but the file metadata will
       # without this step, the binaries will be uploaded but the file metadata will
       # be listed as plaintext after downloading the artifact in a later step
       # be listed as plaintext after downloading the artifact in a later step
-      # 
+      #
       # TODO: investigate
       # TODO: investigate
       - name: Zip Linux binaries
       - name: Zip Linux binaries
         run: |
         run: |
@@ -151,7 +151,7 @@ jobs:
       - name: Install gon via HomeBrew for code signing and app notarization
       - name: Install gon via HomeBrew for code signing and app notarization
         run: |
         run: |
           brew tap mitchellh/gon
           brew tap mitchellh/gon
-          brew install mitchellh/gon/gon  
+          brew install mitchellh/gon/gon
       - name: Create a porter.gon.json file
       - name: Create a porter.gon.json file
         run: |
         run: |
           echo "
           echo "
@@ -246,7 +246,7 @@ jobs:
           draft: false
           draft: false
           prerelease: true
           prerelease: true
       - name: Upload Linux CLI Release Asset
       - name: Upload Linux CLI Release Asset
-        id: upload-linux-cli-release-asset 
+        id: upload-linux-cli-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -257,7 +257,7 @@ jobs:
           asset_name: porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
           asset_name: porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
           asset_content_type: application/zip
           asset_content_type: application/zip
       - name: Upload Linux Server Release Asset
       - name: Upload Linux Server Release Asset
-        id: upload-linux-server-release-asset 
+        id: upload-linux-server-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -268,7 +268,7 @@ jobs:
           asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
           asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
           asset_content_type: application/zip
           asset_content_type: application/zip
       - name: Upload Linux Docker Credential Release Asset
       - name: Upload Linux Docker Credential Release Asset
-        id: upload-linux-docker-cred-release-asset 
+        id: upload-linux-docker-cred-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -279,7 +279,7 @@ jobs:
           asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
           asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Linux_x86_64.zip
           asset_content_type: application/zip
           asset_content_type: application/zip
       - name: Upload Darwin CLI Release Asset
       - name: Upload Darwin CLI Release Asset
-        id: upload-darwin-cli-release-asset 
+        id: upload-darwin-cli-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -290,7 +290,7 @@ jobs:
           asset_name: porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
           asset_name: porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
           asset_content_type: application/zip
           asset_content_type: application/zip
       - name: Upload Darwin Server Release Asset
       - name: Upload Darwin Server Release Asset
-        id: upload-darwin-server-release-asset 
+        id: upload-darwin-server-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -301,7 +301,7 @@ jobs:
           asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
           asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
           asset_content_type: application/zip
           asset_content_type: application/zip
       - name: Upload Darwin Docker Credential Release Asset
       - name: Upload Darwin Docker Credential Release Asset
-        id: upload-darwin-docker-cred-release-asset 
+        id: upload-darwin-docker-cred-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -312,7 +312,7 @@ jobs:
           asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
           asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Darwin_x86_64.zip
           asset_content_type: application/zip
           asset_content_type: application/zip
       - name: Upload Windows CLI Release Asset
       - name: Upload Windows CLI Release Asset
-        id: upload-windows-cli-release-asset 
+        id: upload-windows-cli-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -323,7 +323,7 @@ jobs:
           asset_name: porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_name: porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_content_type: application/zip
           asset_content_type: application/zip
       - name: Upload Windows Server Release Asset
       - name: Upload Windows Server Release Asset
-        id: upload-windows-server-release-asset 
+        id: upload-windows-server-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -334,7 +334,7 @@ jobs:
           asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_content_type: application/zip
           asset_content_type: application/zip
       - name: Upload Windows Docker Credential Release Asset
       - name: Upload Windows Docker Credential Release Asset
-        id: upload-windows-docker-cred-release-asset 
+        id: upload-windows-docker-cred-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -345,7 +345,7 @@ jobs:
           asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_content_type: application/zip
           asset_content_type: application/zip
       - name: Upload Static Release Asset
       - name: Upload Static Release Asset
-        id: upload-static-release-asset 
+        id: upload-static-release-asset
         uses: actions/upload-release-asset@v1
         uses: actions/upload-release-asset@v1
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 1 - 2
.github/workflows/staging.yaml

@@ -43,5 +43,4 @@ jobs:
         gcloud container clusters get-credentials \
         gcloud container clusters get-credentials \
           staging --region us-central1 --project ${{ secrets.GCP_PROJECT_ID }}
           staging --region us-central1 --project ${{ secrets.GCP_PROJECT_ID }}
           
           
-        kubectl rollout restart deployment/porter
-    
+        kubectl rollout restart deployment/porter

+ 16 - 7
README.md

@@ -1,4 +1,5 @@
-# Porter 
+# Porter
+
 [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter) [![Discord](https://img.shields.io/discord/542888846271184896?color=7389D8&label=community&logo=discord&logoColor=ffffff)](https://discord.gg/MhYNuWwqum)
 [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs) [![Go Report Card](https://goreportcard.com/badge/gojp/goreportcard)](https://goreportcard.com/report/github.com/porter-dev/porter) [![Discord](https://img.shields.io/discord/542888846271184896?color=7389D8&label=community&logo=discord&logoColor=ffffff)](https://discord.gg/MhYNuWwqum)
 [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow)](https://twitter.com/getporterdev)
 [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow)](https://twitter.com/getporterdev)
 
 
@@ -13,20 +14,23 @@ For help, questions, or if you just want a place to hang out, [join our Discord
 To keep updated on our progress, please watch the repo for new releases (**Watch > Custom > Releases**) and [follow us on Twitter](https://twitter.com/getporterdev)!
 To keep updated on our progress, please watch the repo for new releases (**Watch > Custom > Releases**) and [follow us on Twitter](https://twitter.com/getporterdev)!
 
 
 ## Why Porter?
 ## Why Porter?
+
 ### A PaaS that grows with your applications
 ### A PaaS that grows with your applications
 
 
-A traditional PaaS like Heroku is great for minimizing unnecessary DevOps work but doesn't offer enough flexibility as your applications grow. Custom network rules, resource constraints, and cost are common reasons developers move their applications off Heroku beyond a certain scale. 
+A traditional PaaS like Heroku is great for minimizing unnecessary DevOps work but doesn't offer enough flexibility as your applications grow. Custom network rules, resource constraints, and cost are common reasons developers move their applications off Heroku beyond a certain scale.
 
 
 Porter brings the simplicity of a traditional PaaS to your own cloud provider while preserving the configurability of Kubernetes. Porter is built on top of a popular Kubernetes package manager `helm` and is compatible with standard Kubernetes management tools like `kubectl`, preparing your infra for mature DevOps work from day one.
 Porter brings the simplicity of a traditional PaaS to your own cloud provider while preserving the configurability of Kubernetes. Porter is built on top of a popular Kubernetes package manager `helm` and is compatible with standard Kubernetes management tools like `kubectl`, preparing your infra for mature DevOps work from day one.
 
 
 ![image](https://user-images.githubusercontent.com/65516095/103713478-71e75800-4f8a-11eb-915f-adee9d4f5bf7.png)
 ![image](https://user-images.githubusercontent.com/65516095/103713478-71e75800-4f8a-11eb-915f-adee9d4f5bf7.png)
 
 
 ## Features
 ## Features
+
 ### Basics
 ### Basics
+
 - One-click provisioning of a Kubernetes cluster in your own cloud console
 - One-click provisioning of a Kubernetes cluster in your own cloud console
-  - ✅   AWS
-  - ✅   GCP
-  - ✅   Digital Ocean
+  - ✅ AWS
+  - ✅ GCP
+  - ✅ Digital Ocean
 - Simple deploy of any public or private Docker image
 - Simple deploy of any public or private Docker image
 - Heroku-like GUI to monitor application status, logs, and history
 - Heroku-like GUI to monitor application status, logs, and history
 - Marketplace for one click add-ons (e.g. MongoDB, Redis, PostgreSQL)
 - Marketplace for one click add-ons (e.g. MongoDB, Redis, PostgreSQL)
@@ -35,6 +39,7 @@ Porter brings the simplicity of a traditional PaaS to your own cloud provider wh
 - Native CI/CD with buildpacks for non-Dockerized apps (🚧 Coming Soon)
 - Native CI/CD with buildpacks for non-Dockerized apps (🚧 Coming Soon)
 
 
 ### DevOps Mode
 ### DevOps Mode
+
 For those who are familiar with Kubernetes and Helm:
 For those who are familiar with Kubernetes and Helm:
 
 
 - Connect to existing Kubernetes clusters that are not provisioned by Porter
 - Connect to existing Kubernetes clusters that are not provisioned by Porter
@@ -50,7 +55,9 @@ For those who are familiar with Kubernetes and Helm:
 Below are instructions for a quickstart. For full documentation, please visit our [official Docs.](https://docs.getporter.dev)
 Below are instructions for a quickstart. For full documentation, please visit our [official Docs.](https://docs.getporter.dev)
 
 
 ## CLI Installation
 ## CLI Installation
-### Mac 
+
+### Mac
+
 Run the following command to grab the latest binary:
 Run the following command to grab the latest binary:
 
 
 ```sh
 ```sh
@@ -73,6 +80,7 @@ sudo mv ./porter /usr/local/bin/porter
 For Linux and Windows installation, see our [Docs](https://docs.getporter.dev/docs/cli-documentation#linux).
 For Linux and Windows installation, see our [Docs](https://docs.getporter.dev/docs/cli-documentation#linux).
 
 
 ## Getting Started
 ## Getting Started
+
 1. Sign up and log into [Porter Dashboard](https://dashboard.getporter.dev).
 1. Sign up and log into [Porter Dashboard](https://dashboard.getporter.dev).
 
 
 2. Create a Project and select a cloud provider you want to provision a Kubernetes cluster in (AWS, GCP, DO). It is also possible to [link up your own Kubernetes cluster.](https://docs.getporter.dev/docs/cli-documentation#connecting-to-an-existing-cluster)
 2. Create a Project and select a cloud provider you want to provision a Kubernetes cluster in (AWS, GCP, DO). It is also possible to [link up your own Kubernetes cluster.](https://docs.getporter.dev/docs/cli-documentation#connecting-to-an-existing-cluster)
@@ -84,6 +92,7 @@ For Linux and Windows installation, see our [Docs](https://docs.getporter.dev/do
 5. From the Templates tab on the Dashboard, select the Docker template. Click on the image you have just pushed, configure the port, then hit deploy.
 5. From the Templates tab on the Dashboard, select the Docker template. Click on the image you have just pushed, configure the port, then hit deploy.
 
 
 ## Want to Help?
 ## Want to Help?
-We welcome all contributions. Submit an issue or a pull request to help us improve Porter! If you're interested in contributing, please [join our Discord community.](https://discord.gg/MhYNuWwqum) 
+
+We welcome all contributions. Submit an issue or a pull request to help us improve Porter! If you're interested in contributing, please [join our Discord community.](https://discord.gg/MhYNuWwqum)
 
 
 ![porter](https://user-images.githubusercontent.com/65516095/103712859-def9ee00-4f88-11eb-804c-4b775d697ec4.jpeg)
 ![porter](https://user-images.githubusercontent.com/65516095/103712859-def9ee00-4f88-11eb-804c-4b775d697ec4.jpeg)

+ 76 - 0
cli/cmd/api/registry.go

@@ -59,6 +59,53 @@ func (c *Client) CreateECR(
 	return bodyResp, nil
 	return bodyResp, nil
 }
 }
 
 
+// CreatePrivateRegistryRequest represents the accepted fields for creating
+// a private registry
+type CreatePrivateRegistryRequest struct {
+	Name               string `json:"name"`
+	URL                string `json:"url"`
+	BasicIntegrationID uint   `json:"basic_integration_id"`
+}
+
+// CreatePrivateRegistryResponse is the resulting registry after creation
+type CreatePrivateRegistryResponse models.RegistryExternal
+
+// CreatePrivateRegistry creates a private registry integration
+func (c *Client) CreatePrivateRegistry(
+	ctx context.Context,
+	projectID uint,
+	createPR *CreatePrivateRegistryRequest,
+) (*CreatePrivateRegistryResponse, error) {
+	data, err := json.Marshal(createPR)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest(
+		"POST",
+		fmt.Sprintf("%s/projects/%d/registries", c.BaseURL, projectID),
+		strings.NewReader(string(data)),
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(ctx)
+	bodyResp := &CreatePrivateRegistryResponse{}
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}
+
 // CreateGCRRequest represents the accepted fields for creating
 // CreateGCRRequest represents the accepted fields for creating
 // a GCR registry
 // a GCR registry
 type CreateGCRRequest struct {
 type CreateGCRRequest struct {
@@ -290,6 +337,35 @@ func (c *Client) GetGCRAuthorizationToken(
 	return bodyResp, nil
 	return bodyResp, nil
 }
 }
 
 
+// GetDockerhubAuthorizationToken gets a Docker Hub authorization token
+func (c *Client) GetDockerhubAuthorizationToken(
+	ctx context.Context,
+	projectID uint,
+) (*GetTokenResponse, error) {
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s/projects/%d/registries/dockerhub/token", c.BaseURL, projectID),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	bodyResp := &GetTokenResponse{}
+	req = req.WithContext(ctx)
+
+	if httpErr, err := c.sendRequest(req, bodyResp, true); httpErr != nil || err != nil {
+		if httpErr != nil {
+			return nil, fmt.Errorf("code %d, errors %v", httpErr.Code, httpErr.Errors)
+		}
+
+		return nil, err
+	}
+
+	return bodyResp, nil
+}
+
 type GetDOCRTokenRequest struct {
 type GetDOCRTokenRequest struct {
 	ServerURL string `json:"server_url"`
 	ServerURL string `json:"server_url"`
 }
 }

+ 3 - 0
cli/cmd/auth.go

@@ -200,6 +200,9 @@ func loginManual() error {
 		return err
 		return err
 	}
 	}
 
 
+	// set the token to empty since this is manual (cookie-based) login
+	setToken("")
+
 	color.New(color.FgGreen).Println("Successfully logged in!")
 	color.New(color.FgGreen).Println("Successfully logged in!")
 
 
 	// get a list of projects, and set the current project
 	// get a list of projects, and set the current project

+ 52 - 0
cli/cmd/connect.go

@@ -43,6 +43,30 @@ var connectECRCmd = &cobra.Command{
 	},
 	},
 }
 }
 
 
+var connectDockerhubCmd = &cobra.Command{
+	Use:   "dockerhub",
+	Short: "Adds a Docker Hub registry integration to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, runConnectDockerhub)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
+var connectRegistryCmd = &cobra.Command{
+	Use:   "registry",
+	Short: "Adds a custom image registry to a project",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := checkLoginAndRun(args, runConnectRegistry)
+
+		if err != nil {
+			os.Exit(1)
+		}
+	},
+}
+
 var connectActionsCmd = &cobra.Command{
 var connectActionsCmd = &cobra.Command{
 	Use:   "actions",
 	Use:   "actions",
 	Short: "Adds Github Actions to a project",
 	Short: "Adds Github Actions to a project",
@@ -127,6 +151,8 @@ func init() {
 
 
 	connectCmd.AddCommand(connectActionsCmd)
 	connectCmd.AddCommand(connectActionsCmd)
 	connectCmd.AddCommand(connectECRCmd)
 	connectCmd.AddCommand(connectECRCmd)
+	connectCmd.AddCommand(connectRegistryCmd)
+	connectCmd.AddCommand(connectDockerhubCmd)
 	connectCmd.AddCommand(connectGCRCmd)
 	connectCmd.AddCommand(connectGCRCmd)
 	connectCmd.AddCommand(connectDOCRCmd)
 	connectCmd.AddCommand(connectDOCRCmd)
 	connectCmd.AddCommand(connectHRCmd)
 	connectCmd.AddCommand(connectHRCmd)
@@ -193,6 +219,32 @@ func runConnectDOCR(_ *api.AuthCheckResponse, client *api.Client, _ []string) er
 	return setRegistry(regID)
 	return setRegistry(regID)
 }
 }
 
 
+func runConnectDockerhub(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
+	regID, err := connect.Dockerhub(
+		client,
+		getProjectID(),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return setRegistry(regID)
+}
+
+func runConnectRegistry(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
+	regID, err := connect.Registry(
+		client,
+		getProjectID(),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	return setRegistry(regID)
+}
+
 func runConnectHelmRepoBasic(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
 func runConnectHelmRepoBasic(_ *api.AuthCheckResponse, client *api.Client, _ []string) error {
 	hrID, err := connect.Helm(
 	hrID, err := connect.Helm(
 		client,
 		client,

+ 76 - 0
cli/cmd/connect/dockerhub.go

@@ -0,0 +1,76 @@
+package connect
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+)
+
+func Dockerhub(
+	client *api.Client,
+	projectID uint,
+) (uint, error) {
+	// if project ID is 0, ask the user to set the project ID or create a project
+	if projectID == 0 {
+		return 0, fmt.Errorf("no project set, please run porter project set [id]")
+	}
+
+	// query for dockerhub name
+
+	repoName, err := utils.PromptPlaintext(fmt.Sprintf(`Provide the Docker Hub image path, in the form of ${org_name}/${repo_name}. For example, porter1/porter.
+Image path: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	username, err := utils.PromptPlaintext(fmt.Sprintf(`Docker Hub username: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	password, err := utils.PromptPassword(`Provide the Docker Hub personal access token.
+Token:`)
+
+	if err != nil {
+		return 0, err
+	}
+
+	// create the basic auth integration
+	integration, err := client.CreateBasicAuthIntegration(
+		context.Background(),
+		projectID,
+		&api.CreateBasicAuthIntegrationRequest{
+			Username: username,
+			Password: password,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
+
+	reg, err := client.CreatePrivateRegistry(
+		context.Background(),
+		projectID,
+		&api.CreatePrivateRegistryRequest{
+			URL:                fmt.Sprintf("index.docker.io/%s", repoName),
+			Name:               repoName,
+			BasicIntegrationID: integration.ID,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created private registry with id %d and name %s\n", reg.ID, reg.Name)
+
+	return reg.ID, nil
+}

+ 76 - 0
cli/cmd/connect/registry.go

@@ -0,0 +1,76 @@
+package connect
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/fatih/color"
+	"github.com/porter-dev/porter/cli/cmd/api"
+	"github.com/porter-dev/porter/cli/cmd/utils"
+)
+
+// Helm connects a Helm repository using HTTP basic authentication
+func Registry(
+	client *api.Client,
+	projectID uint,
+) (uint, error) {
+	// if project ID is 0, ask the user to set the project ID or create a project
+	if projectID == 0 {
+		return 0, fmt.Errorf("no project set, please run porter project set [id]")
+	}
+
+	// query for helm repo name
+	repoURL, err := utils.PromptPlaintext(fmt.Sprintf(`Provide the image registry URL (include the protocol). For example, https://my-custom-registry.getporter.dev.
+Image registry URL: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	username, err := utils.PromptPlaintext(fmt.Sprintf(`Provide the username/password for authentication (press enter if no authenicaiton is required).
+Username: `))
+
+	if err != nil {
+		return 0, err
+	}
+
+	password, err := utils.PromptPasswordWithConfirmation()
+
+	if err != nil {
+		return 0, err
+	}
+
+	// create the basic auth integration
+	integration, err := client.CreateBasicAuthIntegration(
+		context.Background(),
+		projectID,
+		&api.CreateBasicAuthIntegrationRequest{
+			Username: username,
+			Password: password,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created basic auth integration with id %d\n", integration.ID)
+
+	reg, err := client.CreatePrivateRegistry(
+		context.Background(),
+		projectID,
+		&api.CreatePrivateRegistryRequest{
+			URL:                repoURL,
+			Name:               repoURL,
+			BasicIntegrationID: integration.ID,
+		},
+	)
+
+	if err != nil {
+		return 0, err
+	}
+
+	color.New(color.FgGreen).Printf("created private registry with id %d and name %s\n", reg.ID, reg.Name)
+
+	return reg.ID, nil
+}

+ 50 - 1
cli/cmd/docker.go

@@ -2,7 +2,9 @@ package cmd
 
 
 import (
 import (
 	"context"
 	"context"
+	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
+	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net/url"
 	"net/url"
 	"os"
 	"os"
@@ -16,6 +18,7 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 
 
 	"github.com/docker/cli/cli/config/configfile"
 	"github.com/docker/cli/cli/config/configfile"
+	"github.com/docker/cli/cli/config/types"
 )
 )
 
 
 var dockerCmd = &cobra.Command{
 var dockerCmd = &cobra.Command{
@@ -131,8 +134,54 @@ func dockerConfig(user *api.AuthCheckResponse, client *api.Client, args []string
 		return err
 		return err
 	}
 	}
 
 
+	if config.CredentialHelpers == nil {
+		config.CredentialHelpers = make(map[string]string)
+	}
+
 	for _, regURL := range regToAdd {
 	for _, regURL := range regToAdd {
-		config.CredentialHelpers[regURL] = "porter"
+		// if this is a dockerhub registry, see if an auth config has already been generated
+		// for index.docker.io
+		if strings.Contains(regURL, "index.docker.io") {
+			isAuthenticated := false
+
+			for key, _ := range config.AuthConfigs {
+				if key == "https://index.docker.io/v1/" {
+					isAuthenticated = true
+				}
+			}
+
+			if !isAuthenticated {
+				// get a dockerhub token from the Porter API
+				tokenResp, err := client.GetDockerhubAuthorizationToken(context.Background(), getProjectID())
+
+				if err != nil {
+					return err
+				}
+
+				decodedToken, err := base64.StdEncoding.DecodeString(tokenResp.Token)
+
+				if err != nil {
+					return fmt.Errorf("Invalid token: %v", err)
+				}
+
+				parts := strings.SplitN(string(decodedToken), ":", 2)
+
+				if len(parts) < 2 {
+					return fmt.Errorf("Invalid token: expected two parts, got %d", len(parts))
+				}
+
+				config.AuthConfigs["https://index.docker.io/v1/"] = types.AuthConfig{
+					Auth:     tokenResp.Token,
+					Username: parts[0],
+					Password: parts[1],
+				}
+
+				// since we're using token-based auth, unset the credstore
+				config.CredentialsStore = ""
+			}
+		} else {
+			config.CredentialHelpers[regURL] = "porter"
+		}
 	}
 	}
 
 
 	return config.Save()
 	return config.Save()

+ 1 - 1
cli/cmd/version.go

@@ -7,7 +7,7 @@ import (
 )
 )
 
 
 // Version will be linked by an ldflag during build
 // Version will be linked by an ldflag during build
-var Version string = "dev"
+var Version string = "v0.1.0-beta.3.4"
 
 
 var versionCmd = &cobra.Command{
 var versionCmd = &cobra.Command{
 	Use:     "version",
 	Use:     "version",

+ 1 - 1
cmd/docker-credential-porter/main.go

@@ -10,7 +10,7 @@ import (
 )
 )
 
 
 // Version will be linked by an ldflag during build
 // Version will be linked by an ldflag during build
-var Version string = "dev"
+var Version string = "v0.1.0-beta.3.4"
 
 
 func main() {
 func main() {
 	var versionFlag bool
 	var versionFlag bool

+ 3 - 0
dashboard/.prettierignore

@@ -0,0 +1,3 @@
+# Ignore artifacts:
+build
+coverage

+ 1 - 0
dashboard/.prettierrc.json

@@ -0,0 +1 @@
+{}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 10251
dashboard/package-lock.json


+ 1 - 0
dashboard/package.json

@@ -64,6 +64,7 @@
     "@types/styled-components": "^5.1.3",
     "@types/styled-components": "^5.1.3",
     "file-loader": "^6.1.0",
     "file-loader": "^6.1.0",
     "html-webpack-plugin": "^4.5.0",
     "html-webpack-plugin": "^4.5.0",
+    "prettier": "2.2.1",
     "qs": "^6.9.4",
     "qs": "^6.9.4",
     "source-map-loader": "^1.1.0",
     "source-map-loader": "^1.1.0",
     "ts-loader": "^8.0.4",
     "ts-loader": "^8.0.4",

+ 0 - 1
dashboard/src/App.tsx

@@ -7,7 +7,6 @@ type PropsType = {};
 
 
 type StateType = {};
 type StateType = {};
 
 
-
 export default class App extends Component<PropsType, StateType> {
 export default class App extends Component<PropsType, StateType> {
   render() {
   render() {
     return (
     return (

BIN
dashboard/src/assets/DaGsIs8VwAAGHM1.jpg


+ 11 - 11
dashboard/src/assets/GithubIcon.tsx

@@ -1,18 +1,18 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
+import React, { Component } from "react";
+import styled from "styled-components";
 
 
-type PropsType = {
-};
-
-type StateType = {
-};
+type PropsType = {};
 
 
+type StateType = {};
 
 
 export default class GHIcon extends Component<PropsType, StateType> {
 export default class GHIcon extends Component<PropsType, StateType> {
   render() {
   render() {
-    return(
-      <Svg height='18' width='18' viewBox='0 0 16 16'>
-        <path fillRule='evenodd' d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
+    return (
+      <Svg height="18" width="18" viewBox="0 0 16 16">
+        <path
+          fillRule="evenodd"
+          d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
+        />
       </Svg>
       </Svg>
     );
     );
   }
   }
@@ -21,4 +21,4 @@ export default class GHIcon extends Component<PropsType, StateType> {
 const Svg = styled.svg`
 const Svg = styled.svg`
   fill: white;
   fill: white;
   margin-right: 6px;
   margin-right: 6px;
-`;
+`;

+ 1 - 0
dashboard/src/assets/discord.svg

@@ -0,0 +1 @@
+<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

+ 1 - 0
dashboard/src/components/ResourceTab.tsx

@@ -259,6 +259,7 @@ const StatusColor = styled.div`
 
 
 const ResourceName = styled.div`
 const ResourceName = styled.div`
   color: #ffffff;
   color: #ffffff;
+  max-width: 40%;
   margin-left: ${(props: { showKindLabels: boolean }) =>
   margin-left: ${(props: { showKindLabels: boolean }) =>
     props.showKindLabels ? "10px" : ""};
     props.showKindLabels ? "10px" : ""};
   text-transform: none;
   text-transform: none;

+ 1 - 1
dashboard/src/components/TabRegion.tsx

@@ -77,7 +77,7 @@ const Div = styled.div`
 `;
 `;
 
 
 const TabContents = styled.div`
 const TabContents = styled.div`
-  height: calc(100% - 60px);
+  height: calc(100% - 80px);
 `;
 `;
 
 
 const Gap = styled.div`
 const Gap = styled.div`

+ 2 - 2
dashboard/src/components/TabSelector.tsx

@@ -95,10 +95,10 @@ const Tab = styled.div`
 
 
 const StyledTabSelector = styled.div`
 const StyledTabSelector = styled.div`
   display: flex;
   display: flex;
-  width: calc(100% - 4px);
+  width: calc(100% - 2px);
   align-items: center;
   align-items: center;
   border-bottom: 1px solid #aaaabb55;
   border-bottom: 1px solid #aaaabb55;
   padding-bottom: 1px;
   padding-bottom: 1px;
-  margin-left: 2px;
+  margin-left: 1px;
   position: relative;
   position: relative;
 `;
 `;

+ 9 - 3
dashboard/src/components/image-selector/ImageList.tsx

@@ -26,7 +26,7 @@ type StateType = {
   images: ImageType[];
   images: ImageType[];
 };
 };
 
 
-export default class ImageSelector extends Component<PropsType, StateType> {
+export default class ImageList extends Component<PropsType, StateType> {
   state = {
   state = {
     loading: true,
     loading: true,
     error: false,
     error: false,
@@ -37,6 +37,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     const { currentProject, setCurrentError } = this.context;
     const { currentProject, setCurrentError } = this.context;
     let images = [] as ImageType[];
     let images = [] as ImageType[];
     let errors = [] as number[];
     let errors = [] as number[];
+
     if (!this.props.registry) {
     if (!this.props.registry) {
       api
       api
         .getProjectRegistries("<token>", {}, { id: currentProject.id })
         .getProjectRegistries("<token>", {}, { id: currentProject.id })
@@ -98,7 +99,12 @@ export default class ImageSelector extends Component<PropsType, StateType> {
                         loading: false,
                         loading: false,
                         error,
                         error,
                       });
                       });
+                    } else {
+                      this.setState({
+                        images,
+                      });
                     }
                     }
+
                     resolveToNextController();
                     resolveToNextController();
                   });
                   });
               }
               }
@@ -247,7 +253,7 @@ export default class ImageSelector extends Component<PropsType, StateType> {
   }
   }
 }
 }
 
 
-ImageSelector.contextType = Context;
+ImageList.contextType = Context;
 
 
 const BackButton = styled.div`
 const BackButton = styled.div`
   display: flex;
   display: flex;
@@ -280,7 +286,7 @@ const ImageItem = styled.div`
   font-size: 13px;
   font-size: 13px;
   border-bottom: 1px solid
   border-bottom: 1px solid
     ${(props: { lastItem: boolean; isSelected: boolean }) =>
     ${(props: { lastItem: boolean; isSelected: boolean }) =>
-      props.lastItem ? "#00000000" : "#606166"};
+    props.lastItem ? "#00000000" : "#606166"};
   color: #ffffff;
   color: #ffffff;
   user-select: none;
   user-select: none;
   align-items: center;
   align-items: center;

+ 75 - 77
dashboard/src/components/image-selector/ImageSelector.tsx

@@ -3,13 +3,11 @@ import styled from "styled-components";
 import info from "assets/info.svg";
 import info from "assets/info.svg";
 import edit from "assets/edit.svg";
 import edit from "assets/edit.svg";
 
 
-import api from "shared/api";
 import { integrationList } from "shared/common";
 import { integrationList } from "shared/common";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import { ImageType } from "shared/types";
 import { ImageType } from "shared/types";
 
 
 import Loading from "../Loading";
 import Loading from "../Loading";
-import TagList from "./TagList";
 import ImageList from "./ImageList";
 import ImageList from "./ImageList";
 
 
 type PropsType = {
 type PropsType = {
@@ -38,81 +36,81 @@ export default class ImageSelector extends Component<PropsType, StateType> {
     clickedImage: null as ImageType | null,
     clickedImage: null as ImageType | null,
   };
   };
 
 
-  componentDidMount() {
-    const { currentProject, setCurrentError } = this.context;
-    let images = [] as ImageType[];
-    let errors = [] as number[];
-    api
-      .getProjectRegistries("<token>", {}, { id: currentProject.id })
-      .then(async (res) => {
-        let registries = res.data;
-        if (registries.length === 0) {
-          this.setState({ loading: false });
-        }
-
-        // Loop over connected image registries
-        registries.forEach(async (registry: any, i: number) => {
-          await new Promise((nextController: (res?: any) => void) => {
-            api
-              .getImageRepos(
-                "<token>",
-                {},
-                {
-                  project_id: currentProject.id,
-                  registry_id: registry.id,
-                }
-              )
-              .then((res) => {
-                res.data.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
-                // Loop over found image repositories
-                let newImg = res.data.map((img: any) => {
-                  if (this.props.selectedImageUrl === img.uri) {
-                    this.setState({
-                      clickedImage: {
-                        kind: registry.service,
-                        source: img.uri,
-                        name: img.name,
-                        registryId: registry.id,
-                      },
-                    });
-                  }
-                  return {
-                    kind: registry.service,
-                    source: img.uri,
-                    name: img.name,
-                    registryId: registry.id,
-                  };
-                });
-                images.push(...newImg);
-                errors.push(0);
-              })
-              .catch(() => errors.push(1))
-              .finally(() => {
-                if (i == registries.length - 1) {
-                  let error =
-                    errors.reduce((a, b) => {
-                      return a + b;
-                    }) == registries.length
-                      ? true
-                      : false;
-
-                  this.setState({
-                    images,
-                    loading: false,
-                    error,
-                  });
-                }
-
-                nextController();
-              });
-          });
-        });
-      })
-      .catch((err) => {
-        console.log(err);
-        this.setState({ error: true });
-      });
-  }
+  // componentDidMount() {
+  //   const { currentProject, setCurrentError } = this.context;
+  //   let images = [] as ImageType[];
+  //   let errors = [] as number[];
+  //   api
+  //     .getProjectRegistries("<token>", {}, { id: currentProject.id })
+  //     .then(async (res) => {
+  //       let registries = res.data;
+  //       if (registries.length === 0) {
+  //         this.setState({ loading: false });
+  //       }
+
+  //       // Loop over connected image registries
+  //       registries.forEach(async (registry: any, i: number) => {
+  //         await new Promise((nextController: (res?: any) => void) => {
+  //           api
+  //             .getImageRepos(
+  //               "<token>",
+  //               {},
+  //               {
+  //                 project_id: currentProject.id,
+  //                 registry_id: registry.id,
+  //               }
+  //             )
+  //             .then((res) => {
+  //               res.data.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
+  //               // Loop over found image repositories
+  //               let newImg = res.data.map((img: any) => {
+  //                 if (this.props.selectedImageUrl === img.uri) {
+  //                   this.setState({
+  //                     clickedImage: {
+  //                       kind: registry.service,
+  //                       source: img.uri,
+  //                       name: img.name,
+  //                       registryId: registry.id,
+  //                     },
+  //                   });
+  //                 }
+  //                 return {
+  //                   kind: registry.service,
+  //                   source: img.uri,
+  //                   name: img.name,
+  //                   registryId: registry.id,
+  //                 };
+  //               });
+  //               images.push(...newImg);
+  //               errors.push(0);
+  //             })
+  //             .catch(() => errors.push(1))
+  //             .finally(() => {
+  //               if (i == registries.length - 1) {
+  //                 let error =
+  //                   errors.reduce((a, b) => {
+  //                     return a + b;
+  //                   }) == registries.length
+  //                     ? true
+  //                     : false;
+
+  //                 this.setState({
+  //                   images,
+  //                   loading: false,
+  //                   error,
+  //                 });
+  //               }
+
+  //               nextController();
+  //             });
+  //         });
+  //       });
+  //     })
+  //     .catch((err) => {
+  //       console.log(err);
+  //       this.setState({ error: true });
+  //     });
+  // }
 
 
   /*
   /*
   <Highlight onClick={() => this.props.setCurrentView('integrations')}>
   <Highlight onClick={() => this.props.setCurrentView('integrations')}>

+ 1 - 1
dashboard/src/components/repo-selector/ActionConfEditor.tsx

@@ -16,7 +16,7 @@ type PropsType = {
   setActionConfig: (x: ActionConfigType) => void;
   setActionConfig: (x: ActionConfigType) => void;
   setBranch: (x: string) => void;
   setBranch: (x: string) => void;
   setPath: (x: boolean) => void;
   setPath: (x: boolean) => void;
-  reset: () => void;
+  reset: any;
 };
 };
 
 
 type StateType = {
 type StateType = {

+ 9 - 26
dashboard/src/components/repo-selector/ActionDetails.tsx

@@ -45,31 +45,6 @@ export default class ActionDetails extends Component<PropsType, StateType> {
   };
   };
 
 
   renderConfirmation = () => {
   renderConfirmation = () => {
-    var imageComponent
-
-    if (!this.props.actionConfig.image_repo_uri) {
-      imageComponent = <div>
-          <Label>Target Image URL</Label>
-          <ImageSelector
-            selectedTag="latest"
-            selectedImageUrl={this.props.actionConfig.image_repo_uri}
-            setSelectedImageUrl={this.setURL}
-            setSelectedTag={() => null}
-            forceExpanded={true}
-            noTagSelection={true}
-          />
-        </div>
-    } else {
-      imageComponent = <InputRow
-        disabled={true}
-        label="Target Image URL"
-        type="text"
-        width="100%"
-        value={this.props.actionConfig.image_repo_uri}
-        setValue={(x: string) => console.log(x)}
-      />
-    }
-
     return (
     return (
       <Holder>
       <Holder>
         <InputRow
         <InputRow
@@ -88,7 +63,15 @@ export default class ActionDetails extends Component<PropsType, StateType> {
           value={this.props.actionConfig.dockerfile_path}
           value={this.props.actionConfig.dockerfile_path}
           setValue={(x: string) => console.log(x)}
           setValue={(x: string) => console.log(x)}
         />
         />
-        {imageComponent}
+        <Label>Target Image URL</Label>
+        <ImageSelector
+          selectedTag="latest"
+          selectedImageUrl={this.props.actionConfig.image_repo_uri}
+          setSelectedImageUrl={this.setURL}
+          setSelectedTag={() => null}
+          forceExpanded={true}
+          noTagSelection={true}
+        />
       </Holder>
       </Holder>
     );
     );
   };
   };

+ 156 - 0
dashboard/src/components/values-form/InputArray.tsx

@@ -0,0 +1,156 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+type PropsType = {
+  label?: string;
+  values: string[];
+  setValues: (x: string[]) => void;
+  width?: string;
+};
+
+type StateType = {};
+
+export default class InputArray extends Component<PropsType, StateType> {
+  dict2arr = (dict: Record<string, any>) => {
+    let arr = [];
+    for (let key in dict) {
+      arr.push(`${key}: ${dict[key]}`);
+    }
+    return arr;
+  };
+
+  renderInputList = (values: string[]) => {
+    return (
+      <>
+        {values.map((value: string, i: number) => {
+          return (
+            <InputWrapper>
+              <Input
+                placeholder=""
+                width="270px"
+                value={value}
+                onChange={(e: any) => {
+                  let v = [...values];
+                  v[i] = e.target.value;
+                  this.props.setValues(v);
+                }}
+              />
+              <DeleteButton
+                onClick={() => {
+                  let v = [...values];
+                  v.splice(i, 1);
+                  this.props.setValues(v);
+                }}
+              >
+                <i className="material-icons">cancel</i>
+              </DeleteButton>
+            </InputWrapper>
+          );
+        })}
+      </>
+    );
+  };
+
+  render() {
+    let { values } = this.props;
+
+    if (!Array.isArray(values)) {
+      values = this.dict2arr(values);
+    }
+
+    return (
+      <StyledInputArray>
+        <Label>{this.props.label}</Label>
+        {values.length === 0 ? <></> : this.renderInputList(values)}
+        <AddRowButton
+          onClick={() => {
+            let v = [...values];
+            v.push("");
+            this.props.setValues(v);
+          }}
+        >
+          <i className="material-icons">add</i> Add Row
+        </AddRowButton>
+      </StyledInputArray>
+    );
+  }
+}
+
+const AddRowButton = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: 5px;
+  width: 270px;
+  font-size: 13px;
+  color: #aaaabb;
+  height: 30px;
+  border-radius: 3px;
+  cursor: pointer;
+  background: #ffffff11;
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: #ffffff44;
+    font-size: 16px;
+    margin-left: 8px;
+    margin-right: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+`;
+
+const DeleteButton = styled.div`
+  width: 15px;
+  height: 15px;
+  display: flex;
+  align-items: center;
+  margin-left: 8px;
+  margin-top: -3px;
+  justify-content: center;
+
+  > i {
+    font-size: 17px;
+    color: #ffffff44;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    :hover {
+      color: #ffffff88;
+    }
+  }
+`;
+
+const InputWrapper = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const Input = styled.input`
+  outline: none;
+  border: none;
+  margin-bottom: 5px;
+  font-size: 13px;
+  background: #ffffff11;
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  width: ${(props: { disabled?: boolean; width: string }) =>
+    props.width ? props.width : "270px"};
+  color: ${(props: { disabled?: boolean; width: string }) =>
+    props.disabled ? "#ffffff44" : "white"};
+  padding: 5px 10px;
+  height: 35px;
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+`;
+
+const StyledInputArray = styled.div`
+  margin-bottom: 15px;
+  margin-top: 22px;
+`;

+ 1 - 1
dashboard/src/components/values-form/InputRow.tsx

@@ -96,5 +96,5 @@ const Label = styled.div`
 
 
 const StyledInputRow = styled.div`
 const StyledInputRow = styled.div`
   margin-bottom: 15px;
   margin-bottom: 15px;
-  margin-top: 20px;
+  margin-top: 22px;
 `;
 `;

+ 185 - 0
dashboard/src/components/values-form/KeyValueArray.tsx

@@ -0,0 +1,185 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+type PropsType = {
+  label?: string;
+  values: any;
+  setValues: (x: any) => void;
+  width?: string;
+};
+
+type StateType = {
+  values: any[];
+};
+
+export default class KeyValueArray extends Component<PropsType, StateType> {
+  state = {
+    values: [] as any[],
+  };
+
+  componentDidMount() {
+    let arr = [] as any[];
+    Object.keys(this.props.values).forEach((key: string, i: number) => {
+      arr.push({ key, value: this.props.values[key] });
+    });
+    this.setState({ values: arr });
+  }
+
+  valuesToObject = () => {
+    let obj = {} as any;
+    this.state.values.forEach((entry: any, i: number) => {
+      obj[entry.key] = entry.value;
+    });
+    return obj;
+  };
+
+  renderInputList = () => {
+    return (
+      <>
+        {this.state.values.map((entry: any, i: number) => {
+          return (
+            <InputWrapper key={i}>
+              <Input
+                placeholder="ex: key"
+                width="270px"
+                value={entry.key}
+                onChange={(e: any) => {
+                  this.state.values[i].key = e.target.value;
+                  this.setState({ values: this.state.values });
+
+                  let obj = this.valuesToObject();
+                  this.props.setValues(obj);
+                }}
+              />
+              <Spacer />
+              <Input
+                placeholder="ex: value"
+                width="270px"
+                value={entry.value}
+                onChange={(e: any) => {
+                  this.state.values[i].value = e.target.value;
+                  this.setState({ values: this.state.values });
+
+                  let obj = this.valuesToObject();
+                  this.props.setValues(obj);
+                }}
+              />
+              <DeleteButton
+                onClick={() => {
+                  this.state.values.splice(i, 1);
+                  this.setState({ values: this.state.values });
+
+                  let obj = this.valuesToObject();
+                  this.props.setValues(obj);
+                }}
+              >
+                <i className="material-icons">cancel</i>
+              </DeleteButton>
+            </InputWrapper>
+          );
+        })}
+      </>
+    );
+  };
+
+  render() {
+    return (
+      <StyledInputArray>
+        <Label>{this.props.label}</Label>
+        {this.state.values.length === 0 ? <></> : this.renderInputList()}
+        <AddRowButton
+          onClick={() => {
+            this.state.values.push({ key: "", value: "" });
+            this.setState({ values: this.state.values });
+          }}
+        >
+          <i className="material-icons">add</i> Add Row
+        </AddRowButton>
+      </StyledInputArray>
+    );
+  }
+}
+
+const Spacer = styled.div`
+  width: 10px;
+  height: 20px;
+`;
+
+const AddRowButton = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: 5px;
+  width: 270px;
+  font-size: 13px;
+  color: #aaaabb;
+  height: 32px;
+  border-radius: 3px;
+  cursor: pointer;
+  background: #ffffff11;
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: #ffffff44;
+    font-size: 16px;
+    margin-left: 8px;
+    margin-right: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+`;
+
+const DeleteButton = styled.div`
+  width: 15px;
+  height: 15px;
+  display: flex;
+  align-items: center;
+  margin-left: 8px;
+  margin-top: -3px;
+  justify-content: center;
+
+  > i {
+    font-size: 17px;
+    color: #ffffff44;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    :hover {
+      color: #ffffff88;
+    }
+  }
+`;
+
+const InputWrapper = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const Input = styled.input`
+  outline: none;
+  border: none;
+  margin-bottom: 5px;
+  font-size: 13px;
+  background: #ffffff11;
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  width: ${(props: { disabled?: boolean; width: string }) =>
+    props.width ? props.width : "270px"};
+  color: ${(props: { disabled?: boolean; width: string }) =>
+    props.disabled ? "#ffffff44" : "white"};
+  padding: 5px 10px;
+  height: 35px;
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+`;
+
+const StyledInputArray = styled.div`
+  margin-bottom: 15px;
+  margin-top: 22px;
+`;

+ 6 - 11
dashboard/src/components/values-form/RangeSlider.tsx

@@ -1,26 +1,21 @@
 import React, { ChangeEvent, Component } from "react";
 import React, { ChangeEvent, Component } from "react";
-import Slider from '@material-ui/core/Slider';
+import Slider from "@material-ui/core/Slider";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
-type PropsType = {
-};
+type PropsType = {};
 
 
-type StateType = {
-};
+type StateType = {};
 
 
 export default class RangeSelector extends Component<PropsType, StateType> {
 export default class RangeSelector extends Component<PropsType, StateType> {
-  state = {
-  };
+  state = {};
 
 
   render() {
   render() {
     return (
     return (
       <StyledInputRow>
       <StyledInputRow>
-        <Label>
-          asdfasdf
-        </Label>
+        <Label>asdfasdf</Label>
         <Slider
         <Slider
           value={12}
           value={12}
-          onChange={() => console.log('huh')}
+          onChange={() => console.log("huh")}
           valueLabelDisplay="auto"
           valueLabelDisplay="auto"
           aria-labelledby="range-slider"
           aria-labelledby="range-slider"
         />
         />

+ 35 - 7
dashboard/src/components/values-form/ValuesForm.tsx

@@ -12,6 +12,8 @@ import Helper from "./Helper";
 import Heading from "./Heading";
 import Heading from "./Heading";
 import ExpandableResource from "../ExpandableResource";
 import ExpandableResource from "../ExpandableResource";
 import VeleroForm from "../forms/VeleroForm";
 import VeleroForm from "../forms/VeleroForm";
+import InputArray from "./InputArray";
+import KeyValueArray from "./KeyValueArray";
 
 
 type PropsType = {
 type PropsType = {
   sections?: Section[];
   sections?: Section[];
@@ -21,11 +23,12 @@ type PropsType = {
 
 
 type StateType = any;
 type StateType = any;
 
 
+// Requires an internal representation unlike other values components because metaState value underdetermines input order
 export default class ValuesForm extends Component<PropsType, StateType> {
 export default class ValuesForm extends Component<PropsType, StateType> {
   getInputValue = (item: FormElement) => {
   getInputValue = (item: FormElement) => {
     let key = item.name || item.variable;
     let key = item.name || item.variable;
     let value = this.props.metaState[key];
     let value = this.props.metaState[key];
-    
+
     if (item.settings && item.settings.unit && value && value.includes) {
     if (item.settings && item.settings.unit && value && value.includes) {
       value = value.split(item.settings.unit)[0];
       value = value.split(item.settings.unit)[0];
     }
     }
@@ -69,7 +72,28 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               label={item.label}
               label={item.label}
             />
             />
           );
           );
+        case "key-value-array":
+          return (
+            <KeyValueArray
+              values={this.props.metaState[key]}
+              setValues={(x: any) => {
+                this.props.setMetaState({ [key]: x });
+              }}
+              label={item.label}
+            />
+          );
         case "array-input":
         case "array-input":
+          return (
+            <InputArray
+              key={i}
+              values={this.props.metaState[key]}
+              setValues={(x: string[]) => {
+                this.props.setMetaState({ [key]: x });
+              }}
+              label={item.label}
+            />
+          );
+        case "string-input":
           return (
           return (
             <InputRow
             <InputRow
               key={i}
               key={i}
@@ -77,20 +101,24 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               type="text"
               type="text"
               value={this.getInputValue(item)}
               value={this.getInputValue(item)}
               setValue={(x: string) => {
               setValue={(x: string) => {
-                this.props.setMetaState({ [key]: [x] });
+                if (item.settings && item.settings.unit && x !== "") {
+                  x = x + item.settings.unit;
+                }
+                this.props.setMetaState({ [key]: x });
               }}
               }}
               label={item.label}
               label={item.label}
               unit={item.settings ? item.settings.unit : null}
               unit={item.settings ? item.settings.unit : null}
             />
             />
           );
           );
-        case "string-input":
+        case "string-input-password":
           return (
           return (
             <InputRow
             <InputRow
               key={i}
               key={i}
               isRequired={item.required}
               isRequired={item.required}
-              type="text"
+              type="password"
               value={this.getInputValue(item)}
               value={this.getInputValue(item)}
               setValue={(x: string) => {
               setValue={(x: string) => {
+                console.log("string input", x);
                 if (item.settings && item.settings.unit && x !== "") {
                 if (item.settings && item.settings.unit && x !== "") {
                   x = x + item.settings.unit;
                   x = x + item.settings.unit;
                 }
                 }
@@ -143,9 +171,9 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               value={this.props.metaState[key]}
               value={this.props.metaState[key]}
               setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
               setActiveValue={(val) => this.props.setMetaState({ [key]: val })}
               options={[
               options={[
-                { value: 'aws', label: 'Amazon Web Services (AWS)' },
-                { value: 'gcp', label: 'Google Cloud Platform (GCP)' },
-                { value: 'do', label: 'DigitalOcean' },
+                { value: "aws", label: "Amazon Web Services (AWS)" },
+                { value: "gcp", label: "Google Cloud Platform (GCP)" },
+                { value: "do", label: "DigitalOcean" },
               ]}
               ]}
               dropdownLabel=""
               dropdownLabel=""
               label={item.label}
               label={item.label}

+ 9 - 1
dashboard/src/components/values-form/ValuesWrapper.tsx

@@ -37,7 +37,10 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
           section.contents.forEach((item: FormElement, i: number) => {
           section.contents.forEach((item: FormElement, i: number) => {
             // If no name is assigned use values.yaml variable as identifier
             // If no name is assigned use values.yaml variable as identifier
             let key = item.name || item.variable;
             let key = item.name || item.variable;
-            let def = item.settings && item.settings.unit ? `${item.settings.default}${item.settings.unit}` : item.settings.default
+            let def =
+              item.settings && item.settings.unit
+                ? `${item.settings.default}${item.settings.unit}`
+                : item.settings.default;
             def = (item.value && item.value[0]) || def;
             def = (item.value && item.value[0]) || def;
 
 
             // Handle add to list of required fields
             // Handle add to list of required fields
@@ -52,9 +55,14 @@ export default class ValuesWrapper extends Component<PropsType, StateType> {
               case "string-input":
               case "string-input":
                 metaState[key] = def ? def : "";
                 metaState[key] = def ? def : "";
                 break;
                 break;
+              case "string-input-password":
+                metaState[key] = def ? def : item.settings.default;
               case "array-input":
               case "array-input":
                 metaState[key] = def ? def : [];
                 metaState[key] = def ? def : [];
                 break;
                 break;
+              case "key-value-array":
+                metaState[key] = def ? def : {};
+                break;
               case "number-input":
               case "number-input":
                 metaState[key] = def.toString() ? def : "";
                 metaState[key] = def.toString() ? def : "";
                 break;
                 break;

+ 40 - 26
dashboard/src/index.html

@@ -3,31 +3,45 @@
   <head>
   <head>
     <title>Porter | Dashboard</title>
     <title>Porter | Dashboard</title>
 
 
-    <link rel="icon" href="https://i.ibb.co/Xy0QK6P/dsquare.png">
-    <meta name="description" content="Fully-managed remote dev environments for any team." />
-    <meta property='og:title' content='Porter' />
-    <meta property='og:image' content='https://i.ibb.co/DL4695L/logo-wide.png' />
-    <meta property='og:description' content='Fully-managed remote dev environments for any team.' />
-    <meta property='og:url' content='https://getporter.dev' />
+    <link rel="icon" href="https://i.ibb.co/Xy0QK6P/dsquare.png" />
+    <meta
+      name="description"
+      content="Fully-managed remote dev environments for any team."
+    />
+    <meta property="og:title" content="Porter" />
+    <meta
+      property="og:image"
+      content="https://i.ibb.co/DL4695L/logo-wide.png"
+    />
+    <meta
+      property="og:description"
+      content="Fully-managed remote dev environments for any team."
+    />
+    <meta property="og:url" content="https://getporter.dev" />
 
 
-    <link rel="icon" href="https://i.ibb.co/Xy0QK6P/dsquare.png">
-    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
-    <link href="https://fonts.googleapis.com/css?family=Assistant:400,700|Noto+Sans:400,600,700|Work+Sans:400,500,600|Source+Sans+Pro:400,600,700|Hind+Siliguri:500|Cabin:400,600" rel="stylesheet">
-  	<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
-  	<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0/katex.min.css" rel="stylesheet" />
-    <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
+    <link rel="icon" href="https://i.ibb.co/Xy0QK6P/dsquare.png" />
+    <link
+      href="https://fonts.googleapis.com/icon?family=Material+Icons"
+      rel="stylesheet"
+    />
+    <link
+      href="https://fonts.googleapis.com/css?family=Assistant:400,700|Noto+Sans:400,600,700|Work+Sans:400,500,600|Source+Sans+Pro:400,600,700|Hind+Siliguri:500|Cabin:400,600"
+      rel="stylesheet"
+    />
+    <link
+      href="https://fonts.googleapis.com/icon?family=Material+Icons"
+      rel="stylesheet"
+    />
+    <link
+      href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0/katex.min.css"
+      rel="stylesheet"
+    />
+    <link
+      href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined"
+      rel="stylesheet"
+    />
   </head>
   </head>
-<body>
-  <div id="output"></div>
-  <script>
-    window.intercomSettings = {
-      app_id: "gq56g49i"
-    };
-  </script>
-
-  <script>
-  // We pre-filled your app ID in the widget URL: 'https://widget.intercom.io/widget/gq56g49i'
-  (function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/gq56g49i';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
-  </script>
-</body>
-</html>
+  <body>
+    <div id="output"></div>
+  </body>
+</html>

+ 2 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -223,6 +223,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
 
 
     // Convert dotted keys to nested objects
     // Convert dotted keys to nested objects
     let values = {};
     let values = {};
+
     for (let key in rawValues) {
     for (let key in rawValues) {
       _.set(values, key, rawValues[key]);
       _.set(values, key, rawValues[key]);
     }
     }
@@ -368,7 +369,7 @@ export default class ExpandedChart extends Component<PropsType, StateType> {
     if (this.state.devOpsMode) {
     if (this.state.devOpsMode) {
       tabOptions.push(
       tabOptions.push(
         { label: "Manifests", value: "list" },
         { label: "Manifests", value: "list" },
-        { label: "Raw Values", value: "values" }
+        { label: "Helm Values", value: "values" }
       );
       );
     }
     }
 
 

+ 8 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -145,6 +145,10 @@ export default class SettingsSection extends Component<PropsType, StateType> {
     </Helper>
     </Helper>
   */
   */
   renderSourceSection = () => {
   renderSourceSection = () => {
+    if (!this.props.currentChart.form.hasSource) {
+      return;
+    }
+
     if (this.state.action.git_repo.length > 0) {
     if (this.state.action.git_repo.length > 0) {
       return (
       return (
         <>
         <>
@@ -231,6 +235,10 @@ export default class SettingsSection extends Component<PropsType, StateType> {
   };
   };
 
 
   renderWebhookSection = () => {
   renderWebhookSection = () => {
+    if (!this.props.currentChart.form.hasSource) {
+      return;
+    }
+
     if (true || this.state.webhookToken) {
     if (true || this.state.webhookToken) {
       let webhookText = `curl -X POST 'https://dashboard.getporter.dev/api/webhooks/deploy/${this.state.webhookToken}?commit=???&repository=???'`;
       let webhookText = `curl -X POST 'https://dashboard.getporter.dev/api/webhooks/deploy/${this.state.webhookToken}?commit=???&repository=???'`;
       return (
       return (

+ 3 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/graph/Node.tsx

@@ -38,11 +38,9 @@ export default class Node extends Component<PropsType, StateType> {
         h={Math.round(h)}
         h={Math.round(h)}
       >
       >
         <Kind>
         <Kind>
-          <StyledMark>
-            {this.props.showKindLabels ? kind : null}
-          </StyledMark>
+          <StyledMark>{this.props.showKindLabels ? kind : null}</StyledMark>
         </Kind>
         </Kind>
-        <NodeBlock 
+        <NodeBlock
           onMouseDown={nodeMouseDown}
           onMouseDown={nodeMouseDown}
           onMouseUp={nodeMouseUp}
           onMouseUp={nodeMouseUp}
           onMouseEnter={() => this.props.setCurrentNode(this.props.node)}
           onMouseEnter={() => this.props.setCurrentNode(this.props.node)}
@@ -53,9 +51,7 @@ export default class Node extends Component<PropsType, StateType> {
           <i className="material-icons">{icon}</i>
           <i className="material-icons">{icon}</i>
         </NodeBlock>
         </NodeBlock>
         <NodeLabel>
         <NodeLabel>
-          <StyledMark>
-            {name}
-          </StyledMark>
+          <StyledMark>{name}</StyledMark>
         </NodeLabel>
         </NodeLabel>
       </StyledNode>
       </StyledNode>
     );
     );

+ 13 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/ControllerTab.tsx

@@ -73,6 +73,11 @@ export default class ControllerTab extends Component<PropsType, StateType> {
         this.setState({ pods, raw: res.data, showTooltip });
         this.setState({ pods, raw: res.data, showTooltip });
 
 
         if (isFirst) {
         if (isFirst) {
+          let pod = res.data[0];
+          let status = this.getPodStatus(pod.status);
+          status === "failed" &&
+            pod.status?.message &&
+            this.props.setPodError(pod.status?.message);
           selectPod(res.data[0]);
           selectPod(res.data[0]);
         }
         }
       })
       })
@@ -104,11 +109,14 @@ export default class ControllerTab extends Component<PropsType, StateType> {
   };
   };
 
 
   getPodStatus = (status: any) => {
   getPodStatus = (status: any) => {
-    if (status?.phase === "Pending" && status?.containerStatuses !== undefined) {
+    if (
+      status?.phase === "Pending" &&
+      status?.containerStatuses !== undefined
+    ) {
       return status.containerStatuses[0].state.waiting.reason;
       return status.containerStatuses[0].state.waiting.reason;
       // return 'waiting'
       // return 'waiting'
     } else if (status?.phase === "Pending") {
     } else if (status?.phase === "Pending") {
-      return "Pending"
+      return "Pending";
     }
     }
 
 
     if (status?.phase === "Failed") {
     if (status?.phase === "Failed") {
@@ -155,7 +163,9 @@ export default class ControllerTab extends Component<PropsType, StateType> {
               selected={selectedPod?.metadata?.name === pod?.metadata?.name}
               selected={selectedPod?.metadata?.name === pod?.metadata?.name}
               onClick={() => {
               onClick={() => {
                 this.props.setPodError("");
                 this.props.setPodError("");
-                (status === "failed" && pod.status?.message) && this.props.setPodError(pod.status?.message);
+                status === "failed" &&
+                  pod.status?.message &&
+                  this.props.setPodError(pod.status?.message);
                 selectPod(pod);
                 selectPod(pod);
               }}
               }}
             >
             >

+ 5 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -41,7 +41,11 @@ export default class Logs extends Component<PropsType, StateType> {
       return <Message>Please select a pod to view its logs.</Message>;
       return <Message>Please select a pod to view its logs.</Message>;
     }
     }
     if (this.state.logs.length == 0) {
     if (this.state.logs.length == 0) {
-      return <Message>{this.props.podError || "No logs to display from this pod."}</Message>;
+      return (
+        <Message>
+          {this.props.podError || "No logs to display from this pod."}
+        </Message>
+      );
     }
     }
     return this.state.logs.map((log, i) => {
     return this.state.logs.map((log, i) => {
       return <Log key={i}>{log}</Log>;
       return <Log key={i}>{log}</Log>;

+ 89 - 74
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -1,31 +1,31 @@
 import React, { Component } from "react";
 import React, { Component } from "react";
 import styled from "styled-components";
 import styled from "styled-components";
 
 
-import { Context } from '../../../shared/Context';
-import { integrationList } from '../../../shared/common';
-import { ImageType, ActionConfigType } from '../../..//shared/types';
-import ImageList from '../../../components/image-selector/ImageList';
-import RepoList from '../../../components/repo-selector/RepoList';
+import { Context } from "../../../shared/Context";
+import { integrationList } from "../../../shared/common";
+import { ImageType, ActionConfigType } from "../../..//shared/types";
+import ImageList from "../../../components/image-selector/ImageList";
+import RepoList from "../../../components/repo-selector/RepoList";
 
 
 type PropsType = {
 type PropsType = {
-  setCurrent: (x: any) => void,
-  currentCategory: string,
-  integrations: string[],
-  itemIdentifier?: any[],
-  titles?: string[],
-  isCategory?: boolean
+  setCurrent: (x: any) => void;
+  currentCategory: string;
+  integrations: string[];
+  itemIdentifier?: any[];
+  titles?: string[];
+  isCategory?: boolean;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  displayImages: boolean[],
-  allCollapsed: boolean,
+  displayImages: boolean[];
+  allCollapsed: boolean;
 };
 };
 
 
 export default class IntegrationList extends Component<PropsType, StateType> {
 export default class IntegrationList extends Component<PropsType, StateType> {
   state = {
   state = {
     displayImages: [] as boolean[],
     displayImages: [] as boolean[],
     allCollapsed: false,
     allCollapsed: false,
-  }
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     let x: boolean[] = [];
     let x: boolean[] = [];
@@ -54,7 +54,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
       x.push(false);
       x.push(false);
     }
     }
     this.setState({ displayImages: x, allCollapsed: true });
     this.setState({ displayImages: x, allCollapsed: true });
-  }
+  };
 
 
   expandAll = () => {
   expandAll = () => {
     let x = [];
     let x = [];
@@ -62,7 +62,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
       x.push(true);
       x.push(true);
     }
     }
     this.setState({ displayImages: x, allCollapsed: false });
     this.setState({ displayImages: x, allCollapsed: false });
-  }
+  };
 
 
   toggleDisplay = (event: any, index: number) => {
   toggleDisplay = (event: any, index: number) => {
     event.stopPropagation();
     event.stopPropagation();
@@ -75,7 +75,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
       for (let i = 0; i < x.length; i++) {
       for (let i = 0; i < x.length; i++) {
         if (x[i]) {
         if (x[i]) {
           collapsed = false;
           collapsed = false;
-          break
+          break;
         }
         }
       }
       }
       if (collapsed) {
       if (collapsed) {
@@ -85,14 +85,20 @@ export default class IntegrationList extends Component<PropsType, StateType> {
       }
       }
     }
     }
     this.setState({ displayImages: x });
     this.setState({ displayImages: x });
-  }
+  };
 
 
   handleParent = (event: any, integration: string) => {
   handleParent = (event: any, integration: string) => {
     this.props.setCurrent(integration);
     this.props.setCurrent(integration);
-  }
+  };
 
 
   renderContents = () => {
   renderContents = () => {
-    let { integrations, titles, setCurrent, isCategory, currentCategory } = this.props;
+    let {
+      integrations,
+      titles,
+      setCurrent,
+      isCategory,
+      currentCategory,
+    } = this.props;
     if (titles && titles.length > 0) {
     if (titles && titles.length > 0) {
       return integrations.map((integration: string, i: number) => {
       return integrations.map((integration: string, i: number) => {
         let icon =
         let icon =
@@ -101,11 +107,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
           integrationList[integration] && integrationList[integration].label;
           integrationList[integration] && integrationList[integration].label;
         let label = titles[i];
         let label = titles[i];
         return (
         return (
-          <Integration
-            key={i}
-            isCategory={isCategory}
-            disabled={false}
-          >
+          <Integration key={i} isCategory={isCategory} disabled={false}>
             <MainRow
             <MainRow
               onClick={(e: any) => {
               onClick={(e: any) => {
                 this.handleParent(e, integration);
                 this.handleParent(e, integration);
@@ -120,10 +122,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
                   <Subtitle>{subtitle}</Subtitle>
                   <Subtitle>{subtitle}</Subtitle>
                 </Description>
                 </Description>
               </Flex>
               </Flex>
-              <MaterialIconTray
-                isCategory={isCategory}
-                disabled={false}
-              >
+              <MaterialIconTray isCategory={isCategory} disabled={false}>
                 <i className="material-icons">more_vert</i>
                 <i className="material-icons">more_vert</i>
                 <I
                 <I
                   className="material-icons"
                   className="material-icons"
@@ -132,16 +131,13 @@ export default class IntegrationList extends Component<PropsType, StateType> {
                     this.toggleDisplay(e, i);
                     this.toggleDisplay(e, i);
                   }}
                   }}
                 >
                 >
-                  {isCategory ? 'launch' : 'expand_more'}
+                  {isCategory ? "launch" : "expand_more"}
                 </I>
                 </I>
               </MaterialIconTray>
               </MaterialIconTray>
             </MainRow>
             </MainRow>
-            {this.state.displayImages[i] &&
-              <ImageHodler
-                adjustMargin={currentCategory !== 'repo'}
-              >
-                {currentCategory !== 'repo'
-                  ?
+            {this.state.displayImages[i] && (
+              <ImageHodler adjustMargin={currentCategory !== "repo"}>
+                {currentCategory !== "repo" ? (
                   <ImageList
                   <ImageList
                     selectedImageUrl={null}
                     selectedImageUrl={null}
                     selectedTag={null}
                     selectedTag={null}
@@ -151,29 +147,33 @@ export default class IntegrationList extends Component<PropsType, StateType> {
                     setSelectedTag={(x: string) => {}}
                     setSelectedTag={(x: string) => {}}
                     setClickedImage={(x: ImageType) => {}}
                     setClickedImage={(x: ImageType) => {}}
                   />
                   />
-                  :
+                ) : (
                   <RepoList
                   <RepoList
-                    actionConfig={{
-                      git_repo: '',
-                      image_repo_uri: '',
-                      git_repo_id: 0,
-                      dockerfile_path: '',
-                    } as ActionConfigType}
+                    actionConfig={
+                      {
+                        git_repo: "",
+                        image_repo_uri: "",
+                        git_repo_id: 0,
+                        dockerfile_path: "",
+                      } as ActionConfigType
+                    }
                     setActionConfig={(x: ActionConfigType) => {}}
                     setActionConfig={(x: ActionConfigType) => {}}
                     readOnly={true}
                     readOnly={true}
                     userId={this.props.itemIdentifier[i]}
                     userId={this.props.itemIdentifier[i]}
                   />
                   />
-                }
+                )}
               </ImageHodler>
               </ImageHodler>
-            }
+            )}
           </Integration>
           </Integration>
         );
         );
       });
       });
     } else if (integrations && integrations.length > 0) {
     } else if (integrations && integrations.length > 0) {
       return integrations.map((integration: string, i: number) => {
       return integrations.map((integration: string, i: number) => {
-        let icon = integrationList[integration] && integrationList[integration].icon;
-        let label = integrationList[integration] && integrationList[integration].label;
-        let disabled = integration === 'kubernetes';
+        let icon =
+          integrationList[integration] && integrationList[integration].icon;
+        let label =
+          integrationList[integration] && integrationList[integration].label;
+        let disabled = integration === "kubernetes";
         return (
         return (
           <Integration
           <Integration
             key={i}
             key={i}
@@ -181,15 +181,14 @@ export default class IntegrationList extends Component<PropsType, StateType> {
             isCategory={isCategory}
             isCategory={isCategory}
             disabled={disabled}
             disabled={disabled}
           >
           >
-            <MainRow
-              isCategory={isCategory}
-              disabled={disabled}
-            >
+            <MainRow isCategory={isCategory} disabled={disabled}>
               <Flex>
               <Flex>
                 <Icon src={icon && icon} />
                 <Icon src={icon && icon} />
                 <Label>{label}</Label>
                 <Label>{label}</Label>
               </Flex>
               </Flex>
-              <i className="material-icons">{isCategory ? 'launch' : 'more_vert'}</i>
+              <i className="material-icons">
+                {isCategory ? "launch" : "more_vert"}
+              </i>
             </MainRow>
             </MainRow>
           </Integration>
           </Integration>
         );
         );
@@ -199,26 +198,31 @@ export default class IntegrationList extends Component<PropsType, StateType> {
   };
   };
 
 
   render() {
   render() {
-    return ( 
+    return (
       <StyledIntegrationList>
       <StyledIntegrationList>
-        {(this.props.titles && this.props.titles.length > 0) &&
+        {this.props.titles && this.props.titles.length > 0 && (
           <ControlRow>
           <ControlRow>
             <Button
             <Button
               onClick={() => {
               onClick={() => {
                 if (this.state.allCollapsed) {
                 if (this.state.allCollapsed) {
-                  this.expandAll()
+                  this.expandAll();
                 } else {
                 } else {
-                  this.collapseAll()
+                  this.collapseAll();
                 }
                 }
               }}
               }}
             >
             >
-              {this.state.allCollapsed
-                ? <><i className="material-icons">expand_more</i> Expand All</>
-                : <><i className="material-icons">expand_less</i> Collapse All</>
-              }
+              {this.state.allCollapsed ? (
+                <>
+                  <i className="material-icons">expand_more</i> Expand All
+                </>
+              ) : (
+                <>
+                  <i className="material-icons">expand_less</i> Collapse All
+                </>
+              )}
             </Button>
             </Button>
           </ControlRow>
           </ControlRow>
-        }
+        )}
         {this.renderContents()}
         {this.renderContents()}
       </StyledIntegrationList>
       </StyledIntegrationList>
     );
     );
@@ -236,7 +240,8 @@ const Flex = styled.div`
 const ImageHodler = styled.div`
 const ImageHodler = styled.div`
   width: 100%;
   width: 100%;
   padding: 12px;
   padding: 12px;
-  margin-top: ${(props: {adjustMargin: boolean}) => props.adjustMargin ? '-10px' : '0px'};
+  margin-top: ${(props: { adjustMargin: boolean }) =>
+    props.adjustMargin ? "-10px" : "0px"};
 `;
 `;
 
 
 const MaterialIconTray = styled.div`
 const MaterialIconTray = styled.div`
@@ -250,9 +255,11 @@ const MaterialIconTray = styled.div`
     border-radius: 20px;
     border-radius: 20px;
     font-size: 18px;
     font-size: 18px;
     padding: 5px;
     padding: 5px;
-    color: ${(props: { isCategory: boolean, disabled: boolean }) => props.isCategory ? '#616feecc' : '#ffffff44'};
+    color: ${(props: { isCategory: boolean; disabled: boolean }) =>
+      props.isCategory ? "#616feecc" : "#ffffff44"};
     :hover {
     :hover {
-      background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+      background: ${(props: { isCategory: boolean; disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
     }
     }
   }
   }
 `;
 `;
@@ -266,9 +273,11 @@ const MainRow = styled.div`
   padding: 25px;
   padding: 25px;
   border-radius: 5px;
   border-radius: 5px;
   :hover {
   :hover {
-    background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+    background: ${(props: { isCategory: boolean; disabled: boolean }) =>
+      props.disabled ? "" : "#ffffff11"};
     > i {
     > i {
-      background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11' };
+      background: ${(props: { isCategory: boolean; disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
     }
     }
   }
   }
 
 
@@ -280,7 +289,8 @@ const MainRow = styled.div`
       props.isCategory ? "#616feecc" : "#ffffff44"};
       props.isCategory ? "#616feecc" : "#ffffff44"};
     margin-right: -7px;
     margin-right: -7px;
     :hover {
     :hover {
-      background: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? '' : '#ffffff11'};
+      background: ${(props: { isCategory: boolean; disabled: boolean }) =>
+        props.disabled ? "" : "#ffffff11"};
     }
     }
   }
   }
 `;
 `;
@@ -290,7 +300,8 @@ const Integration = styled.div`
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   background: #26282f;
   background: #26282f;
-  cursor: ${(props: { isCategory: boolean, disabled: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  cursor: ${(props: { isCategory: boolean; disabled: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
   margin-bottom: 15px;
   margin-bottom: 15px;
   border-radius: 5px;
   border-radius: 5px;
   box-shadow: 0 5px 8px 0px #00000033;
   box-shadow: 0 5px 8px 0px #00000033;
@@ -341,7 +352,8 @@ const StyledIntegrationList = styled.div`
 `;
 `;
 
 
 const I = styled.i`
 const I = styled.i`
-  transform: ${(props: { showList: boolean }) => props.showList ? 'rotate(180deg)' : ''};
+  transform: ${(props: { showList: boolean }) =>
+    props.showList ? "rotate(180deg)" : ""};
 `;
 `;
 
 
 const ControlRow = styled.div`
 const ControlRow = styled.div`
@@ -368,7 +380,7 @@ const Button = styled.div`
   justify-content: space-between;
   justify-content: space-between;
   font-size: 13px;
   font-size: 13px;
   cursor: pointer;
   cursor: pointer;
-  font-family: 'Work Sans', sans-serif;
+  font-family: "Work Sans", sans-serif;
   border-radius: 8px;
   border-radius: 8px;
   color: white;
   color: white;
   height: 35px;
   height: 35px;
@@ -381,11 +393,14 @@ const Button = styled.div`
   white-space: nowrap;
   white-space: nowrap;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
   box-shadow: 0 5px 8px 0px #00000010;
   box-shadow: 0 5px 8px 0px #00000010;
-  cursor: ${(props: { disabled?: boolean }) => props.disabled ? 'not-allowed' : 'pointer'};
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
 
 
-  background: ${(props: { disabled?: boolean }) => props.disabled ? '#aaaabbee' : '#616FEEcc'};
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
   :hover {
   :hover {
-    background: ${(props: { disabled?: boolean }) => props.disabled ? '' : '#505edddd'};
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
   }
   }
 
 
   > i {
   > i {

+ 2 - 2
dashboard/src/main/home/launch/Launch.tsx

@@ -13,8 +13,8 @@ import hardcodedNames from "./hardcodedNameDict";
 import { Link } from "react-router-dom";
 import { Link } from "react-router-dom";
 
 
 const tabOptions = [
 const tabOptions = [
-  { label: "Launch service", value: "docker" },
-  { label: "Community Templates", value: "community" },
+  { label: "New Application", value: "docker" },
+  { label: "Community Add-ons", value: "community" },
 ];
 ];
 
 
 type PropsType = {};
 type PropsType = {};

+ 1 - 1
dashboard/src/main/home/launch/expanded-template/ExpandedTemplate.tsx

@@ -129,7 +129,7 @@ const LoadingWrapper = styled.div`
 `;
 `;
 
 
 const StyledExpandedTemplate = styled.div`
 const StyledExpandedTemplate = styled.div`
-  width: calc(90% - 150px);
+  width: 100%;
   min-width: 300px;
   min-width: 300px;
   padding-top: 30px;
   padding-top: 30px;
 `;
 `;

+ 160 - 97
dashboard/src/main/home/launch/expanded-template/LaunchTemplate.tsx

@@ -103,7 +103,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
   };
   };
 
 
   onSubmitAddon = (wildcard?: any) => {
   onSubmitAddon = (wildcard?: any) => {
-    let { currentCluster, currentProject } = this.context;
+    let { currentCluster, currentProject, setCurrentError } = this.context;
     let name =
     let name =
       this.state.templateName || randomWords({ exactly: 3, join: "-" });
       this.state.templateName || randomWords({ exactly: 3, join: "-" });
     this.setState({ saveValuesStatus: "loading" });
     this.setState({ saveValuesStatus: "loading" });
@@ -131,8 +131,6 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         }
         }
       )
       )
       .then((_) => {
       .then((_) => {
-        console.log("ST");
-        console.log(this.state.sourceType);
         if (this.state.sourceType === "repo") {
         if (this.state.sourceType === "repo") {
           this.createGHAction(name, this.state.selectedNamespace);
           this.createGHAction(name, this.state.selectedNamespace);
         }
         }
@@ -148,6 +146,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
       })
       })
       .catch((err) => {
       .catch((err) => {
         this.setState({ saveValuesStatus: "error" });
         this.setState({ saveValuesStatus: "error" });
+        setCurrentError(err.response.data.errors[0]);
         posthog.capture("Failed to deploy template", {
         posthog.capture("Failed to deploy template", {
           name: this.props.currentTemplate.name,
           name: this.props.currentTemplate.name,
           namespace: this.state.selectedNamespace,
           namespace: this.state.selectedNamespace,
@@ -181,12 +180,32 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     }
     }
 
 
     if (this.state.sourceType === "repo") {
     if (this.state.sourceType === "repo") {
-      imageUrl = "hello-world";
+      imageUrl = "porterdev/hello-porter";
       tag = "latest";
       tag = "latest";
     }
     }
 
 
-    _.set(values, "image.repository", imageUrl);
-    _.set(values, "image.tag", tag);
+    let provider;
+    switch (currentCluster.service) {
+      case "eks":
+        provider = "aws";
+        break;
+      case "gke":
+        provider = "gcp";
+        break;
+      case "doks":
+        provider = "digitalocean";
+        break;
+      default:
+        provider = null;
+    }
+
+    // don't overwrite for templates that already have a source (i.e. non-Docker templates)
+    if (imageUrl && tag) {
+      _.set(values, "image.repository", imageUrl);
+      _.set(values, "image.tag", tag);
+    }
+
+    _.set(values, "ingress.provider", provider);
 
 
     console.log(`
     console.log(`
       ${this.props.currentTemplate.name}\n
       ${this.props.currentTemplate.name}\n
@@ -289,7 +308,6 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
     if (this.props.currentTemplate.name !== "docker") {
     if (this.props.currentTemplate.name !== "docker") {
       this.setState({ saveValuesStatus: "" });
       this.setState({ saveValuesStatus: "" });
     }
     }
-
     // Retrieve tab options
     // Retrieve tab options
     let tabOptions = [] as ChoiceType[];
     let tabOptions = [] as ChoiceType[];
     this.props.form?.tabs.map((tab: any, i: number) => {
     this.props.form?.tabs.map((tab: any, i: number) => {
@@ -297,7 +315,11 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
         tabOptions.push({ value: tab.name, label: tab.label });
         tabOptions.push({ value: tab.name, label: tab.label });
       }
       }
     });
     });
-    this.setState({ tabOptions, currentTab: tabOptions[0]["value"] });
+
+    this.setState({
+      tabOptions,
+      currentTab: tabOptions[0] && tabOptions[0]["value"],
+    });
 
 
     // TODO: query with selected filter once implemented
     // TODO: query with selected filter once implemented
     let { currentProject, currentCluster } = this.context;
     let { currentProject, currentCluster } = this.context;
@@ -406,65 +428,67 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 
   // Display if current template uses source (image or repo)
   // Display if current template uses source (image or repo)
   renderSourceSelectorContent = () => {
   renderSourceSelectorContent = () => {
-    if (this.props.form?.hasSource) {
-      if (this.state.sourceType === "registry") {
-        return (
-          <>
-            <Subtitle>
-              Select the container image you would like to connect to this
-              template
-              {/* <Highlight onClick={() => this.setState({ sourceType: "repo" })}>
-                link a git repository
-              </Highlight> */}
-              .<Required>*</Required>
-            </Subtitle>
-            <DarkMatter />
-            <ImageSelector
-              selectedTag={this.state.selectedTag}
-              selectedImageUrl={this.state.selectedImageUrl}
-              setSelectedImageUrl={this.setSelectedImageUrl}
-              setSelectedTag={(x: string) => this.setState({ selectedTag: x })}
-              forceExpanded={true}
-            />
-            <br />
-          </>
-        );
-      } else {
-        return (
-          <>
-            <Subtitle>
-              Select a repo to connect to, then a Dockerfile to build from.
-              <Required>*</Required>
-            </Subtitle>
-            <ActionConfEditor
-              actionConfig={this.state.actionConfig}
-              branch={this.state.branch}
-              pathIsSet={this.state.pathIsSet}
-              setActionConfig={(actionConfig: ActionConfigType) =>
-                this.setState({ actionConfig }, () => {
-                  this.setSelectedImageUrl(
-                    this.state.actionConfig.image_repo_uri
-                  );
-                })
-              }
-              setBranch={(branch: string) => this.setState({ branch })}
-              setPath={(pathIsSet: boolean) => this.setState({ pathIsSet })}
-              reset={() => {
-                this.setState({
-                  actionConfig: { ...defaultActionConfig },
-                  branch: "",
-                  pathIsSet: false,
-                });
-              }}
-            />
-            <br />
-          </>
-        );
-      }
+    if (this.state.sourceType === "registry") {
+      return (
+        <>
+          <Subtitle>
+            Select the container image you would like to connect to this
+            template
+            {/* <Highlight onClick={() => this.setState({ sourceType: "repo" })}>
+              link a git repository
+            </Highlight> */}
+            .<Required>*</Required>
+          </Subtitle>
+          <DarkMatter />
+          <ImageSelector
+            selectedTag={this.state.selectedTag}
+            selectedImageUrl={this.state.selectedImageUrl}
+            setSelectedImageUrl={this.setSelectedImageUrl}
+            setSelectedTag={(x: string) => this.setState({ selectedTag: x })}
+            forceExpanded={true}
+          />
+          <br />
+        </>
+      );
+    } else {
+      return (
+        <>
+          <Subtitle>
+            Select a repo to connect to, then a Dockerfile to build from.
+            <Required>*</Required>
+          </Subtitle>
+          <ActionConfEditor
+            actionConfig={this.state.actionConfig}
+            branch={this.state.branch}
+            pathIsSet={this.state.pathIsSet}
+            setActionConfig={(actionConfig: ActionConfigType) =>
+              this.setState({ actionConfig }, () => {
+                this.setSelectedImageUrl(
+                  this.state.actionConfig.image_repo_uri
+                );
+              })
+            }
+            setBranch={(branch: string) => this.setState({ branch })}
+            setPath={(pathIsSet: boolean) => this.setState({ pathIsSet })}
+            reset={() => {
+              this.setState({
+                actionConfig: { ...defaultActionConfig },
+                branch: "",
+                pathIsSet: false,
+              });
+            }}
+          />
+          <br />
+        </>
+      );
     }
     }
   };
   };
 
 
   renderSourceSelector = () => {
   renderSourceSelector = () => {
+    if (!this.props.form?.hasSource) {
+      return;
+    }
+
     return (
     return (
       <>
       <>
         <TabRegion
         <TabRegion
@@ -489,21 +513,44 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 
     return (
     return (
       <StyledLaunchTemplate>
       <StyledLaunchTemplate>
-        <ClusterSection>
-          {this.props.hideBackButton ? null : (
-            <Flex>
-              <i className="material-icons" onClick={this.props.hideLaunch}>
-                keyboard_backspace
-              </i>
-            </Flex>
-          )}
-          <Template>
+        {name !== "docker" && (
+          <HeaderSection>
+            <i className="material-icons" onClick={this.props.hideLaunch}>
+              keyboard_backspace
+            </i>
             {icon
             {icon
               ? this.renderIcon(icon)
               ? this.renderIcon(icon)
               : this.renderIcon(currentTemplate.icon)}
               : this.renderIcon(currentTemplate.icon)}
-            {name}
-          </Template>
-          <i className="material-icons">arrow_right_alt</i>
+            <Title>{name}</Title>
+          </HeaderSection>
+        )}
+        <DarkMatter antiHeight="-13px" />
+        <Heading isAtTop={name !== "docker"}>Name</Heading>
+        <Subtitle>
+          Randomly generated if left blank.
+          <Warning
+            highlight={
+              !isAlphanumeric(this.state.templateName) &&
+              this.state.templateName !== ""
+            }
+          >
+            Lowercase letters, numbers, and "-" only.
+          </Warning>
+        </Subtitle>
+        <DarkMatter antiHeight="-29px" />
+        <InputRow
+          type="text"
+          value={this.state.templateName}
+          setValue={(x: string) => this.setState({ templateName: x })}
+          placeholder="ex: doctor-scientist"
+          width="100%"
+        />
+        <Heading>Destination</Heading>
+        <Subtitle>
+          Specify the cluster and namespace you would like to deploy your
+          application to.
+        </Subtitle>
+        <ClusterSection>
           <ClusterLabel>
           <ClusterLabel>
             <i className="material-icons">device_hub</i>Cluster
             <i className="material-icons">device_hub</i>Cluster
           </ClusterLabel>
           </ClusterLabel>
@@ -535,26 +582,6 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
             closeOverlay={true}
             closeOverlay={true}
           />
           />
         </ClusterSection>
         </ClusterSection>
-        <Subtitle>
-          Template name
-          <Warning
-            highlight={
-              !isAlphanumeric(this.state.templateName) &&
-              this.state.templateName !== ""
-            }
-          >
-            (lowercase letters, numbers, and "-" only)
-          </Warning>
-          . (Optional)
-        </Subtitle>
-        <DarkMatter antiHeight="-27px" />
-        <InputRow
-          type="text"
-          value={this.state.templateName}
-          setValue={(x: string) => this.setState({ templateName: x })}
-          placeholder="ex: doctor-scientist"
-          width="100%"
-        />
         {this.renderSourceSelector()}
         {this.renderSourceSelector()}
         {this.renderSettingsRegion()}
         {this.renderSettingsRegion()}
       </StyledLaunchTemplate>
       </StyledLaunchTemplate>
@@ -564,6 +591,41 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
 
 
 LaunchTemplate.contextType = Context;
 LaunchTemplate.contextType = Context;
 
 
+const Title = styled.div`
+  font-size: 24px;
+  font-weight: 600;
+  font-family: "Work Sans", sans-serif;
+  margin-left: 10px;
+  border-radius: 2px;
+  color: #ffffff;
+`;
+
+const HeaderSection = styled.div`
+  display: flex;
+  align-items: center;
+
+  > i {
+    cursor: pointer;
+    font-size 24px;
+    color: #969Fbbaa;
+    padding: 3px;
+    border-radius: 100px;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
+const Heading = styled.div<{ isAtTop?: boolean }>`
+  color: white;
+  font-weight: 500;
+  font-size: 16px;
+  margin-bottom: 5px;
+  margin-top: ${(props) => (props.isAtTop ? "30px" : "10px")};
+  display: flex;
+  align-items: center;
+`;
+
 const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>`
 const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>`
   color: ${(props) => (props.highlight ? "#f5cb42" : "")};
   color: ${(props) => (props.highlight ? "#f5cb42" : "")};
   margin-left: ${(props) => (props.makeFlush ? "" : "5px")};
   margin-left: ${(props) => (props.makeFlush ? "" : "5px")};
@@ -604,7 +666,7 @@ const DarkMatter = styled.div<{ antiHeight?: string }>`
 `;
 `;
 
 
 const Subtitle = styled.div`
 const Subtitle = styled.div`
-  padding: 11px 0px 20px;
+  padding: 11px 0px 16px;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
   font-size: 13px;
   font-size: 13px;
   color: #aaaabb;
   color: #aaaabb;
@@ -661,8 +723,9 @@ const ClusterSection = styled.div`
   color: #ffffff;
   color: #ffffff;
   font-family: "Work Sans", sans-serif;
   font-family: "Work Sans", sans-serif;
   font-size: 14px;
   font-size: 14px;
+  margin-top: 2px;
   font-weight: 500;
   font-weight: 500;
-  margin-bottom: 15px;
+  margin-bottom: 22px;
 
 
   > i {
   > i {
     font-size: 25px;
     font-size: 25px;

+ 1 - 1
dashboard/src/main/home/launch/expanded-template/TemplateInfo.tsx

@@ -301,7 +301,7 @@ const TitleSection = styled.div`
   flex-direction: row;
   flex-direction: row;
   height: 40px;
   height: 40px;
   justify-content: space-between;
   justify-content: space-between;
-  width: calc(100% + 42px);
+  width: 100%;
   align-items: center;
   align-items: center;
 `;
 `;
 
 

+ 4 - 17
dashboard/src/main/home/modals/Modal.tsx

@@ -46,7 +46,7 @@ export default class Modal extends Component<PropsType, StateType> {
 }
 }
 
 
 const Overlay = styled.div`
 const Overlay = styled.div`
-  position: absolute;
+  position: fixed;
   margin: 0;
   margin: 0;
   padding: 0;
   padding: 0;
   top: 0;
   top: 0;
@@ -55,26 +55,13 @@ const Overlay = styled.div`
   height: 100%;
   height: 100%;
   background-color: rgba(0, 0, 0, 0.6);
   background-color: rgba(0, 0, 0, 0.6);
   z-index: 3;
   z-index: 3;
+  display: flex;
+  align-items: center;
+  justify-content: center;
 `;
 `;
 
 
 const StyledModal = styled.div`
 const StyledModal = styled.div`
   position: absolute;
   position: absolute;
-  top: calc(
-    50% -
-      (
-        ${(props: { width?: string; height?: string }) =>
-            props.height ? props.height : "425px"} / 2
-      )
-  );
-  left: calc(
-    50% -
-      (
-        ${(props: { width?: string; height?: string }) =>
-            props.width ? props.width : "760px"} / 2
-      )
-  );
-  display: flex;
-  justify-content: center;
   width: ${(props: { width?: string; height?: string }) =>
   width: ${(props: { width?: string; height?: string }) =>
     props.width ? props.width : "760px"};
     props.width ? props.width : "760px"};
   max-width: 80vw;
   max-width: 80vw;

+ 1 - 0
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -395,6 +395,7 @@ const MailTd = styled(Td)`
   max-width: 186px;
   max-width: 186px;
   min-width: 186px;
   min-width: 186px;
   overflow: hidden;
   overflow: hidden;
+  color: #aaaabb;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
 `;
 `;
 
 

+ 18 - 12
dashboard/src/main/home/provisioner/InfraStatuses.tsx

@@ -6,9 +6,9 @@ import { InfraType } from "shared/types";
 import { infraNames } from "shared/common";
 import { infraNames } from "shared/common";
 
 
 type PropsType = {
 type PropsType = {
-  infras: InfraType[],
-  selectInfra: (infra: InfraType) => void,
-  selectedInfra: InfraType,
+  infras: InfraType[];
+  selectInfra: (infra: InfraType) => void;
+  selectedInfra: InfraType;
 };
 };
 
 
 type StateType = {};
 type StateType = {};
@@ -19,10 +19,14 @@ export default class InfraStatuses extends Component<PropsType, StateType> {
   renderStatusIcon = (status: string) => {
   renderStatusIcon = (status: string) => {
     if (status === "created") {
     if (status === "created") {
       return <StatusIcon>✓</StatusIcon>;
       return <StatusIcon>✓</StatusIcon>;
-    } else if (status === 'creating' || status === 'destroying') {
-      return <StatusIcon><img src={loadingDots} /></StatusIcon>
-    } else if (status === 'error' || status === 'destroyed') {
-      return <StatusIcon color='#e3366d'>✗</StatusIcon>
+    } else if (status === "creating" || status === "destroying") {
+      return (
+        <StatusIcon>
+          <img src={loadingDots} />
+        </StatusIcon>
+      );
+    } else if (status === "error" || status === "destroyed") {
+      return <StatusIcon color="#e3366d">✗</StatusIcon>;
     }
     }
   };
   };
 
 
@@ -31,9 +35,9 @@ export default class InfraStatuses extends Component<PropsType, StateType> {
       <StyledInfraStatuses>
       <StyledInfraStatuses>
         {this.props.infras.map((infra: InfraType, i: number) => {
         {this.props.infras.map((infra: InfraType, i: number) => {
           return (
           return (
-            <InfraRow 
+            <InfraRow
               key={infra.id}
               key={infra.id}
-              selected={(infra.id === this.props.selectedInfra?.id)}
+              selected={infra.id === this.props.selectedInfra?.id}
               onClick={() => this.props.selectInfra(infra)}
               onClick={() => this.props.selectInfra(infra)}
             >
             >
               {infraNames[infra.kind]}
               {infraNames[infra.kind]}
@@ -52,7 +56,7 @@ const StatusIcon = styled.div<{ color?: string }>`
   justify-content: center;
   justify-content: center;
   width: 20px;
   width: 20px;
   font-size: 16px;
   font-size: 16px;
-  color: ${props => props.color ? props.color : '#68c49c'};
+  color: ${(props) => (props.color ? props.color : "#68c49c")};
   margin-left: 10px;
   margin-left: 10px;
 `;
 `;
 
 
@@ -75,8 +79,10 @@ const InfraRow = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: space-between;
   justify-content: space-between;
-  color: ${(props: {selected: boolean}) => props.selected ? 'white' : '#ffffff66'};
-  background: ${(props: {selected: boolean}) => props.selected ? '#ffffff18' : ''};
+  color: ${(props: { selected: boolean }) =>
+    props.selected ? "white" : "#ffffff66"};
+  background: ${(props: { selected: boolean }) =>
+    props.selected ? "#ffffff18" : ""};
   font-size: 13px;
   font-size: 13px;
   padding: 20px 19px 20px 42px;
   padding: 20px 19px 20px 42px;
   text-shadow: 0px 0px 8px none;
   text-shadow: 0px 0px 8px none;

+ 91 - 73
dashboard/src/main/home/provisioner/ProvisionerLogs.tsx

@@ -1,59 +1,62 @@
-import React, { Component } from 'react';
-import styled from 'styled-components';
-import { Context } from 'shared/Context';
-import { InfraType } from 'shared/types';
-import posthog from 'posthog-js';
+import React, { Component } from "react";
+import styled from "styled-components";
+import { Context } from "shared/Context";
+import { InfraType } from "shared/types";
+import posthog from "posthog-js";
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
 
 
-import ansiparse from 'shared/ansiparser'
-import loading from 'assets/loading.gif';
-import warning from 'assets/warning.png';
+import ansiparse from "shared/ansiparser";
+import loading from "assets/loading.gif";
+import warning from "assets/warning.png";
 
 
 type PropsType = RouteComponentProps & {
 type PropsType = RouteComponentProps & {
-    selectedInfra: InfraType
+  selectedInfra: InfraType;
 };
 };
 
 
 type StateType = {
 type StateType = {
-  logs: string[],
-  ws: any,
-  scroll: boolean,
-  maxStep: number,
-  error: boolean,
+  logs: string[];
+  ws: any;
+  scroll: boolean;
+  maxStep: number;
+  error: boolean;
 };
 };
 
 
 class ProvisionerLogs extends Component<PropsType, StateType> {
 class ProvisionerLogs extends Component<PropsType, StateType> {
-  
   state = {
   state = {
     logs: [] as string[],
     logs: [] as string[],
-    ws : null as any,
+    ws: null as any,
     scroll: true,
     scroll: true,
     maxStep: 0,
     maxStep: 0,
     error: false,
     error: false,
-  }
+  };
 
 
   ws = null as any;
   ws = null as any;
-  parentRef = React.createRef<HTMLDivElement>()
+  parentRef = React.createRef<HTMLDivElement>();
 
 
   scrollToBottom = () => {
   scrollToBottom = () => {
-    this.parentRef.current.lastElementChild.scrollIntoView({ behavior: "auto" })
-  }
+    this.parentRef.current.lastElementChild.scrollIntoView({
+      behavior: "auto",
+    });
+  };
 
 
   renderLogs = () => {
   renderLogs = () => {
     let { selectedInfra } = this.props;
     let { selectedInfra } = this.props;
     let { logs, maxStep } = this.state;
     let { logs, maxStep } = this.state;
     if (!selectedInfra) {
     if (!selectedInfra) {
-        return <Message>Please select a resource.</Message>
+      return <Message>Please select a resource.</Message>;
     }
     }
 
 
-    if (selectedInfra.status == 'destroyed') {
-        return (
-          <Message>
-              This resource has been auto-destroyed due to an error during provisioning.
-              <div>
-                Please check with your cloud provider to make sure all resources have been properly destroyed.
-              </div>
-          </Message>
-        )
+    if (selectedInfra.status == "destroyed") {
+      return (
+        <Message>
+          This resource has been auto-destroyed due to an error during
+          provisioning.
+          <div>
+            Please check with your cloud provider to make sure all resources
+            have been properly destroyed.
+          </div>
+        </Message>
+      );
     }
     }
 
 
     if (logs.length == 0) {
     if (logs.length == 0) {
@@ -63,17 +66,21 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
             <Loading>
             <Loading>
               <LoadingGif src={loading} /> Provisioning resources...
               <LoadingGif src={loading} /> Provisioning resources...
             </Loading>
             </Loading>
-          )
+          );
         case "destroying":
         case "destroying":
           return (
           return (
             <Message>
             <Message>
               <LoadingGif src={loading} /> Destroying resources...
               <LoadingGif src={loading} /> Destroying resources...
             </Message>
             </Message>
-          )
+          );
         case "error":
         case "error":
-          return <Message>Porter encountered an error while provisioning this resource.</Message>
+          return (
+            <Message>
+              Porter encountered an error while provisioning this resource.
+            </Message>
+          );
         default:
         default:
-          return <Message>{selectedInfra.status}</Message>
+          return <Message>{selectedInfra.status}</Message>;
       }
       }
     }
     }
 
 
@@ -81,10 +88,10 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
     return logs.map((log, i) => {
     return logs.map((log, i) => {
       if (log.trim().length != 0) {
       if (log.trim().length != 0) {
         count += 1;
         count += 1;
-        return <Log key={i + 1}>{`[Step ${count}/${maxStep}] ` + log}</Log>
+        return <Log key={i + 1}>{`[Step ${count}/${maxStep}] ` + log}</Log>;
       }
       }
-    })
-  }
+    });
+  };
 
 
   isJSON = (str: string) => {
   isJSON = (str: string) => {
     try {
     try {
@@ -93,12 +100,12 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
       return false;
       return false;
     }
     }
     return true;
     return true;
-  }
+  };
 
 
   setupWebsocket = () => {
   setupWebsocket = () => {
     this.ws.onopen = () => {
     this.ws.onopen = () => {
-      console.log('connected to websocket')
-    }
+      console.log("connected to websocket");
+    };
 
 
     this.ws.onmessage = (evt: MessageEvent) => {
     this.ws.onmessage = (evt: MessageEvent) => {
       let event = JSON.parse(evt.data);
       let event = JSON.parse(evt.data);
@@ -107,7 +114,11 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
 
 
       for (var i = 0; i < event.length; i++) {
       for (var i = 0; i < event.length; i++) {
         let msg = event[i];
         let msg = event[i];
-        if (msg["Values"] && msg["Values"]["data"] && this.isJSON(msg["Values"]["data"])) { 
+        if (
+          msg["Values"] &&
+          msg["Values"]["data"] &&
+          this.isJSON(msg["Values"]["data"])
+        ) {
           let d = JSON.parse(msg["Values"]["data"]);
           let d = JSON.parse(msg["Values"]["data"]);
 
 
           if (d["kind"] == "error") {
           if (d["kind"] == "error") {
@@ -116,18 +127,22 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
           }
           }
 
 
           // add only valid events
           // add only valid events
-          if (d["log"] != null && d["created_resources"] != null && d["total_resources"] != null) {
+          if (
+            d["log"] != null &&
+            d["created_resources"] != null &&
+            d["total_resources"] != null
+          ) {
             validEvents.push(d);
             validEvents.push(d);
           }
           }
         }
         }
       }
       }
 
 
       if (err) {
       if (err) {
-        posthog.capture('Provisioning Error', {error: err});
+        posthog.capture("Provisioning Error", { error: err });
 
 
         let e = ansiparse(err).map((el: any) => {
         let e = ansiparse(err).map((el: any) => {
           return el.text;
           return el.text;
-        })
+        });
 
 
         this.setState({ logs: [...this.state.logs, ...e], error: true });
         this.setState({ logs: [...this.state.logs, ...e], error: true });
         return;
         return;
@@ -136,32 +151,35 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
       if (validEvents.length == 0) {
       if (validEvents.length == 0) {
         return;
         return;
       }
       }
-      
-      let logs = [] as any[]
+
+      let logs = [] as any[];
       validEvents.forEach((e: any) => {
       validEvents.forEach((e: any) => {
-        logs.push(...ansiparse(e["log"]))
-      })
+        logs.push(...ansiparse(e["log"]));
+      });
 
 
       logs = logs.map((log: any) => {
       logs = logs.map((log: any) => {
-        return log.text
-      })
-
-      this.setState({ 
-        logs: [...this.state.logs, ...logs], 
-        maxStep: validEvents[validEvents.length - 1]["total_resources"]
-      }, () => {
-        this.scrollToBottom()
-      })
-    }
+        return log.text;
+      });
+
+      this.setState(
+        {
+          logs: [...this.state.logs, ...logs],
+          maxStep: validEvents[validEvents.length - 1]["total_resources"],
+        },
+        () => {
+          this.scrollToBottom();
+        }
+      );
+    };
 
 
     this.ws.onerror = (err: ErrorEvent) => {
     this.ws.onerror = (err: ErrorEvent) => {
-      console.log('websocket err', err)
-    }
+      console.log("websocket err", err);
+    };
 
 
     this.ws.onclose = () => {
     this.ws.onclose = () => {
-      console.log('closing provisioner websocket')
-    }
-  }
+      console.log("closing provisioner websocket");
+    };
+  };
 
 
   componentDidMount() {
   componentDidMount() {
     let { currentProject } = this.context;
     let { currentProject } = this.context;
@@ -169,25 +187,25 @@ class ProvisionerLogs extends Component<PropsType, StateType> {
 
 
     if (!selectedInfra) return;
     if (!selectedInfra) return;
 
 
-    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
-    this.ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${selectedInfra.kind}/${selectedInfra.id}/logs`)
+    let protocol = process.env.NODE_ENV == "production" ? "wss" : "ws";
+    this.ws = new WebSocket(
+      `${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${selectedInfra.kind}/${selectedInfra.id}/logs`
+    );
 
 
-    this.setupWebsocket()
+    this.setupWebsocket();
     this.scrollToBottom();
     this.scrollToBottom();
   }
   }
 
 
   componentWillUnmount() {
   componentWillUnmount() {
     if (this.ws) {
     if (this.ws) {
-      this.ws.close()
+      this.ws.close();
     }
     }
   }
   }
 
 
   render() {
   render() {
     return (
     return (
       <LogStream>
       <LogStream>
-        <Wrapper ref={this.parentRef}>
-          {this.renderLogs()}
-        </Wrapper>
+        <Wrapper ref={this.parentRef}>{this.renderLogs()}</Wrapper>
       </LogStream>
       </LogStream>
     );
     );
   }
   }
@@ -205,7 +223,7 @@ const Loading = styled.div`
   width: 100%;
   width: 100%;
   color: #ffffff44;
   color: #ffffff44;
   font-size: 13px;
   font-size: 13px;
-`
+`;
 
 
 const LoadingGif = styled.img`
 const LoadingGif = styled.img`
   width: 15px;
   width: 15px;
@@ -231,7 +249,7 @@ const LogStream = styled.div`
   user-select: text;
   user-select: text;
   max-width: 65%;
   max-width: 65%;
   overflow-y: auto;
   overflow-y: auto;
-  overflow-wrap: break-word; 
+  overflow-wrap: break-word;
 `;
 `;
 
 
 const Message = styled.div`
 const Message = styled.div`
@@ -248,4 +266,4 @@ const Message = styled.div`
 const Log = styled.div`
 const Log = styled.div`
   font-family: monospace;
   font-family: monospace;
   font-size: 12px;
   font-size: 12px;
-`;
+`;

+ 39 - 0
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -4,6 +4,7 @@ import category from "assets/category.svg";
 import integrations from "assets/integrations.svg";
 import integrations from "assets/integrations.svg";
 import rocket from "assets/rocket.png";
 import rocket from "assets/rocket.png";
 import settings from "assets/settings.svg";
 import settings from "assets/settings.svg";
+import discordLogo from "assets/discord.svg";
 
 
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 
 
@@ -187,6 +188,11 @@ class Sidebar extends Component<PropsType, StateType> {
           <br />
           <br />
 
 
           {this.renderProjectContents()}
           {this.renderProjectContents()}
+
+          <DiscordButton href="https://discord.gg/Tky6bzHVHd" target="_blank">
+            <Icon src={discordLogo} />
+            Join Our Discord
+          </DiscordButton>
         </StyledSidebar>
         </StyledSidebar>
       </>
       </>
     );
     );
@@ -197,6 +203,14 @@ Sidebar.contextType = Context;
 
 
 export default withRouter(Sidebar);
 export default withRouter(Sidebar);
 
 
+const Icon = styled.img`
+  height: 25px;
+  width: 25px;
+  opacity: 30%;
+  margin-left: 7px;
+  margin-right: 5px;
+`;
+
 const ProjectPlaceholder = styled.div`
 const ProjectPlaceholder = styled.div`
   background: #ffffff11;
   background: #ffffff11;
   border-radius: 5px;
   border-radius: 5px;
@@ -267,6 +281,31 @@ const BottomSection = styled.div`
   bottom: 10px;
   bottom: 10px;
 `;
 `;
 
 
+const DiscordButton = styled.a`
+  position: absolute;
+  text-decoration: none;
+  bottom: 15px;
+  display: flex;
+  align-items: center;
+  width: calc(100% - 30px);
+  left: 15px;
+  border: 2px solid #ffffff44;
+  border-radius: 3px;
+  color: #ffffff44;
+  height: 40px;
+  font-family: Work Sans, sans-serif;
+  font-size: 14px;
+  font-weight: bold;
+  cursor: pointer;
+  :hover {
+    > img {
+      opacity: 60%;
+    }
+    color: #ffffff88;
+    border-color: #ffffff88;
+  }
+`;
+
 const LogOutButton = styled(NavButton)`
 const LogOutButton = styled(NavButton)`
   width: calc(100% - 55px);
   width: calc(100% - 55px);
   border-top-right-radius: 3px;
   border-top-right-radius: 3px;

+ 2 - 2
dashboard/src/shared/Context.tsx

@@ -42,9 +42,9 @@ class ContextProvider extends Component {
     currentProject: null as ProjectType | null,
     currentProject: null as ProjectType | null,
     setCurrentProject: (currentProject: ProjectType, callback?: any) => {
     setCurrentProject: (currentProject: ProjectType, callback?: any) => {
       if (currentProject) {
       if (currentProject) {
-        localStorage.setItem('currentProject', currentProject.id.toString());
+        localStorage.setItem("currentProject", currentProject.id.toString());
       } else {
       } else {
-        localStorage.removeItem('currentProject');
+        localStorage.removeItem("currentProject");
       }
       }
       this.setState({ currentProject }, () => {
       this.setState({ currentProject }, () => {
         callback && callback();
         callback && callback();

+ 35 - 23
dashboard/src/shared/api.tsx

@@ -10,29 +10,38 @@ import { StorageType } from "./types";
  * @param {(err: Object, res: Object) => void} callback - Callback function.
  * @param {(err: Object, res: Object) => void} callback - Callback function.
  */
  */
 
 
-const checkAuth = baseApi('GET', '/api/auth/check');
+const checkAuth = baseApi("GET", "/api/auth/check");
 
 
-const connectECRRegistry = baseApi<{
-  name: string,
-  aws_integration_id: string,
-}, { id: number }>('POST', pathParams => {
+const connectECRRegistry = baseApi<
+  {
+    name: string;
+    aws_integration_id: string;
+  },
+  { id: number }
+>("POST", (pathParams) => {
   return `/api/projects/${pathParams.id}/registries`;
   return `/api/projects/${pathParams.id}/registries`;
 });
 });
 
 
-const connectGCRRegistry = baseApi<{
-  name: string,
-  gcp_integration_id: string,
-  url: string,
-}, { id: number }>('POST', pathParams => {
+const connectGCRRegistry = baseApi<
+  {
+    name: string;
+    gcp_integration_id: string;
+    url: string;
+  },
+  { id: number }
+>("POST", (pathParams) => {
   return `/api/projects/${pathParams.id}/registries`;
   return `/api/projects/${pathParams.id}/registries`;
 });
 });
 
 
-const createAWSIntegration = baseApi<{
-  aws_region: string,
-  aws_cluster_id?: string,
-  aws_access_key_id: string,
-  aws_secret_access_key: string,
-}, { id: number }>('POST', pathParams => {
+const createAWSIntegration = baseApi<
+  {
+    aws_region: string;
+    aws_cluster_id?: string;
+    aws_access_key_id: string;
+    aws_secret_access_key: string;
+  },
+  { id: number }
+>("POST", (pathParams) => {
   return `/api/projects/${pathParams.id}/integrations/aws`;
   return `/api/projects/${pathParams.id}/integrations/aws`;
 });
 });
 
 
@@ -62,13 +71,16 @@ const createDOKS = baseApi<
   return `/api/projects/${pathParams.project_id}/provision/doks`;
   return `/api/projects/${pathParams.project_id}/provision/doks`;
 });
 });
 
 
-const createGCPIntegration = baseApi<{
-  gcp_region: string,
-  gcp_key_data: string,
-  gcp_project_id: string,
-}, {
-  project_id: number,
-}>('POST', pathParams => {
+const createGCPIntegration = baseApi<
+  {
+    gcp_region: string;
+    gcp_key_data: string;
+    gcp_project_id: string;
+  },
+  {
+    project_id: number;
+  }
+>("POST", (pathParams) => {
   return `/api/projects/${pathParams.project_id}/integrations/gcp`;
   return `/api/projects/${pathParams.project_id}/integrations/gcp`;
 });
 });
 
 

+ 18 - 17
dashboard/src/shared/common.tsx

@@ -1,8 +1,8 @@
-import aws from '../assets/aws.png';
-import digitalOcean from '../assets/do.png';
-import gcp from '../assets/gcp.png';
-import github from '../assets/github.png';
-import { InfraType } from '../shared/types';
+import aws from "../assets/aws.png";
+import digitalOcean from "../assets/do.png";
+import gcp from "../assets/gcp.png";
+import github from "../assets/github.png";
+import { InfraType } from "../shared/types";
 
 
 export const infraNames: any = {
 export const infraNames: any = {
   ecr: "Elastic Container Registry (ECR)",
   ecr: "Elastic Container Registry (ECR)",
@@ -20,10 +20,11 @@ export const integrationList: any = {
     label: "Kubernetes",
     label: "Kubernetes",
     buttonText: "Add a Cluster",
     buttonText: "Add a Cluster",
   },
   },
-  'repo': {
-    icon: 'https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png',
-    label: 'Git Repository',
-    buttonText: 'Link a Github Account',
+  repo: {
+    icon:
+      "https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png",
+    label: "Git Repository",
+    buttonText: "Link a Github Account",
   },
   },
   registry: {
   registry: {
     icon:
     icon:
@@ -69,16 +70,16 @@ export const integrationList: any = {
   },
   },
   do: {
   do: {
     icon: digitalOcean,
     icon: digitalOcean,
-    label: 'DigitalOcean',
+    label: "DigitalOcean",
   },
   },
-  'github': {
+  github: {
     icon: github,
     icon: github,
-    label: 'GitHub',
+    label: "GitHub",
+  },
+  gitlab: {
+    icon: "https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png",
+    label: "Gitlab",
   },
   },
-  'gitlab': {
-    icon: 'https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png',
-    label: 'Gitlab',
-  }
 };
 };
 
 
 export const isAlphanumeric = (x: string | null) => {
 export const isAlphanumeric = (x: string | null) => {
@@ -93,4 +94,4 @@ export const getIgnoreCase = (object: any, key: string) => {
   return object[
   return object[
     Object.keys(object).find((k) => k.toLowerCase() === key.toLowerCase())
     Object.keys(object).find((k) => k.toLowerCase() === key.toLowerCase())
   ];
   ];
-}
+};

+ 1 - 0
dashboard/src/shared/types.tsx

@@ -82,6 +82,7 @@ export interface FormYAML {
   name?: string;
   name?: string;
   icon?: string;
   icon?: string;
   description?: string;
   description?: string;
+  hasSource?: string;
   tags?: string[];
   tags?: string[];
   tabs?: {
   tabs?: {
     name: string;
     name: string;

+ 1 - 1
dashboard/tsconfig.json

@@ -12,4 +12,4 @@
     "removeComments": true,
     "removeComments": true,
     "moduleResolution": "node"
     "moduleResolution": "node"
   }
   }
-}
+}

+ 4 - 4
docker-compose.dev.yaml

@@ -1,4 +1,4 @@
-version: '3'
+version: "3"
 services:
 services:
   webpack:
   webpack:
     build:
     build:
@@ -52,14 +52,14 @@ services:
     container_name: nginx
     container_name: nginx
     restart: unless-stopped
     restart: unless-stopped
     ports:
     ports:
-      - '8080:8080'
+      - "8080:8080"
     volumes:
     volumes:
       - ./docker/nginx_local.conf:/etc/nginx/nginx.conf:ro
       - ./docker/nginx_local.conf:/etc/nginx/nginx.conf:ro
     depends_on:
     depends_on:
       - porter
       - porter
-      - webpack    
+      - webpack
 
 
 volumes:
 volumes:
   database:
   database:
   metabase:
   metabase:
-  chartmuseum:
+  chartmuseum:

+ 2 - 2
docs/GCR.md

@@ -8,13 +8,13 @@ Select **Create Service Account** and provide a name and brief description for t
 
 
 <img src="https://files.readme.io/aa8cda5-Screen_Shot_2020-06-24_at_4.03.33_PM.png" width="80%">
 <img src="https://files.readme.io/aa8cda5-Screen_Shot_2020-06-24_at_4.03.33_PM.png" width="80%">
 
 
-After the service account has been created, you need to create a JSON key for your service account by going to **Actions** -> **Create key** and then selecting JSON as your key type. Once your JSON key file has downloaded, use the `porter connect gcr` command to add the registry to your project. 
+After the service account has been created, you need to create a JSON key for your service account by going to **Actions** -> **Create key** and then selecting JSON as your key type. Once your JSON key file has downloaded, use the `porter connect gcr` command to add the registry to your project.
 
 
 For example, for a key named `gcp-key-file.json` on Mac:
 For example, for a key named `gcp-key-file.json` on Mac:
 
 
 ```diff
 ```diff
 $ cd ~/Downloads
 $ cd ~/Downloads
-$ porter connect gcr 
+$ porter connect gcr
 Please provide the full path to a service account key file.
 Please provide the full path to a service account key file.
 Key file location: ./gcp-key-file.json
 Key file location: ./gcp-key-file.json
 + created gcp integration with id 3
 + created gcp integration with id 3

+ 17 - 18
docs/GETTING_STARTED.md

@@ -2,17 +2,17 @@
 
 
 - [Prerequisites](#prerequisites)
 - [Prerequisites](#prerequisites)
 - [Installing](#installing)
 - [Installing](#installing)
-    - [Mac Installation](#mac-installation)
-    - [Linux Installation](#linux-installation)
-    - [Windows Installation](#windows-installation)
+  - [Mac Installation](#mac-installation)
+  - [Linux Installation](#linux-installation)
+  - [Windows Installation](#windows-installation)
 - [Local Setup](#local-setup)
 - [Local Setup](#local-setup)
-    - [Connecting to a Cluster](#connecting-to-a-cluster)
+  - [Connecting to a Cluster](#connecting-to-a-cluster)
 
 
 ## Prerequisites
 ## Prerequisites
 
 
-You must have access to a Kubernetes cluster with Helm charts installed and the Docker engine must be running on your machine. To quickly get a local Kubernetes cluster set up, following the instructions for [installing minikube](https://minikube.sigs.k8s.io/docs/start/), and make sure that minikube is set as the current context by ensuring the output of `kubectl config current-context` is `minikube`. 
+You must have access to a Kubernetes cluster with Helm charts installed and the Docker engine must be running on your machine. To quickly get a local Kubernetes cluster set up, following the instructions for [installing minikube](https://minikube.sigs.k8s.io/docs/start/), and make sure that minikube is set as the current context by ensuring the output of `kubectl config current-context` is `minikube`.
 
 
-## Installing 
+## Installing
 
 
 ### Mac Installation
 ### Mac Installation
 
 
@@ -58,12 +58,11 @@ sudo mv ./porter /usr/local/bin/porter
 
 
 ### Windows Installation
 ### Windows Installation
 
 
-Go [here](https://github.com/porter-dev/porter/releases/latest/download/porter_0.1.0-beta.1_Windows_x86_64.zip
-) to download the Windows executable and add the binary to your `PATH`. 
+Go [here](https://github.com/porter-dev/porter/releases/latest/download/porter_0.1.0-beta.1_Windows_x86_64.zip) to download the Windows executable and add the binary to your `PATH`.
 
 
 ## Local Setup
 ## Local Setup
 
 
-> **Note:** the local setup process is tracked in [issue #60](https://github.com/porter-dev/porter/issues/60), while the overall onboarding flow is tracked in [issue #50](https://github.com/porter-dev/porter/issues/50). 
+> **Note:** the local setup process is tracked in [issue #60](https://github.com/porter-dev/porter/issues/60), while the overall onboarding flow is tracked in [issue #50](https://github.com/porter-dev/porter/issues/50).
 
 
 To view Porter locally, you must have access to a Kubernetes cluster with Helm charts installed. The simplest way to run Porter is via `porter server start`. After doing this, you can go to `http://localhost:8080` to register an account and create a project manually. Alternatively, you can run the following commands:
 To view Porter locally, you must have access to a Kubernetes cluster with Helm charts installed. The simplest way to run Porter is via `porter server start`. After doing this, you can go to `http://localhost:8080` to register an account and create a project manually. Alternatively, you can run the following commands:
 
 
@@ -74,18 +73,18 @@ porter project create porter-test
 
 
 ### Connecting to a Cluster
 ### Connecting to a Cluster
 
 
-In the case of local setup, you will have to connect to a cluster using the CLI command `porter connect kubeconfig`. By default, this command will read the `current-context` that's set in your default `kubeconfig` (either by reading the `$KUBECONFIG` env variable or reading from `$HOME/.kube/config`). You can also pass a path to a kubeconfig file explicitly (see below). 
+In the case of local setup, you will have to connect to a cluster using the CLI command `porter connect kubeconfig`. By default, this command will read the `current-context` that's set in your default `kubeconfig` (either by reading the `$KUBECONFIG` env variable or reading from `$HOME/.kube/config`). You can also pass a path to a kubeconfig file explicitly (see below).
 
 
 The Porter CLI will attempt to generate a working kubeconfig for many types of cluster configurations and auth mechanisms, even though the necessary commands and/or certificates will not be present in the Porter container. The CLI will attempt the following resolutions:
 The Porter CLI will attempt to generate a working kubeconfig for many types of cluster configurations and auth mechanisms, even though the necessary commands and/or certificates will not be present in the Porter container. The CLI will attempt the following resolutions:
 
 
-1. If a kubeconfig requires cluster CA data via the `certificate-authority` field, the CA data will be automatically populated. 
-2. If a kubeconfig requires client cert data via the `client-certificate` field, the certificate data will be automatically populated. 
-3. If a kubeconfig requires client key data via the `client-key` field, the key data will be automatically populated. 
-4. If a kubeconfig requires a custom `oidc` auth mechanism, and this mechanism requires OIDC issuer CA data via the `idp-certificate-authority` field, the CA data will be automatically populated. 
-5. If a kubeconfig requires a bearer token to be read from a `token-file` field, the token data will be automatically populated. 
-6. If a kubeconfig requires a custom `gcp` auth mechanism (for connecting with GKE clusters), the CLI will require a GCP `service-account` that has permissions to read from the GKE cluster. The CLI will ask the user if it can set this up automatically: if so, it will automatically detect the correct GCP project ID and will create a service account and download a key file. If the user does not wish the CLI to set this up automatically, the user will need to provide a file path to a service account key file that was downloaded from GCloud. 
+1. If a kubeconfig requires cluster CA data via the `certificate-authority` field, the CA data will be automatically populated.
+2. If a kubeconfig requires client cert data via the `client-certificate` field, the certificate data will be automatically populated.
+3. If a kubeconfig requires client key data via the `client-key` field, the key data will be automatically populated.
+4. If a kubeconfig requires a custom `oidc` auth mechanism, and this mechanism requires OIDC issuer CA data via the `idp-certificate-authority` field, the CA data will be automatically populated.
+5. If a kubeconfig requires a bearer token to be read from a `token-file` field, the token data will be automatically populated.
+6. If a kubeconfig requires a custom `gcp` auth mechanism (for connecting with GKE clusters), the CLI will require a GCP `service-account` that has permissions to read from the GKE cluster. The CLI will ask the user if it can set this up automatically: if so, it will automatically detect the correct GCP project ID and will create a service account and download a key file. If the user does not wish the CLI to set this up automatically, the user will need to provide a file path to a service account key file that was downloaded from GCloud.
 
 
-> **Note:** AWS EKS support coming soon. 
+> **Note:** AWS EKS support coming soon.
 
 
 #### Passing `kubeconfig` explicitly
 #### Passing `kubeconfig` explicitly
 
 
@@ -101,4 +100,4 @@ You can initialize Porter with a set of contexts by passing a context list to st
 
 
 ```sh
 ```sh
 porter connect kubeconfig --contexts minikube --contexts staging
 porter connect kubeconfig --contexts minikube --contexts staging
-```
+```

+ 5 - 7
helm/templates/service.yaml

@@ -1,15 +1,13 @@
 apiVersion: v1
 apiVersion: v1
 kind: Service
 kind: Service
 metadata:
 metadata:
-  name: {{ include "porter-prod.fullname" . }}
-  labels:
-    {{- include "porter-prod.labels" . | nindent 4 }}
+  name: { { include "porter-prod.fullname" . } }
+  labels: { { - include "porter-prod.labels" . | nindent 4 } }
 spec:
 spec:
-  type: {{ .Values.service.type }}
+  type: { { .Values.service.type } }
   ports:
   ports:
-    - port: {{ .Values.service.port }}
+    - port: { { .Values.service.port } }
       targetPort: http
       targetPort: http
       protocol: TCP
       protocol: TCP
       name: http
       name: http
-  selector:
-    {{- include "porter-prod.selectorLabels" . | nindent 4 }}
+  selector: { { - include "porter-prod.selectorLabels" . | nindent 4 } }

+ 9 - 5
helm/values.yaml

@@ -25,10 +25,12 @@ serviceAccount:
 
 
 podAnnotations: {}
 podAnnotations: {}
 
 
-podSecurityContext: {}
+podSecurityContext:
+  {}
   # fsGroup: 2000
   # fsGroup: 2000
 
 
-securityContext: {}
+securityContext:
+  {}
   # capabilities:
   # capabilities:
   #   drop:
   #   drop:
   #   - ALL
   #   - ALL
@@ -42,18 +44,20 @@ service:
 
 
 ingress:
 ingress:
   enabled: true
   enabled: true
-  annotations: {}
+  annotations:
+    {}
     # kubernetes.io/ingress.class: nginx
     # kubernetes.io/ingress.class: nginx
     # kubernetes.io/tls-acme: "true"
     # kubernetes.io/tls-acme: "true"
   hosts:
   hosts:
     - host: dashboard.getporter.dev
     - host: dashboard.getporter.dev
-      paths: ['/*']
+      paths: ["/*"]
   tls:
   tls:
     - secretName: ingress-dashboard
     - secretName: ingress-dashboard
       hosts:
       hosts:
         - dashboard.getporter.dev
         - dashboard.getporter.dev
 
 
-resources: {}
+resources:
+  {}
   # We usually recommend not to specify default resources and to leave this as a conscious
   # We usually recommend not to specify default resources and to leave this as a conscious
   # choice for the user. This also increases chances charts run on environments with little
   # choice for the user. This also increases chances charts run on environments with little
   # resources, such as Minikube. If you do want to specify resources, uncomment the following
   # resources, such as Minikube. If you do want to specify resources, uncomment the following

+ 1 - 1
internal/config/config.go

@@ -22,7 +22,7 @@ type ServerConf struct {
 	Port                 int           `env:"SERVER_PORT,default=8080"`
 	Port                 int           `env:"SERVER_PORT,default=8080"`
 	StaticFilePath       string        `env:"STATIC_FILE_PATH,default=/porter/static"`
 	StaticFilePath       string        `env:"STATIC_FILE_PATH,default=/porter/static"`
 	CookieName           string        `env:"COOKIE_NAME,default=porter"`
 	CookieName           string        `env:"COOKIE_NAME,default=porter"`
-	CookieSecrets        []string      `env:"COOKIE_SECRETS,default=hashkey;blockkey"`
+	CookieSecrets        []string      `env:"COOKIE_SECRETS,default=random_hash_key_;random_block_key"`
 	TokenGeneratorSecret string        `env:"TOKEN_GENERATOR_SECRET,default=secret"`
 	TokenGeneratorSecret string        `env:"TOKEN_GENERATOR_SECRET,default=secret"`
 	TimeoutRead          time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
 	TimeoutRead          time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
 	TimeoutWrite         time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
 	TimeoutWrite         time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`

+ 14 - 12
internal/forms/registry.go

@@ -9,23 +9,25 @@ import (
 // CreateRegistry represents the accepted values for creating a
 // CreateRegistry represents the accepted values for creating a
 // registry
 // registry
 type CreateRegistry struct {
 type CreateRegistry struct {
-	Name             string `json:"name" form:"required"`
-	ProjectID        uint   `json:"project_id" form:"required"`
-	URL              string `json:"url"`
-	GCPIntegrationID uint   `json:"gcp_integration_id"`
-	AWSIntegrationID uint   `json:"aws_integration_id"`
-	DOIntegrationID  uint   `json:"do_integration_id"`
+	Name               string `json:"name" form:"required"`
+	ProjectID          uint   `json:"project_id" form:"required"`
+	URL                string `json:"url"`
+	GCPIntegrationID   uint   `json:"gcp_integration_id"`
+	AWSIntegrationID   uint   `json:"aws_integration_id"`
+	DOIntegrationID    uint   `json:"do_integration_id"`
+	BasicIntegrationID uint   `json:"basic_integration_id"`
 }
 }
 
 
 // ToRegistry converts the form to a gorm registry model
 // ToRegistry converts the form to a gorm registry model
 func (cr *CreateRegistry) ToRegistry(repo repository.Repository) (*models.Registry, error) {
 func (cr *CreateRegistry) ToRegistry(repo repository.Repository) (*models.Registry, error) {
 	registry := &models.Registry{
 	registry := &models.Registry{
-		Name:             cr.Name,
-		ProjectID:        cr.ProjectID,
-		URL:              cr.URL,
-		GCPIntegrationID: cr.GCPIntegrationID,
-		AWSIntegrationID: cr.AWSIntegrationID,
-		DOIntegrationID:  cr.DOIntegrationID,
+		Name:               cr.Name,
+		ProjectID:          cr.ProjectID,
+		URL:                cr.URL,
+		GCPIntegrationID:   cr.GCPIntegrationID,
+		AWSIntegrationID:   cr.AWSIntegrationID,
+		DOIntegrationID:    cr.DOIntegrationID,
+		BasicIntegrationID: cr.BasicIntegrationID,
 	}
 	}
 
 
 	if registry.URL == "" && registry.AWSIntegrationID != 0 {
 	if registry.URL == "" && registry.AWSIntegrationID != 0 {

+ 0 - 4
internal/forms/release.go

@@ -1,7 +1,6 @@
 package forms
 package forms
 
 
 import (
 import (
-	"fmt"
 	"net/url"
 	"net/url"
 	"strconv"
 	"strconv"
 
 
@@ -32,17 +31,14 @@ func (rf *ReleaseForm) PopulateHelmOptionsFromQueryParams(
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-		fmt.Println("setting cluster")
 		rf.Cluster = cluster
 		rf.Cluster = cluster
 	}
 	}
 
 
 	if namespace, ok := vals["namespace"]; ok && len(namespace) == 1 {
 	if namespace, ok := vals["namespace"]; ok && len(namespace) == 1 {
-		fmt.Println("setting namespace")
 		rf.Namespace = namespace[0]
 		rf.Namespace = namespace[0]
 	}
 	}
 
 
 	if storage, ok := vals["storage"]; ok && len(storage) == 1 {
 	if storage, ok := vals["storage"]; ok && len(storage) == 1 {
-		fmt.Println("setting storage")
 		rf.Storage = storage[0]
 		rf.Storage = storage[0]
 	}
 	}
 
 

+ 9 - 11
internal/helm/grapher/test_yaml/cassandra.yaml

@@ -94,8 +94,8 @@ spec:
       app.kubernetes.io/name: cassandra
       app.kubernetes.io/name: cassandra
       app.kubernetes.io/instance: my-release
       app.kubernetes.io/instance: my-release
     matchExpressions:
     matchExpressions:
-      - {key: tier, operator: In, values: [cache]}
-      - {key: environment, operator: NotIn, values: [dev]}
+      - { key: tier, operator: In, values: [cache] }
+      - { key: environment, operator: NotIn, values: [dev] }
   serviceName: my-release-cassandra-headless
   serviceName: my-release-cassandra-headless
   podManagementPolicy: OrderedReady
   podManagementPolicy: OrderedReady
   replicas: 2
   replicas: 2
@@ -109,10 +109,9 @@ spec:
         app.kubernetes.io/instance: my-release
         app.kubernetes.io/instance: my-release
         app.kubernetes.io/managed-by: Helm
         app.kubernetes.io/managed-by: Helm
     spec:
     spec:
-      
       affinity:
       affinity:
         podAffinity:
         podAffinity:
-          
+
         podAntiAffinity:
         podAntiAffinity:
           preferredDuringSchedulingIgnoredDuringExecution:
           preferredDuringSchedulingIgnoredDuringExecution:
             - podAffinityTerm:
             - podAffinityTerm:
@@ -125,7 +124,7 @@ spec:
                 topologyKey: kubernetes.io/hostname
                 topologyKey: kubernetes.io/hostname
               weight: 1
               weight: 1
         nodeAffinity:
         nodeAffinity:
-          
+
       securityContext:
       securityContext:
         fsGroup: 1001
         fsGroup: 1001
       containers:
       containers:
@@ -211,17 +210,17 @@ spec:
               containerPort: 9042
               containerPort: 9042
             - name: thrift
             - name: thrift
               containerPort: 9160
               containerPort: 9160
-          resources: 
+          resources:
             limits: {}
             limits: {}
             requests: {}
             requests: {}
           volumeMounts:
           volumeMounts:
             - name: data
             - name: data
               mountPath: /bitnami/cassandra
               mountPath: /bitnami/cassandra
-            
+
       volumes:
       volumes:
-      - name: config-volume
-        configMap:
-          name: config-example
+        - name: config-volume
+          configMap:
+            name: config-example
   volumeClaimTemplates:
   volumeClaimTemplates:
     - metadata:
     - metadata:
         name: data
         name: data
@@ -234,4 +233,3 @@ spec:
         resources:
         resources:
           requests:
           requests:
             storage: "8Gi"
             storage: "8Gi"
-

+ 41 - 41
internal/helm/grapher/test_yaml/ingress.yaml

@@ -7,15 +7,15 @@ metadata:
     nginx.ingress.kubernetes.io/rewrite-target: /
     nginx.ingress.kubernetes.io/rewrite-target: /
 spec:
 spec:
   rules:
   rules:
-  - http:
-      paths:
-      - path: /testpath
-        pathType: Prefix
-        backend:
-          service:
-            name: test
-            port:
-              number: 80
+    - http:
+        paths:
+          - path: /testpath
+            pathType: Prefix
+            backend:
+              service:
+                name: test
+                port:
+                  number: 80
 ---
 ---
 apiVersion: v1
 apiVersion: v1
 kind: Service
 kind: Service
@@ -26,9 +26,9 @@ spec:
   selector:
   selector:
     app: foo
     app: foo
   ports:
   ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 80
+    - protocol: TCP
+      port: 80
+      targetPort: 80
 ---
 ---
 apiVersion: v1
 apiVersion: v1
 kind: Service
 kind: Service
@@ -39,9 +39,9 @@ spec:
   selector:
   selector:
     app: foo
     app: foo
   ports:
   ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 80
+    - protocol: TCP
+      port: 80
+      targetPort: 80
 ---
 ---
 apiVersion: networking.k8s.io/v1
 apiVersion: networking.k8s.io/v1
 kind: Ingress
 kind: Ingress
@@ -51,16 +51,16 @@ metadata:
     nginx.ingress.kubernetes.io/rewrite-target: /
     nginx.ingress.kubernetes.io/rewrite-target: /
 spec:
 spec:
   rules:
   rules:
-  - http:
-      paths:
-      - path: /testpath
-        pathType: Prefix
-        backend:
-          resource:
-            name: resource-test
-            kind: StatefulSet
-            port:
-              number: 80
+    - http:
+        paths:
+          - path: /testpath
+            pathType: Prefix
+            backend:
+              resource:
+                name: resource-test
+                kind: StatefulSet
+                port:
+                  number: 80
 ---
 ---
 apiVersion: apps/v1
 apiVersion: apps/v1
 kind: StatefulSet
 kind: StatefulSet
@@ -85,22 +85,22 @@ spec:
               - key: log_level
               - key: log_level
                 path: log_level
                 path: log_level
       containers:
       containers:
-      - name: nginx
-        image: k8s.gcr.io/nginx-slim:0.8
-        ports:
-        - containerPort: 80
-          name: web
-        volumeMounts:
-        - name: www
-          mountPath: /usr/share/nginx/html
+        - name: nginx
+          image: k8s.gcr.io/nginx-slim:0.8
+          ports:
+            - containerPort: 80
+              name: web
+          volumeMounts:
+            - name: www
+              mountPath: /usr/share/nginx/html
   volumeClaimTemplates:
   volumeClaimTemplates:
-  - metadata:
-      name: www
-    spec:
-      accessModes: [ "ReadWriteOnce" ]
-      resources:
-        requests:
-          storage: 1Gi
+    - metadata:
+        name: www
+      spec:
+        accessModes: ["ReadWriteOnce"]
+        resources:
+          requests:
+            storage: 1Gi
 ---
 ---
 apiVersion: v1
 apiVersion: v1
 kind: ConfigMap
 kind: ConfigMap
@@ -116,4 +116,4 @@ data:
     lives=3
     lives=3
     secret.code.lives=30
     secret.code.lives=30
   ui.properties: |
   ui.properties: |
-    color.good=purple
+    color.good=purple

+ 32 - 30
internal/helm/grapher/test_yaml/kafka.yaml

@@ -47,12 +47,10 @@ spec:
   clusterIP: None
   clusterIP: None
   publishNotReadyAddresses: true
   publishNotReadyAddresses: true
   ports:
   ports:
-    
     - name: tcp-client
     - name: tcp-client
       port: 2181
       port: 2181
       targetPort: client
       targetPort: client
-    
-    
+
     - name: follower
     - name: follower
       port: 2888
       port: 2888
       targetPort: follower
       targetPort: follower
@@ -79,12 +77,10 @@ metadata:
 spec:
 spec:
   type: ClusterIP
   type: ClusterIP
   ports:
   ports:
-    
     - name: tcp-client
     - name: tcp-client
       port: 2181
       port: 2181
       targetPort: client
       targetPort: client
-    
-    
+
     - name: follower
     - name: follower
       port: 2888
       port: 2888
       targetPort: follower
       targetPort: follower
@@ -182,7 +178,6 @@ spec:
         app.kubernetes.io/managed-by: Helm
         app.kubernetes.io/managed-by: Helm
         app.kubernetes.io/component: zookeeper
         app.kubernetes.io/component: zookeeper
     spec:
     spec:
-      
       serviceAccountName: default
       serviceAccountName: default
       securityContext:
       securityContext:
         fsGroup: 1001
         fsGroup: 1001
@@ -196,16 +191,16 @@ spec:
             - bash
             - bash
             - -ec
             - -ec
             - |
             - |
-                # Execute entrypoint as usual after obtaining ZOO_SERVER_ID based on POD hostname
-                HOSTNAME=`hostname -s`
-                if [[ $HOSTNAME =~ (.*)-([0-9]+)$ ]]; then
-                  ORD=${BASH_REMATCH[2]}
-                  export ZOO_SERVER_ID=$((ORD+1))
-                else
-                  echo "Failed to get index from hostname $HOST"
-                  exit 1
-                fi
-                exec /entrypoint.sh /run.sh
+              # Execute entrypoint as usual after obtaining ZOO_SERVER_ID based on POD hostname
+              HOSTNAME=`hostname -s`
+              if [[ $HOSTNAME =~ (.*)-([0-9]+)$ ]]; then
+                ORD=${BASH_REMATCH[2]}
+                export ZOO_SERVER_ID=$((ORD+1))
+              else
+                echo "Failed to get index from hostname $HOST"
+                exit 1
+              fi
+              exec /entrypoint.sh /run.sh
           resources:
           resources:
             requests:
             requests:
               cpu: 250m
               cpu: 250m
@@ -234,7 +229,7 @@ spec:
             - name: ZOO_MAX_SESSION_TIMEOUT
             - name: ZOO_MAX_SESSION_TIMEOUT
               value: "40000"
               value: "40000"
             - name: ZOO_SERVERS
             - name: ZOO_SERVERS
-              value: my-release-zookeeper-0.my-release-zookeeper-headless.default.svc.cluster.local:2888:3888 
+              value: my-release-zookeeper-0.my-release-zookeeper-headless.default.svc.cluster.local:2888:3888
             - name: ZOO_ENABLE_AUTH
             - name: ZOO_ENABLE_AUTH
               value: "no"
               value: "no"
             - name: ZOO_HEAP_SIZE
             - name: ZOO_HEAP_SIZE
@@ -249,18 +244,21 @@ spec:
                   apiVersion: v1
                   apiVersion: v1
                   fieldPath: metadata.name
                   fieldPath: metadata.name
           ports:
           ports:
-            
             - name: client
             - name: client
               containerPort: 2181
               containerPort: 2181
-            
-            
+
             - name: follower
             - name: follower
               containerPort: 2888
               containerPort: 2888
             - name: election
             - name: election
               containerPort: 3888
               containerPort: 3888
           livenessProbe:
           livenessProbe:
             exec:
             exec:
-              command: ['/bin/bash', '-c', 'echo "ruok" | timeout 2 nc -w 2 localhost 2181 | grep imok']
+              command:
+                [
+                  "/bin/bash",
+                  "-c",
+                  'echo "ruok" | timeout 2 nc -w 2 localhost 2181 | grep imok',
+                ]
             initialDelaySeconds: 30
             initialDelaySeconds: 30
             periodSeconds: 10
             periodSeconds: 10
             timeoutSeconds: 5
             timeoutSeconds: 5
@@ -268,7 +266,12 @@ spec:
             failureThreshold: 6
             failureThreshold: 6
           readinessProbe:
           readinessProbe:
             exec:
             exec:
-              command: ['/bin/bash', '-c', 'echo "ruok" | timeout 2 nc -w 2 localhost 2181 | grep imok']
+              command:
+                [
+                  "/bin/bash",
+                  "-c",
+                  'echo "ruok" | timeout 2 nc -w 2 localhost 2181 | grep imok',
+                ]
             initialDelaySeconds: 5
             initialDelaySeconds: 5
             periodSeconds: 10
             periodSeconds: 10
             timeoutSeconds: 5
             timeoutSeconds: 5
@@ -319,7 +322,7 @@ spec:
         app.kubernetes.io/instance: my-release
         app.kubernetes.io/instance: my-release
         app.kubernetes.io/managed-by: Helm
         app.kubernetes.io/managed-by: Helm
         app.kubernetes.io/component: kafka
         app.kubernetes.io/component: kafka
-    spec:      
+    spec:
       securityContext:
       securityContext:
         fsGroup: 1001
         fsGroup: 1001
         runAsUser: 1001
         runAsUser: 1001
@@ -409,17 +412,17 @@ spec:
               port: kafka-client
               port: kafka-client
             initialDelaySeconds: 10
             initialDelaySeconds: 10
             timeoutSeconds: 5
             timeoutSeconds: 5
-            failureThreshold: 
-            periodSeconds: 
-            successThreshold: 
+            failureThreshold:
+            periodSeconds:
+            successThreshold:
           readinessProbe:
           readinessProbe:
             tcpSocket:
             tcpSocket:
               port: kafka-client
               port: kafka-client
             initialDelaySeconds: 5
             initialDelaySeconds: 5
             timeoutSeconds: 5
             timeoutSeconds: 5
             failureThreshold: 6
             failureThreshold: 6
-            periodSeconds: 
-            successThreshold: 
+            periodSeconds:
+            successThreshold:
           resources:
           resources:
             limits: {}
             limits: {}
             requests: {}
             requests: {}
@@ -443,4 +446,3 @@ spec:
         resources:
         resources:
           requests:
           requests:
             storage: "8Gi"
             storage: "8Gi"
-

+ 1 - 1
internal/helm/grapher/test_yaml/volumes.yaml

@@ -32,4 +32,4 @@ data:
     lives=3
     lives=3
     secret.code.lives=30
     secret.code.lives=30
   ui.properties: |
   ui.properties: |
-    color.good=purple
+    color.good=purple

+ 1 - 1
internal/integrations/ci/actions/actions.go

@@ -117,7 +117,7 @@ func (g *GithubActions) GetGithubActionYAML() ([]byte, error) {
 		},
 		},
 		Name: "Deploy to Porter",
 		Name: "Deploy to Porter",
 		Jobs: map[string]GithubActionYAMLJob{
 		Jobs: map[string]GithubActionYAMLJob{
-			"porter-deploy": GithubActionYAMLJob{
+			"porter-deploy": {
 				RunsOn: "ubuntu-latest",
 				RunsOn: "ubuntu-latest",
 				Steps: []GithubActionYAMLStep{
 				Steps: []GithubActionYAMLStep{
 					getCheckoutCodeStep(),
 					getCheckoutCodeStep(),

+ 6 - 3
internal/integrations/ci/actions/steps.go

@@ -1,6 +1,9 @@
 package actions
 package actions
 
 
-import "fmt"
+import (
+	"fmt"
+	"path/filepath"
+)
 
 
 func getCheckoutCodeStep() GithubActionYAMLStep {
 func getCheckoutCodeStep() GithubActionYAMLStep {
 	return GithubActionYAMLStep{
 	return GithubActionYAMLStep{
@@ -41,7 +44,7 @@ func getConfigurePorterStep(porterTokenSecretName string) GithubActionYAMLStep {
 }
 }
 
 
 const dockerBuildPush string = `
 const dockerBuildPush string = `
-docker build . --file %s -t %s:$(git rev-parse --short HEAD)
+docker build %s --file %s -t %s:$(git rev-parse --short HEAD)
 docker push %s:$(git rev-parse --short HEAD)
 docker push %s:$(git rev-parse --short HEAD)
 `
 `
 
 
@@ -49,7 +52,7 @@ func getDockerBuildPushStep(dockerFilePath, repoURL string) GithubActionYAMLStep
 	return GithubActionYAMLStep{
 	return GithubActionYAMLStep{
 		Name: "Docker build, push",
 		Name: "Docker build, push",
 		ID:   "docker_build_push",
 		ID:   "docker_build_push",
-		Run:  fmt.Sprintf(dockerBuildPush, dockerFilePath, repoURL, repoURL),
+		Run:  fmt.Sprintf(dockerBuildPush, filepath.Dir(dockerFilePath), dockerFilePath, repoURL, repoURL),
 	}
 	}
 }
 }
 
 

+ 0 - 2
internal/kubernetes/agent.go

@@ -146,7 +146,6 @@ func (a *Agent) GetPodLogs(namespace string, name string, conn *websocket.Conn)
 			if _, _, err := conn.ReadMessage(); err != nil {
 			if _, _, err := conn.ReadMessage(); err != nil {
 				defer conn.Close()
 				defer conn.Close()
 				errorchan <- nil
 				errorchan <- nil
-				fmt.Println("Successfully closed log stream")
 				return
 				return
 			}
 			}
 		}
 		}
@@ -230,7 +229,6 @@ func (a *Agent) StreamControllerStatus(conn *websocket.Conn, kind string) error
 			if _, _, err := conn.ReadMessage(); err != nil {
 			if _, _, err := conn.ReadMessage(); err != nil {
 				defer conn.Close()
 				defer conn.Close()
 				defer close(stopper)
 				defer close(stopper)
-				defer fmt.Println("Successfully closed controller status stream")
 				errorchan <- nil
 				errorchan <- nil
 				return
 				return
 			}
 			}

+ 13 - 4
internal/kubernetes/config.go

@@ -2,6 +2,7 @@ package kubernetes
 
 
 import (
 import (
 	"errors"
 	"errors"
+	"fmt"
 	"path/filepath"
 	"path/filepath"
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
@@ -99,7 +100,13 @@ type OutOfClusterConfig struct {
 // the result of ToRawKubeConfigLoader, and also adds a custom http transport layer
 // the result of ToRawKubeConfigLoader, and also adds a custom http transport layer
 // if necessary (required for GCP auth)
 // if necessary (required for GCP auth)
 func (conf *OutOfClusterConfig) ToRESTConfig() (*rest.Config, error) {
 func (conf *OutOfClusterConfig) ToRESTConfig() (*rest.Config, error) {
-	restConf, err := conf.ToRawKubeConfigLoader().ClientConfig()
+	cmdConf, err := conf.GetClientConfigFromCluster()
+
+	if err != nil {
+		return nil, err
+	}
+
+	restConf, err := cmdConf.ClientConfig()
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -157,11 +164,13 @@ func (conf *OutOfClusterConfig) ToRESTMapper() (meta.RESTMapper, error) {
 // GetClientConfigFromCluster will construct new clientcmd.ClientConfig using
 // GetClientConfigFromCluster will construct new clientcmd.ClientConfig using
 // the configuration saved within a Cluster model
 // the configuration saved within a Cluster model
 func (conf *OutOfClusterConfig) GetClientConfigFromCluster() (clientcmd.ClientConfig, error) {
 func (conf *OutOfClusterConfig) GetClientConfigFromCluster() (clientcmd.ClientConfig, error) {
-	cluster := conf.Cluster
+	if conf.Cluster == nil {
+		return nil, fmt.Errorf("cluster cannot be nil")
+	}
 
 
-	if cluster.AuthMechanism == models.Local {
+	if conf.Cluster.AuthMechanism == models.Local {
 		kubeAuth, err := conf.Repo.KubeIntegration.ReadKubeIntegration(
 		kubeAuth, err := conf.Repo.KubeIntegration.ReadKubeIntegration(
-			cluster.KubeIntegrationID,
+			conf.Cluster.KubeIntegrationID,
 		)
 		)
 
 
 		if err != nil {
 		if err != nil {

+ 0 - 4
internal/kubernetes/provisioner/global_stream.go

@@ -85,8 +85,6 @@ func GlobalStreamListener(
 	repo repository.Repository,
 	repo repository.Repository,
 	errorChan chan error,
 	errorChan chan error,
 ) {
 ) {
-	fmt.Println("starting global stream listener")
-
 	for {
 	for {
 		xstreams, err := client.XReadGroup(
 		xstreams, err := client.XReadGroup(
 			context.Background(),
 			context.Background(),
@@ -98,8 +96,6 @@ func GlobalStreamListener(
 			},
 			},
 		).Result()
 		).Result()
 
 
-		fmt.Println(xstreams, err)
-
 		if err != nil {
 		if err != nil {
 			errorChan <- err
 			errorChan <- err
 			return
 			return

+ 0 - 2
internal/kubernetes/provisioner/resource_stream.go

@@ -2,7 +2,6 @@ package provisioner
 
 
 import (
 import (
 	"context"
 	"context"
-	"fmt"
 
 
 	redis "github.com/go-redis/redis/v8"
 	redis "github.com/go-redis/redis/v8"
 	"github.com/gorilla/websocket"
 	"github.com/gorilla/websocket"
@@ -39,7 +38,6 @@ func ResourceStream(client *redis.Client, streamName string, conn *websocket.Con
 			).Result()
 			).Result()
 
 
 			if err != nil {
 			if err != nil {
-				fmt.Println("ERROR XREAD", err)
 				return
 				return
 			}
 			}
 
 

+ 13 - 12
internal/models/integrations/integration.go

@@ -5,18 +5,19 @@ type IntegrationService string
 
 
 // The list of supported third-party services
 // The list of supported third-party services
 const (
 const (
-	GKE      IntegrationService = "gke"
-	DOKS     IntegrationService = "doks"
-	GCS      IntegrationService = "gcs"
-	S3       IntegrationService = "s3"
-	HelmRepo IntegrationService = "helm"
-	EKS      IntegrationService = "eks"
-	Kube     IntegrationService = "kube"
-	GCR      IntegrationService = "gcr"
-	ECR      IntegrationService = "ecr"
-	DOCR     IntegrationService = "docr"
-	Github   IntegrationService = "github"
-	Docker   IntegrationService = "docker"
+	GKE       IntegrationService = "gke"
+	DOKS      IntegrationService = "doks"
+	GCS       IntegrationService = "gcs"
+	S3        IntegrationService = "s3"
+	HelmRepo  IntegrationService = "helm"
+	EKS       IntegrationService = "eks"
+	Kube      IntegrationService = "kube"
+	GCR       IntegrationService = "gcr"
+	ECR       IntegrationService = "ecr"
+	DOCR      IntegrationService = "docr"
+	Github    IntegrationService = "github"
+	DockerHub IntegrationService = "dockerhub"
+	Docker    IntegrationService = "docker"
 )
 )
 
 
 // PorterIntegration is a supported integration service, specifying an auth
 // PorterIntegration is a supported integration service, specifying an auth

+ 8 - 3
internal/models/registry.go

@@ -1,6 +1,8 @@
 package models
 package models
 
 
 import (
 import (
+	"strings"
+
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/models/integrations"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 )
 )
@@ -26,9 +28,10 @@ type Registry struct {
 	// All fields below this line are encrypted before storage
 	// All fields below this line are encrypted before storage
 	// ------------------------------------------------------------------
 	// ------------------------------------------------------------------
 
 
-	GCPIntegrationID uint
-	AWSIntegrationID uint
-	DOIntegrationID  uint
+	GCPIntegrationID   uint
+	AWSIntegrationID   uint
+	DOIntegrationID    uint
+	BasicIntegrationID uint
 
 
 	// A token cache that can be used by an auth mechanism (integration), if desired
 	// A token cache that can be used by an auth mechanism (integration), if desired
 	TokenCache integrations.RegTokenCache
 	TokenCache integrations.RegTokenCache
@@ -64,6 +67,8 @@ func (r *Registry) Externalize() *RegistryExternal {
 		serv = integrations.GCR
 		serv = integrations.GCR
 	} else if r.DOIntegrationID != 0 {
 	} else if r.DOIntegrationID != 0 {
 		serv = integrations.DOCR
 		serv = integrations.DOCR
+	} else if strings.Contains(r.URL, "index.docker.io") {
+		serv = integrations.DockerHub
 	}
 	}
 
 
 	return &RegistryExternal{
 	return &RegistryExternal{

+ 252 - 0
internal/registry/registry.go

@@ -71,6 +71,10 @@ func (r *Registry) ListRepositories(
 		return r.listDOCRRepositories(repo, doAuth)
 		return r.listDOCRRepositories(repo, doAuth)
 	}
 	}
 
 
+	if r.BasicIntegrationID != 0 {
+		return r.listPrivateRegistryRepositories(repo)
+	}
+
 	return nil, fmt.Errorf("error listing repositories")
 	return nil, fmt.Errorf("error listing repositories")
 }
 }
 
 
@@ -252,6 +256,98 @@ func (r *Registry) listDOCRRepositories(
 	return res, nil
 	return res, nil
 }
 }
 
 
+func (r *Registry) listPrivateRegistryRepositories(
+	repo repository.Repository,
+) ([]*Repository, error) {
+	// handle dockerhub different, as it doesn't implement the docker registry http api
+	if strings.Contains(r.URL, "docker.io") {
+		// in this case, we just return the single dockerhub repository that's linked
+		res := make([]*Repository, 0)
+
+		res = append(res, &Repository{
+			Name: strings.Split(r.URL, "docker.io/")[1],
+			URI:  r.URL,
+		})
+
+		return res, nil
+	}
+
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		r.BasicIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// Just use service account key to authenticate, since scopes may not be in place
+	// for oauth. This also prevents us from making more requests.
+	client := &http.Client{}
+
+	// get the host and scheme to make the request
+	parsedURL, err := url.Parse(r.URL)
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s://%s/v2/_catalog", parsedURL.Scheme, parsedURL.Host),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req.SetBasicAuth(string(basic.Username), string(basic.Password))
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// if the status code is 404, fallback to the Docker Hub implementation
+	if resp.StatusCode == 404 {
+		req, err := http.NewRequest(
+			"GET",
+			fmt.Sprintf("%s/", r.URL),
+			nil,
+		)
+
+		if err != nil {
+			return nil, err
+		}
+
+		req.SetBasicAuth(string(basic.Username), string(basic.Password))
+
+		resp, err = client.Do(req)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	gcrResp := gcrRepositoryResp{}
+
+	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
+		return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
+	}
+
+	res := make([]*Repository, 0)
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, repo := range gcrResp.Repositories {
+		res = append(res, &Repository{
+			Name: repo,
+			URI:  parsedURL.Host + "/" + repo,
+		})
+	}
+
+	return res, nil
+}
+
 func (r *Registry) getTokenCache() (tok *ints.TokenCache, err error) {
 func (r *Registry) getTokenCache() (tok *ints.TokenCache, err error) {
 	return &ints.TokenCache{
 	return &ints.TokenCache{
 		Token:  r.TokenCache.Token,
 		Token:  r.TokenCache.Token,
@@ -296,6 +392,10 @@ func (r *Registry) ListImages(
 		return r.listDOCRImages(repoName, repo, doAuth)
 		return r.listDOCRImages(repoName, repo, doAuth)
 	}
 	}
 
 
+	if r.BasicIntegrationID != 0 {
+		return r.listPrivateRegistryImages(repoName, repo)
+	}
+
 	return nil, fmt.Errorf("error listing images")
 	return nil, fmt.Errorf("error listing images")
 }
 }
 
 
@@ -444,6 +544,118 @@ func (r *Registry) listDOCRImages(
 	return res, nil
 	return res, nil
 }
 }
 
 
+func (r *Registry) listPrivateRegistryImages(repoName string, repo repository.Repository) ([]*Image, error) {
+	// handle dockerhub different, as it doesn't implement the docker registry http api
+	if strings.Contains(r.URL, "docker.io") {
+		return r.listDockerHubImages(repoName, repo)
+	}
+
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		r.BasicIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// Just use service account key to authenticate, since scopes may not be in place
+	// for oauth. This also prevents us from making more requests.
+	client := &http.Client{}
+
+	// get the host and scheme to make the request
+	parsedURL, err := url.Parse(r.URL)
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("%s://%s/v2/%s/tags/list", parsedURL.Scheme, parsedURL.Host, repoName),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req.SetBasicAuth(string(basic.Username), string(basic.Password))
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	gcrResp := gcrImageResp{}
+
+	if err := json.NewDecoder(resp.Body).Decode(&gcrResp); err != nil {
+		return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
+	}
+
+	res := make([]*Image, 0)
+
+	for _, tag := range gcrResp.Tags {
+		res = append(res, &Image{
+			RepositoryName: repoName,
+			Tag:            tag,
+		})
+	}
+
+	return res, nil
+}
+
+type dockerHubImageResult struct {
+	Name string `json:"name"`
+}
+
+type dockerHubImageResp struct {
+	Results []dockerHubImageResult `json:"results"`
+}
+
+func (r *Registry) listDockerHubImages(repoName string, repo repository.Repository) ([]*Image, error) {
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		r.BasicIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	client := &http.Client{}
+
+	req, err := http.NewRequest(
+		"GET",
+		fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/tags", strings.Split(r.URL, "docker.io/")[1]),
+		nil,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	req.SetBasicAuth(string(basic.Username), string(basic.Password))
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	imageResp := dockerHubImageResp{}
+
+	if err := json.NewDecoder(resp.Body).Decode(&imageResp); err != nil {
+		return nil, fmt.Errorf("Could not read private registry repositories: %v", err)
+	}
+
+	res := make([]*Image, 0)
+
+	for _, result := range imageResp.Results {
+		res = append(res, &Image{
+			RepositoryName: repoName,
+			Tag:            result.Name,
+		})
+	}
+
+	return res, nil
+}
+
 // GetDockerConfigJSON returns a dockerconfigjson file contents with "auths"
 // GetDockerConfigJSON returns a dockerconfigjson file contents with "auths"
 // populated.
 // populated.
 func (r *Registry) GetDockerConfigJSON(
 func (r *Registry) GetDockerConfigJSON(
@@ -466,6 +678,10 @@ func (r *Registry) GetDockerConfigJSON(
 		conf, err = r.getDOCRDockerConfigFile(repo, doAuth)
 		conf, err = r.getDOCRDockerConfigFile(repo, doAuth)
 	}
 	}
 
 
+	if r.BasicIntegrationID != 0 {
+		conf, err = r.getPrivateRegistryDockerConfigFile(repo)
+	}
+
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -596,6 +812,42 @@ func (r *Registry) getDOCRDockerConfigFile(
 	}, nil
 	}, nil
 }
 }
 
 
+func (r *Registry) getPrivateRegistryDockerConfigFile(
+	repo repository.Repository,
+) (*configfile.ConfigFile, error) {
+	basic, err := repo.BasicIntegration.ReadBasicIntegration(
+		r.BasicIntegrationID,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	key := r.URL
+
+	if !strings.Contains(key, "http") {
+		key = "https://" + key
+	}
+
+	parsedURL, _ := url.Parse(key)
+
+	authConfigKey := parsedURL.Host
+
+	if strings.Contains(r.URL, "index.docker.io") {
+		authConfigKey = "https://index.docker.io/v1/"
+	}
+
+	return &configfile.ConfigFile{
+		AuthConfigs: map[string]types.AuthConfig{
+			authConfigKey: types.AuthConfig{
+				Username: string(basic.Username),
+				Password: string(basic.Password),
+				Auth:     generateAuthToken(string(basic.Username), string(basic.Password)),
+			},
+		},
+	}, nil
+}
+
 func generateAuthToken(username, password string) string {
 func generateAuthToken(username, password string) string {
 	return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
 	return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
 }
 }

+ 24 - 12
internal/repository/gorm/cluster.go

@@ -1,6 +1,8 @@
 package gorm
 package gorm
 
 
 import (
 import (
+	"context"
+
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
@@ -117,6 +119,8 @@ func (repo *ClusterRepository) UpdateClusterCandidateCreatedClusterID(
 func (repo *ClusterRepository) CreateCluster(
 func (repo *ClusterRepository) CreateCluster(
 	cluster *models.Cluster,
 	cluster *models.Cluster,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
+	ctxDB := repo.db.WithContext(context.Background())
+
 	err := repo.EncryptClusterData(cluster, repo.key)
 	err := repo.EncryptClusterData(cluster, repo.key)
 
 
 	if err != nil {
 	if err != nil {
@@ -125,11 +129,11 @@ func (repo *ClusterRepository) CreateCluster(
 
 
 	project := &models.Project{}
 	project := &models.Project{}
 
 
-	if err := repo.db.Where("id = ?", cluster.ProjectID).First(&project).Error; err != nil {
+	if err := ctxDB.Where("id = ?", cluster.ProjectID).First(&project).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	assoc := repo.db.Model(&project).Association("Clusters")
+	assoc := ctxDB.Model(&project).Association("Clusters")
 
 
 	if assoc.Error != nil {
 	if assoc.Error != nil {
 		return nil, assoc.Error
 		return nil, assoc.Error
@@ -140,7 +144,7 @@ func (repo *ClusterRepository) CreateCluster(
 	}
 	}
 
 
 	// create a token cache by default
 	// create a token cache by default
-	assoc = repo.db.Model(cluster).Association("TokenCache")
+	assoc = ctxDB.Model(cluster).Association("TokenCache")
 
 
 	if assoc.Error != nil {
 	if assoc.Error != nil {
 		return nil, assoc.Error
 		return nil, assoc.Error
@@ -163,10 +167,12 @@ func (repo *ClusterRepository) CreateCluster(
 func (repo *ClusterRepository) ReadCluster(
 func (repo *ClusterRepository) ReadCluster(
 	id uint,
 	id uint,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
+	ctxDB := repo.db.WithContext(context.Background())
+
 	cluster := &models.Cluster{}
 	cluster := &models.Cluster{}
 
 
 	// preload Clusters association
 	// preload Clusters association
-	if err := repo.db.Preload("TokenCache").Where("id = ?", id).First(&cluster).Error; err != nil {
+	if err := ctxDB.Preload("TokenCache").Where("id = ?", id).First(&cluster).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -184,9 +190,11 @@ func (repo *ClusterRepository) ReadCluster(
 func (repo *ClusterRepository) ListClustersByProjectID(
 func (repo *ClusterRepository) ListClustersByProjectID(
 	projectID uint,
 	projectID uint,
 ) ([]*models.Cluster, error) {
 ) ([]*models.Cluster, error) {
+	ctxDB := repo.db.WithContext(context.Background())
+
 	clusters := []*models.Cluster{}
 	clusters := []*models.Cluster{}
 
 
-	if err := repo.db.Where("project_id = ?", projectID).Find(&clusters).Error; err != nil {
+	if err := ctxDB.Where("project_id = ?", projectID).Find(&clusters).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -201,13 +209,15 @@ func (repo *ClusterRepository) ListClustersByProjectID(
 func (repo *ClusterRepository) UpdateCluster(
 func (repo *ClusterRepository) UpdateCluster(
 	cluster *models.Cluster,
 	cluster *models.Cluster,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
+	ctxDB := repo.db.WithContext(context.Background())
+
 	err := repo.EncryptClusterData(cluster, repo.key)
 	err := repo.EncryptClusterData(cluster, repo.key)
 
 
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if err := repo.db.Save(cluster).Error; err != nil {
+	if err := ctxDB.Save(cluster).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -224,6 +234,8 @@ func (repo *ClusterRepository) UpdateCluster(
 func (repo *ClusterRepository) UpdateClusterTokenCache(
 func (repo *ClusterRepository) UpdateClusterTokenCache(
 	tokenCache *ints.ClusterTokenCache,
 	tokenCache *ints.ClusterTokenCache,
 ) (*models.Cluster, error) {
 ) (*models.Cluster, error) {
+	ctxDB := repo.db.WithContext(context.Background())
+
 	if tok := tokenCache.Token; len(tok) > 0 {
 	if tok := tokenCache.Token; len(tok) > 0 {
 		cipherData, err := repository.Encrypt(tok, repo.key)
 		cipherData, err := repository.Encrypt(tok, repo.key)
 
 
@@ -236,14 +248,14 @@ func (repo *ClusterRepository) UpdateClusterTokenCache(
 
 
 	cluster := &models.Cluster{}
 	cluster := &models.Cluster{}
 
 
-	if err := repo.db.Where("id = ?", tokenCache.ClusterID).First(&cluster).Error; err != nil {
+	if err := ctxDB.Where("id = ?", tokenCache.ClusterID).First(&cluster).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	cluster.TokenCache.Token = tokenCache.Token
 	cluster.TokenCache.Token = tokenCache.Token
 	cluster.TokenCache.Expiry = tokenCache.Expiry
 	cluster.TokenCache.Expiry = tokenCache.Expiry
 
 
-	if err := repo.db.Save(cluster).Error; err != nil {
+	if err := ctxDB.Save(cluster).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -347,14 +359,14 @@ func (repo *ClusterRepository) DecryptClusterData(
 	}
 	}
 
 
 	if tok := cluster.TokenCache.Token; len(tok) > 0 {
 	if tok := cluster.TokenCache.Token; len(tok) > 0 {
-
 		plaintext, err := repository.Decrypt(tok, key)
 		plaintext, err := repository.Decrypt(tok, key)
 
 
+		// in the case that the token cache is down, set empty token
 		if err != nil {
 		if err != nil {
-			return err
+			cluster.TokenCache.Token = []byte{}
+		} else {
+			cluster.TokenCache.Token = plaintext
 		}
 		}
-
-		cluster.TokenCache.Token = plaintext
 	}
 	}
 
 
 	return nil
 	return nil

+ 5 - 1213
package-lock.json

@@ -2,1219 +2,11 @@
   "requires": true,
   "requires": true,
   "lockfileVersion": 1,
   "lockfileVersion": 1,
   "dependencies": {
   "dependencies": {
-    "@types/classnames": {
-      "version": "2.2.11",
-      "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz",
-      "integrity": "sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw=="
-    },
-    "@types/d3": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/@types/d3/-/d3-6.3.0.tgz",
-      "integrity": "sha512-YILdGsjNTbvkWZKsBasB4cVDwNPnni7ILMJg9keMErQHyuII2yO2jyFdUy5E+7k/HTNP/AucrPddQuu27udbeA==",
-      "requires": {
-        "@types/d3-array": "*",
-        "@types/d3-axis": "*",
-        "@types/d3-brush": "*",
-        "@types/d3-chord": "*",
-        "@types/d3-color": "*",
-        "@types/d3-contour": "*",
-        "@types/d3-delaunay": "*",
-        "@types/d3-dispatch": "*",
-        "@types/d3-drag": "*",
-        "@types/d3-dsv": "*",
-        "@types/d3-ease": "*",
-        "@types/d3-fetch": "*",
-        "@types/d3-force": "*",
-        "@types/d3-format": "*",
-        "@types/d3-geo": "*",
-        "@types/d3-hierarchy": "*",
-        "@types/d3-interpolate": "*",
-        "@types/d3-path": "*",
-        "@types/d3-polygon": "*",
-        "@types/d3-quadtree": "*",
-        "@types/d3-random": "*",
-        "@types/d3-scale": "*",
-        "@types/d3-scale-chromatic": "*",
-        "@types/d3-selection": "*",
-        "@types/d3-shape": "*",
-        "@types/d3-time": "*",
-        "@types/d3-time-format": "*",
-        "@types/d3-timer": "*",
-        "@types/d3-transition": "*",
-        "@types/d3-zoom": "*"
-      }
-    },
-    "@types/d3-array": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-2.9.0.tgz",
-      "integrity": "sha512-sdBMGfNvLUkBypPMEhOcKcblTQfgHbqbYrUqRE31jOwdDHBJBxz4co2MDAq93S4Cp++phk4UiwoEg/1hK3xXAQ=="
-    },
-    "@types/d3-axis": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-2.0.0.tgz",
-      "integrity": "sha512-gUdlEwGBLl3tXGiBnBNmNzph9W3bCfa4tBgWZD60Z1eDQKTY4zyCAcZ3LksignGfKawYatmDYcBdjJ5h/54sqA==",
-      "requires": {
-        "@types/d3-selection": "*"
-      }
-    },
-    "@types/d3-brush": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-2.1.0.tgz",
-      "integrity": "sha512-rLQqxQeXWF4ArXi81GlV8HBNwJw9EDpz0jcWvvzv548EDE4tXrayBTOHYi/8Q4FZ/Df8PGXFzxpAVQmJMjOtvQ==",
-      "requires": {
-        "@types/d3-selection": "*"
-      }
-    },
-    "@types/d3-chord": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-2.0.0.tgz",
-      "integrity": "sha512-3nHsLY7lImpZlM/hrPeDqqW2a+lRXXoHsG54QSurDGihZAIE/doQlohs0evoHrWOJqXyn4A4xbSVEtXnMEZZiw=="
-    },
-    "@types/d3-color": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.1.tgz",
-      "integrity": "sha512-u7LTCL7RnaavFSmob2rIAJLNwu50i6gFwY9cHFr80BrQURYQBRkJ+Yv47nA3Fm7FeRhdWTiVTeqvSeOuMAOzBQ=="
-    },
-    "@types/d3-contour": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-2.0.0.tgz",
-      "integrity": "sha512-PS9UO6zBQqwHXsocbpdzZFONgK1oRUgWtjjh/iz2vM06KaXLInLiKZ9e3OLBRerc1cU2uJYpO+8zOnb6frvCGQ==",
-      "requires": {
-        "@types/d3-array": "*",
-        "@types/geojson": "*"
-      }
-    },
-    "@types/d3-delaunay": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-5.3.0.tgz",
-      "integrity": "sha512-gJYcGxLu0xDZPccbUe32OUpeaNtd1Lz0NYJtko6ZLMyG2euF4pBzrsQXms67LHZCDFzzszw+dMhSL/QAML3bXw=="
-    },
-    "@types/d3-dispatch": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-2.0.0.tgz",
-      "integrity": "sha512-Sh0KW6z/d7uxssD7K4s4uCSzlEG/+SP+U47q098NVdOfFvUKNTvKAIV4XqjxsUuhE/854ARAREHOxkr9gQOCyg=="
-    },
-    "@types/d3-drag": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-2.0.0.tgz",
-      "integrity": "sha512-VaUJPjbMnDn02tcRqsHLRAX5VjcRIzCjBfeXTLGe6QjMn5JccB5Cz4ztMRXMJfkbC45ovgJFWuj6DHvWMX1thA==",
-      "requires": {
-        "@types/d3-selection": "*"
-      }
-    },
-    "@types/d3-dsv": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-2.0.1.tgz",
-      "integrity": "sha512-wovgiG9Mgkr/SZ/m/c0m+RwrIT4ozsuCWeLxJyoObDWsie2DeQT4wzMdHZPR9Ya5oZLQT3w3uSl0NehG0+0dCA=="
-    },
-    "@types/d3-ease": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-2.0.0.tgz",
-      "integrity": "sha512-6aZrTyX5LG+ptofVHf+gTsThLRY1nhLotJjgY4drYqk1OkJMu2UvuoZRlPw2fffjRHeYepue3/fxTufqKKmvsA=="
-    },
-    "@types/d3-fetch": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-2.0.0.tgz",
-      "integrity": "sha512-WnLepGtxepFfXRdPI8I5FTgNiHn9p4vMTTqaNCzJJfAswXx0rOY2jjeolzEU063em3iJmGZ+U79InnEeFOrCRw==",
-      "requires": {
-        "@types/d3-dsv": "*"
-      }
-    },
-    "@types/d3-force": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-2.1.0.tgz",
-      "integrity": "sha512-LGDtC2YADu8OBniq9EBx/MOsXsMcJbEkmfSpXuz6oVdRamB+3CLCiq5EKFPEILGZQckkilGFq1ZTJ7kc289k+Q=="
-    },
-    "@types/d3-format": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-2.0.0.tgz",
-      "integrity": "sha512-uagdkftxnGkO4pZw5jEYOM5ZnZOEsh7z8j11Qxk85UkB2RzfUUxRl7R9VvvJZHwKn8l+x+rpS77Nusq7FkFmIg=="
-    },
-    "@types/d3-geo": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-2.0.0.tgz",
-      "integrity": "sha512-DHHgYXW36lnAEQMYU2udKVOxxljHrn2EdOINeSC9jWCAXwOnGn7A19B8sNsHqgpu4F7O2bSD7//cqBXD3W0Deg==",
-      "requires": {
-        "@types/geojson": "*"
-      }
-    },
-    "@types/d3-hierarchy": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz",
-      "integrity": "sha512-YxdskUvwzqggpnSnDQj4KVkicgjpkgXn/g/9M9iGsiToLS3nG6Ytjo1FoYhYVAAElV/fJBGVL3cQ9Hb7tcv+lw=="
-    },
-    "@types/d3-interpolate": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.0.tgz",
-      "integrity": "sha512-Wt1v2zTlEN8dSx8hhx6MoOhWQgTkz0Ukj7owAEIOF2QtI0e219paFX9rf/SLOr/UExWb1TcUzatU8zWwFby6gg==",
-      "requires": {
-        "@types/d3-color": "*"
-      }
-    },
-    "@types/d3-path": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.0.tgz",
-      "integrity": "sha512-tXcR/9OtDdeCIsyl6eTNHC3XOAOdyc6ceF3QGBXOd9jTcK+ex/ecr00p9L9362e/op3UEPpxrToi1FHrtTSj7Q=="
-    },
-    "@types/d3-polygon": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-2.0.0.tgz",
-      "integrity": "sha512-fISnMd8ePED1G4aa4V974Jmt+ajHSgPoxMa2D0ULxMybpx0Vw4WEzhQEaMIrL3hM8HVRcKTx669I+dTy/4PhAw=="
-    },
-    "@types/d3-quadtree": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-2.0.0.tgz",
-      "integrity": "sha512-YZuJuGBnijD0H+98xMJD4oZXgv/umPXy5deu3IimYTPGH3Kr8Th6iQUff0/6S80oNBD7KtOuIHwHUCymUiRoeQ=="
-    },
-    "@types/d3-random": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-2.2.0.tgz",
-      "integrity": "sha512-Hjfj9m68NmYZzushzEG7etPvKH/nj9b9s9+qtkNG3/dbRBjQZQg1XS6nRuHJcCASTjxXlyXZnKu2gDxyQIIu9A=="
-    },
-    "@types/d3-scale": {
-      "version": "3.2.2",
-      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.2.2.tgz",
-      "integrity": "sha512-qpQe8G02tzUwt9sdWX1h8A/W0Q1+N48wMnYXVOkrzeLUkCfvzJYV9Ee3aORCS4dN4ONRLFmMvaXdziQ29XGLjQ==",
-      "requires": {
-        "@types/d3-time": "*"
-      }
-    },
-    "@types/d3-scale-chromatic": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz",
-      "integrity": "sha512-Y62+2clOwZoKua84Ha0xU77w7lePiaBoTjXugT4l8Rd5LAk+Mn/ZDtrgs087a+B5uJ3jYUHHtKw5nuEzp0WBHw=="
-    },
-    "@types/d3-selection": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.0.tgz",
-      "integrity": "sha512-EF0lWZ4tg7oDFg4YQFlbOU3936e3a9UmoQ2IXlBy1+cv2c2Pv7knhKUzGlH5Hq2sF/KeDTH1amiRPey2rrLMQA=="
-    },
-    "@types/d3-shape": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.0.0.tgz",
-      "integrity": "sha512-NLzD02m5PiD1KLEDjLN+MtqEcFYn4ZL9+Rqc9ZwARK1cpKZXd91zBETbe6wpBB6Ia0D0VZbpmbW3+BsGPGnCpA==",
-      "requires": {
-        "@types/d3-path": "^1"
-      },
-      "dependencies": {
-        "@types/d3-path": {
-          "version": "1.0.9",
-          "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
-          "integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ=="
-        }
-      }
-    },
-    "@types/d3-time": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.0.0.tgz",
-      "integrity": "sha512-Abz8bTzy8UWDeYs9pCa3D37i29EWDjNTjemdk0ei1ApYVNqulYlGUKip/jLOpogkPSsPz/GvZCYiC7MFlEk0iQ=="
-    },
-    "@types/d3-time-format": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.0.tgz",
-      "integrity": "sha512-UpLg1mn/8PLyjr+J/JwdQJM/GzysMvv2CS8y+WYAL5K0+wbvXv/pPSLEfdNaprCZsGcXTxPsFMy8QtkYv9ueew=="
-    },
-    "@types/d3-timer": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-2.0.0.tgz",
-      "integrity": "sha512-l6stHr1VD1BWlW6u3pxrjLtJfpPZq9I3XmKIQtq7zHM/s6fwEtI1Yn6Sr5/jQTrUDCC5jkS6gWqlFGCDArDqNg=="
-    },
-    "@types/d3-transition": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-2.0.0.tgz",
-      "integrity": "sha512-UJDzI98utcZQUJt3uIit/Ho0/eBIANzrWJrTmi4+TaKIyWL2iCu7ShP0o4QajCskhyjOA7C8+4CE3b1YirTzEQ==",
-      "requires": {
-        "@types/d3-selection": "*"
-      }
-    },
-    "@types/d3-voronoi": {
-      "version": "1.1.9",
-      "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.9.tgz",
-      "integrity": "sha512-DExNQkaHd1F3dFPvGA/Aw2NGyjMln6E9QzsiqOcBgnE+VInYnFBHBBySbZQts6z6xD+5jTfKCP7M4OqMyVjdwQ=="
-    },
-    "@types/d3-zoom": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-2.0.0.tgz",
-      "integrity": "sha512-daL0PJm4yT0ISTGa7p2lHX0kvv9FO/IR1ooWbHR/7H4jpbaKiLux5FslyS/OvISPiJ5SXb4sOqYhO6fMB6hKRw==",
-      "requires": {
-        "@types/d3-interpolate": "*",
-        "@types/d3-selection": "*"
-      }
-    },
-    "@types/geojson": {
-      "version": "7946.0.7",
-      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
-      "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ=="
-    },
-    "@types/lodash": {
-      "version": "4.14.168",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
-      "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
-    },
-    "@types/prop-types": {
-      "version": "15.7.3",
-      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
-      "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
-    },
-    "@types/react": {
-      "version": "17.0.2",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.2.tgz",
-      "integrity": "sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==",
-      "requires": {
-        "@types/prop-types": "*",
-        "csstype": "^3.0.2"
-      }
-    },
-    "@types/react-dom": {
-      "version": "17.0.1",
-      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.1.tgz",
-      "integrity": "sha512-yIVyopxQb8IDZ7SOHeTovurFq+fXiPICa+GV3gp0Xedsl+MwQlMLKmvrnEjFbQxjliH5YVAEWFh975eVNmKj7Q==",
-      "requires": {
-        "@types/react": "*"
-      }
-    },
-    "@visx/annotation": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@visx/annotation/-/annotation-1.4.0.tgz",
-      "integrity": "sha512-LxskgApI1/2cMR6F+CR2KmMFSVvQwSu6ihEzjn+hhdpS9v5NUEJiTe0YwkV53yCmol9k+h1asmqc4JfIJY14nw==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "@visx/drag": "1.3.0",
-        "@visx/group": "1.0.0",
-        "@visx/point": "1.0.0",
-        "@visx/shape": "1.4.0",
-        "@visx/text": "1.3.0",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.5.10",
-        "react-use-measure": "2.0.1"
-      }
-    },
-    "@visx/axis": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-1.4.0.tgz",
-      "integrity": "sha512-VBq170tBu+PJ1XJLk6xwzmNEXrqriaSiaP808peR3oArXd0yMOgmI+HKm18qzPS2hbdRgxZNQOnlDZvB43H95w==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "@visx/group": "1.0.0",
-        "@visx/point": "1.0.0",
-        "@visx/scale": "1.4.0",
-        "@visx/shape": "1.4.0",
-        "@visx/text": "1.3.0",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.6.0"
-      }
-    },
-    "@visx/bounds": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-1.0.0.tgz",
-      "integrity": "sha512-QxD/OkZVkzpeP6L0YxUnIAsxlFemkDPfOumchVDRlrO4lZ3YXLmsnaEEiJpU5tSgNamZAUh+Tz3d2RbHp3qqxA==",
-      "requires": {
-        "@types/react": "*",
-        "@types/react-dom": "*",
-        "prop-types": "^15.5.10"
-      }
-    },
-    "@visx/brush": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@visx/brush/-/brush-1.4.0.tgz",
-      "integrity": "sha512-Hg+J45rj3csXT98/DTyzigxPFndlJ65vdR7O+JdefQ5i3XJGK6vp/HDILthG5pdyCxqyYoCtfsy+u4bqzR1nrQ==",
-      "requires": {
-        "@visx/drag": "1.3.0",
-        "@visx/group": "1.0.0",
-        "@visx/shape": "1.4.0",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.6.1"
-      }
-    },
-    "@visx/clip-path": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/clip-path/-/clip-path-1.0.0.tgz",
-      "integrity": "sha512-s5Xdjlf/1RaOOS5kJO8d0xOY6BdEifP8dq0gIJwEfBMyQALOtTyiDNZ0v0yTT82qssLrtZ0fm3xVSRJC1cPB/Q==",
-      "requires": {
-        "@types/react": "*",
-        "prop-types": "^15.5.10"
-      }
-    },
-    "@visx/curve": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-1.0.0.tgz",
-      "integrity": "sha512-rN9TUf4uRmPuQ5Rd4kbvinSDsTbR61YB26+ucK6RNMHIr9aLmujpcPJhVwk22EWphRRGIxzK2OSp0d5dgpNppQ==",
-      "requires": {
-        "@types/d3-shape": "^1.3.1",
-        "d3-shape": "^1.0.6"
-      },
-      "dependencies": {
-        "@types/d3-path": {
-          "version": "1.0.9",
-          "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
-          "integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ=="
-        },
-        "@types/d3-shape": {
-          "version": "1.3.5",
-          "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.5.tgz",
-          "integrity": "sha512-aPEax03owTAKynoK8ZkmkZEDZvvT4Y5pWgii4Jp4oQt0gH45j6siDl9gNDVC5kl64XHN2goN9jbYoHK88tFAcA==",
-          "requires": {
-            "@types/d3-path": "^1"
-          }
-        },
-        "d3-path": {
-          "version": "1.0.9",
-          "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
-          "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
-        },
-        "d3-shape": {
-          "version": "1.3.7",
-          "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
-          "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
-          "requires": {
-            "d3-path": "1"
-          }
-        }
-      }
-    },
-    "@visx/drag": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@visx/drag/-/drag-1.3.0.tgz",
-      "integrity": "sha512-Hr5ceQ7R4pVUGQ1yFLwfiWd2Nd4Z74xqaxB2xNs/vpxqX6xhzS90agVoD1KiDtCN9IKOrS0J2bEPStJFFpu1OQ==",
-      "requires": {
-        "@types/react": "*",
-        "@visx/event": "1.3.0",
-        "prop-types": "^15.5.10"
-      }
-    },
-    "@visx/event": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@visx/event/-/event-1.3.0.tgz",
-      "integrity": "sha512-Nq0xz7c1eMc8j3CTt94hTZO+veQ1Ti7u22LZF4M2W36yKh5PtZRfM4O6tILSdO7cxigeNd1vbmZo9MSDmk5lbQ==",
-      "requires": {
-        "@types/react": "*",
-        "@visx/point": "1.0.0"
-      }
-    },
-    "@visx/geo": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/geo/-/geo-1.0.0.tgz",
-      "integrity": "sha512-lUTqKsZtYoz7+ei9PRkuSrqqDKLcS5T7w05TZZGng/jJKtXofUwmooKxoJEe6lu8szdsOgOPX0ijyIrLAvS1Ig==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/d3-geo": "^1.11.1",
-        "@types/geojson": "*",
-        "@types/react": "*",
-        "@visx/group": "1.0.0",
-        "classnames": "^2.2.5",
-        "d3-geo": "^1.11.3",
-        "prop-types": "^15.5.10"
-      },
-      "dependencies": {
-        "@types/d3-geo": {
-          "version": "1.12.1",
-          "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.12.1.tgz",
-          "integrity": "sha512-8+gyGFyMCXIHtnMNKQDT++tZ4XYFXgiP5NK7mcv34aYXA16GQFiBBITjKzxghpO8QNVceOd9rUn1JY92WLNGQw==",
-          "requires": {
-            "@types/geojson": "*"
-          }
-        },
-        "d3-array": {
-          "version": "1.2.4",
-          "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
-          "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
-        },
-        "d3-geo": {
-          "version": "1.12.1",
-          "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz",
-          "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==",
-          "requires": {
-            "d3-array": "1"
-          }
-        }
-      }
-    },
-    "@visx/glyph": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/glyph/-/glyph-1.0.0.tgz",
-      "integrity": "sha512-htDMWLoPApXt7pxxd6e0cebvB2VXoCvMrhkeRjgG0sYYWVe1LWUPs83DN2U8VEpLKp0uxmy28Pf/COBwj8qGIQ==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/d3-shape": "^1.3.1",
-        "@types/react": "*",
-        "@visx/group": "1.0.0",
-        "classnames": "^2.2.5",
-        "d3-shape": "^1.2.0",
-        "prop-types": "^15.6.2"
-      },
-      "dependencies": {
-        "@types/d3-path": {
-          "version": "1.0.9",
-          "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
-          "integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ=="
-        },
-        "@types/d3-shape": {
-          "version": "1.3.5",
-          "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.5.tgz",
-          "integrity": "sha512-aPEax03owTAKynoK8ZkmkZEDZvvT4Y5pWgii4Jp4oQt0gH45j6siDl9gNDVC5kl64XHN2goN9jbYoHK88tFAcA==",
-          "requires": {
-            "@types/d3-path": "^1"
-          }
-        },
-        "d3-path": {
-          "version": "1.0.9",
-          "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
-          "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
-        },
-        "d3-shape": {
-          "version": "1.3.7",
-          "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
-          "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
-          "requires": {
-            "d3-path": "1"
-          }
-        }
-      }
-    },
-    "@visx/gradient": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/gradient/-/gradient-1.0.0.tgz",
-      "integrity": "sha512-Y+Xoz4dRF5+1Ru693Ik7v/Bb4d9kyVWz4iTBB2Oyfu7Ceo2VC7bh7McmLgmYmsnbbFWeJiAnow2Qzx0EHR5eCg==",
-      "requires": {
-        "@types/react": "*",
-        "prop-types": "^15.5.7"
-      }
-    },
-    "@visx/grid": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-1.4.0.tgz",
-      "integrity": "sha512-POwnGByfqYlZc5MOHU2I8RFXA5qO97LVx8ibSXLk2cV44Uoif7zHwQMZ+qup3JpYDKhtvjdO6GkBJ4UCE0apmg==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "@visx/group": "1.0.0",
-        "@visx/point": "1.0.0",
-        "@visx/scale": "1.4.0",
-        "@visx/shape": "1.4.0",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.6.2"
-      }
-    },
-    "@visx/group": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/group/-/group-1.0.0.tgz",
-      "integrity": "sha512-2YlhGHTINUl7do046p/bkIYiD4xDv/sJ4JAaGrqFXwX68EJZ5Er/0gpZZ4nrADQlxB8/uyJvZzp1Q54ySfTMiA==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.6.2"
-      }
-    },
-    "@visx/heatmap": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/heatmap/-/heatmap-1.0.0.tgz",
-      "integrity": "sha512-sLobst48pdEVBG/LjPnLmylmZ0BrcC7M1m9ZwtYfwSpfh8u3si6l/E8w9hv1ohqILYCHp7tpvmC7soGUh5qk0g==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "@visx/group": "1.0.0",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.6.1"
-      }
-    },
-    "@visx/hierarchy": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/hierarchy/-/hierarchy-1.0.0.tgz",
-      "integrity": "sha512-kT8RBebsbuN6zn+LaBhQEG+xVKd5IJki3kn0fki4pPHbJTaSMdNYUJicPtsKnWyRqxAuNQfKdKaKJfam4iNTtA==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/d3-hierarchy": "^1.1.6",
-        "@types/react": "*",
-        "@visx/group": "1.0.0",
-        "classnames": "^2.2.5",
-        "d3-hierarchy": "^1.1.4",
-        "prop-types": "^15.6.1"
-      },
-      "dependencies": {
-        "@types/d3-hierarchy": {
-          "version": "1.1.7",
-          "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-1.1.7.tgz",
-          "integrity": "sha512-fvht6DOYKzqmXjMb/+xfgkmrWM4SD7rMA/ZbM+gGwr9ZTuIDfky95J8CARtaJo/ExeWyS0xGVdL2gqno2zrQ0Q=="
-        },
-        "d3-hierarchy": {
-          "version": "1.1.9",
-          "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz",
-          "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ=="
-        }
-      }
-    },
-    "@visx/legend": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@visx/legend/-/legend-1.4.0.tgz",
-      "integrity": "sha512-NkYfk62D+uHOkcsury5OqnTNnzbSakuwbocqwn3DHdE/jdpJIJVx9gh3OBHWqdIIallLhnT7p2oBtDuN8lrsvw==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "@visx/group": "1.0.0",
-        "@visx/scale": "1.4.0",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.5.10"
-      }
-    },
-    "@visx/marker": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@visx/marker/-/marker-1.4.0.tgz",
-      "integrity": "sha512-UelBxlyzIjrj0YAWo80TL/tc2dBpqD4Fz0/+W7i1d6c2y4Y5lEp9SxRVK3nl4LmHd2dZAQe5XOmreKhiFvq3PQ==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "@visx/group": "1.0.0",
-        "@visx/shape": "1.4.0",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.6.2"
-      }
-    },
-    "@visx/mock-data": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/mock-data/-/mock-data-1.0.0.tgz",
-      "integrity": "sha512-yd4lult1oEpmbj7pxzNb398VW+fRYaZWBFFKcqJEibAxlko4kmBfVqFR2gPZTp/K7I0/5mvD3hhNr1NpH2SIHA==",
-      "requires": {
-        "@types/d3-random": "^1.1.2",
-        "d3-random": "^1.0.3"
-      },
-      "dependencies": {
-        "@types/d3-random": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-1.1.3.tgz",
-          "integrity": "sha512-XXR+ZbFCoOd4peXSMYJzwk0/elP37WWAzS/DG+90eilzVbUSsgKhBcWqylGWe+lA2ubgr7afWAOBaBxRgMUrBQ=="
-        },
-        "d3-random": {
-          "version": "1.1.2",
-          "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
-          "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ=="
-        }
-      }
-    },
-    "@visx/network": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@visx/network/-/network-1.1.0.tgz",
-      "integrity": "sha512-TEsXvUIc1RAK2Jq+pHXWYNi/Gly/v8J/s9jBlFF4YHxNvq9Ra2cJ/8lJkiBVXRLZU9Pyxr48sUfnFoevkhMN6w==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "@visx/group": "1.0.0",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.6.2"
-      }
-    },
-    "@visx/pattern": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/@visx/pattern/-/pattern-1.1.0.tgz",
-      "integrity": "sha512-mjuc/fvDb5rfKkvfDKyduwjbB2bidJtLj5q0GzhGeXImKxlqG255VwMSrkyPsPSmSnV39EHSrfKb/2oc/gSUGA==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.5.10"
-      }
-    },
-    "@visx/point": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/point/-/point-1.0.0.tgz",
-      "integrity": "sha512-0L3ILwv6ro0DsQVbA1lo8fo6q3wvIeSTt9C8NarUUkoTNSFZaJtlmvwg2238r8fwwmSv0v9QFBj1hBz4o0bHrg=="
-    },
-    "@visx/responsive": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-1.3.0.tgz",
-      "integrity": "sha512-RMfpjdnHKyhB/bb2i6x/vfQeYzfz+pJc3VUK+dP88lXXTkqv1O/NYIkXk2sWk6QhDw5muChHFmnZ1L8TnIOMXg==",
-      "requires": {
-        "@types/lodash": "^4.14.146",
-        "@types/react": "*",
-        "lodash": "^4.17.10",
-        "prop-types": "^15.6.1",
-        "resize-observer-polyfill": "1.5.1"
-      }
-    },
-    "@visx/scale": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-1.4.0.tgz",
-      "integrity": "sha512-uNy/hsZCmCtL1hC7rTasMUtf9/faG/QcXNtQioe6VYwtbZxCMR53+yvz3W1oqAW8Y0bslGfKRMzT8T29OjAD/g==",
-      "requires": {
-        "@types/d3-interpolate": "^1.3.1",
-        "@types/d3-scale": "^3.2.1",
-        "@types/d3-time": "^1.0.10",
-        "d3-interpolate": "^1.4.0",
-        "d3-scale": "^3.2.3",
-        "d3-time": "^1.1.0"
-      },
-      "dependencies": {
-        "@types/d3-color": {
-          "version": "1.4.1",
-          "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-1.4.1.tgz",
-          "integrity": "sha512-xkPLi+gbgUU9ED6QX4g6jqYL2KCB0/3AlM+ncMGqn49OgH0gFMY/ITGqPF8HwEiLzJaC+2L0I+gNwBgABv1Pvg=="
-        },
-        "@types/d3-interpolate": {
-          "version": "1.4.2",
-          "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-1.4.2.tgz",
-          "integrity": "sha512-ylycts6llFf8yAEs1tXzx2loxxzDZHseuhPokrqKprTQSTcD3JbJI1omZP1rphsELZO3Q+of3ff0ZS7+O6yVzg==",
-          "requires": {
-            "@types/d3-color": "^1"
-          }
-        },
-        "@types/d3-time": {
-          "version": "1.1.1",
-          "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.1.tgz",
-          "integrity": "sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw=="
-        },
-        "d3-color": {
-          "version": "1.4.1",
-          "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
-          "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
-        },
-        "d3-interpolate": {
-          "version": "1.4.0",
-          "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
-          "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
-          "requires": {
-            "d3-color": "1"
-          }
-        },
-        "d3-time": {
-          "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
-          "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
-        }
-      }
-    },
-    "@visx/shape": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-1.4.0.tgz",
-      "integrity": "sha512-iTeFGtsidHXoeEyfriwRj7vkgs3BYqXWuDVe/uW4kpn76u7M0LhRCy59ADlufYDn+19qdA/rVPv4oD6nrWMhCw==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/d3-path": "^1.0.8",
-        "@types/d3-shape": "^1.3.1",
-        "@types/lodash": "^4.14.146",
-        "@types/react": "*",
-        "@visx/curve": "1.0.0",
-        "@visx/group": "1.0.0",
-        "@visx/scale": "1.4.0",
-        "classnames": "^2.2.5",
-        "d3-path": "^1.0.5",
-        "d3-shape": "^1.2.0",
-        "lodash": "^4.17.15",
-        "prop-types": "^15.5.10"
-      },
-      "dependencies": {
-        "@types/d3-path": {
-          "version": "1.0.9",
-          "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz",
-          "integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ=="
-        },
-        "@types/d3-shape": {
-          "version": "1.3.5",
-          "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.5.tgz",
-          "integrity": "sha512-aPEax03owTAKynoK8ZkmkZEDZvvT4Y5pWgii4Jp4oQt0gH45j6siDl9gNDVC5kl64XHN2goN9jbYoHK88tFAcA==",
-          "requires": {
-            "@types/d3-path": "^1"
-          }
-        },
-        "d3-path": {
-          "version": "1.0.9",
-          "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
-          "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
-        },
-        "d3-shape": {
-          "version": "1.3.7",
-          "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
-          "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
-          "requires": {
-            "d3-path": "1"
-          }
-        }
-      }
-    },
-    "@visx/text": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@visx/text/-/text-1.3.0.tgz",
-      "integrity": "sha512-fmFZ1S26DOH6u5ublcY33GmCBMyUufh9Mna8i30YZkS6kk7WO7ZCxMR9fVF71hrZ+rYWLylswXGwzL95LroE8g==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/lodash": "^4.14.160",
-        "@types/react": "*",
-        "classnames": "^2.2.5",
-        "lodash": "^4.17.20",
-        "prop-types": "^15.7.2",
-        "reduce-css-calc": "^1.3.0"
-      }
-    },
-    "@visx/tooltip": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-1.3.0.tgz",
-      "integrity": "sha512-8ZliQmcE3R2TvNHyCnViPlT9Msnx/prn6gfsa1QMgWySQzVhFlL4Man9hkmbbIvpwU1i1OarbANF7hUqz86jZQ==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/react": "*",
-        "@visx/bounds": "1.0.0",
-        "classnames": "^2.2.5",
-        "prop-types": "^15.5.10",
-        "react-use-measure": "2.0.1"
-      }
-    },
-    "@visx/visx": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@visx/visx/-/visx-1.4.0.tgz",
-      "integrity": "sha512-5fv10x3JaEAShkZM73d/hTY4oA8nRSaIJ33B8utYci9jBBDHx5p7fpWBK1ocdlLwMzQnoU3oUxO5NcG0FK6Sow==",
-      "requires": {
-        "@visx/annotation": "1.4.0",
-        "@visx/axis": "1.4.0",
-        "@visx/bounds": "1.0.0",
-        "@visx/brush": "1.4.0",
-        "@visx/clip-path": "1.0.0",
-        "@visx/curve": "1.0.0",
-        "@visx/drag": "1.3.0",
-        "@visx/event": "1.3.0",
-        "@visx/geo": "1.0.0",
-        "@visx/glyph": "1.0.0",
-        "@visx/gradient": "1.0.0",
-        "@visx/grid": "1.4.0",
-        "@visx/group": "1.0.0",
-        "@visx/heatmap": "1.0.0",
-        "@visx/hierarchy": "1.0.0",
-        "@visx/legend": "1.4.0",
-        "@visx/marker": "1.4.0",
-        "@visx/mock-data": "1.0.0",
-        "@visx/network": "1.1.0",
-        "@visx/pattern": "1.1.0",
-        "@visx/point": "1.0.0",
-        "@visx/responsive": "1.3.0",
-        "@visx/scale": "1.4.0",
-        "@visx/shape": "1.4.0",
-        "@visx/text": "1.3.0",
-        "@visx/tooltip": "1.3.0",
-        "@visx/voronoi": "1.0.0",
-        "@visx/zoom": "1.3.0"
-      }
-    },
-    "@visx/voronoi": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@visx/voronoi/-/voronoi-1.0.0.tgz",
-      "integrity": "sha512-eLjZKCJpGHjoPgX0t5a0C6jF612FT4rEuCOkQnWFx71ahpKF7dT2WXY00SrvufPGlmA5asjaVY1/Juss6SmgHA==",
-      "requires": {
-        "@types/classnames": "^2.2.9",
-        "@types/d3-voronoi": "^1.1.9",
-        "@types/react": "*",
-        "classnames": "^2.2.5",
-        "d3-voronoi": "^1.1.2",
-        "prop-types": "^15.6.1"
-      }
-    },
-    "@visx/zoom": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@visx/zoom/-/zoom-1.3.0.tgz",
-      "integrity": "sha512-CsxdV4srxWaZcbv2rsM0JXC30LnYOzWRrDkFgT7sKtV2h8uxlzNSeafrDIXyuu5YVWs6FYttapPms9jFGoS8SA==",
-      "requires": {
-        "@types/react": "*",
-        "@visx/event": "1.3.0",
-        "prop-types": "^15.6.2"
-      }
-    },
-    "balanced-match": {
-      "version": "0.4.2",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
-      "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg="
-    },
-    "classnames": {
-      "version": "2.2.6",
-      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
-      "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
-    },
-    "commander": {
-      "version": "2.20.3",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
-      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
-    },
-    "csstype": {
-      "version": "3.0.6",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
-      "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw=="
-    },
-    "d3": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/d3/-/d3-6.5.0.tgz",
-      "integrity": "sha512-gr7FoRecKtBkBxelTeGVYERRTPgjPFLh2rOBisHdbXe3RIrVLjCo7COZYMSeFeiwVPOHDtxAGJlvN7XssNAIcg==",
-      "requires": {
-        "d3-array": "2",
-        "d3-axis": "2",
-        "d3-brush": "2",
-        "d3-chord": "2",
-        "d3-color": "2",
-        "d3-contour": "2",
-        "d3-delaunay": "5",
-        "d3-dispatch": "2",
-        "d3-drag": "2",
-        "d3-dsv": "2",
-        "d3-ease": "2",
-        "d3-fetch": "2",
-        "d3-force": "2",
-        "d3-format": "2",
-        "d3-geo": "2",
-        "d3-hierarchy": "2",
-        "d3-interpolate": "2",
-        "d3-path": "2",
-        "d3-polygon": "2",
-        "d3-quadtree": "2",
-        "d3-random": "2",
-        "d3-scale": "3",
-        "d3-scale-chromatic": "2",
-        "d3-selection": "2",
-        "d3-shape": "2",
-        "d3-time": "2",
-        "d3-time-format": "3",
-        "d3-timer": "2",
-        "d3-transition": "2",
-        "d3-zoom": "2"
-      }
-    },
-    "d3-array": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.11.0.tgz",
-      "integrity": "sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw==",
-      "requires": {
-        "internmap": "^1.0.0"
-      }
-    },
-    "d3-axis": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-2.0.0.tgz",
-      "integrity": "sha512-9nzB0uePtb+u9+dWir+HTuEAKJOEUYJoEwbJPsZ1B4K3iZUgzJcSENQ05Nj7S4CIfbZZ8/jQGoUzGKFznBhiiQ=="
-    },
-    "d3-brush": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-2.1.0.tgz",
-      "integrity": "sha512-cHLLAFatBATyIKqZOkk/mDHUbzne2B3ZwxkzMHvFTCZCmLaXDpZRihQSn8UNXTkGD/3lb/W2sQz0etAftmHMJQ==",
-      "requires": {
-        "d3-dispatch": "1 - 2",
-        "d3-drag": "2",
-        "d3-interpolate": "1 - 2",
-        "d3-selection": "2",
-        "d3-transition": "2"
-      }
-    },
-    "d3-chord": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-2.0.0.tgz",
-      "integrity": "sha512-D5PZb7EDsRNdGU4SsjQyKhja8Zgu+SHZfUSO5Ls8Wsn+jsAKUUGkcshLxMg9HDFxG3KqavGWaWkJ8EpU8ojuig==",
-      "requires": {
-        "d3-path": "1 - 2"
-      }
-    },
-    "d3-color": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
-      "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="
-    },
-    "d3-contour": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-2.0.0.tgz",
-      "integrity": "sha512-9unAtvIaNk06UwqBmvsdHX7CZ+NPDZnn8TtNH1myW93pWJkhsV25JcgnYAu0Ck5Veb1DHiCv++Ic5uvJ+h50JA==",
-      "requires": {
-        "d3-array": "2"
-      }
-    },
-    "d3-delaunay": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz",
-      "integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==",
-      "requires": {
-        "delaunator": "4"
-      }
-    },
-    "d3-dispatch": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz",
-      "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA=="
-    },
-    "d3-drag": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz",
-      "integrity": "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==",
-      "requires": {
-        "d3-dispatch": "1 - 2",
-        "d3-selection": "2"
-      }
-    },
-    "d3-dsv": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-2.0.0.tgz",
-      "integrity": "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==",
-      "requires": {
-        "commander": "2",
-        "iconv-lite": "0.4",
-        "rw": "1"
-      }
-    },
-    "d3-ease": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz",
-      "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ=="
-    },
-    "d3-fetch": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-2.0.0.tgz",
-      "integrity": "sha512-TkYv/hjXgCryBeNKiclrwqZH7Nb+GaOwo3Neg24ZVWA3MKB+Rd+BY84Nh6tmNEMcjUik1CSUWjXYndmeO6F7sw==",
-      "requires": {
-        "d3-dsv": "1 - 2"
-      }
-    },
-    "d3-force": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz",
-      "integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==",
-      "requires": {
-        "d3-dispatch": "1 - 2",
-        "d3-quadtree": "1 - 2",
-        "d3-timer": "1 - 2"
-      }
-    },
-    "d3-format": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz",
-      "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA=="
-    },
-    "d3-geo": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.1.tgz",
-      "integrity": "sha512-M6yzGbFRfxzNrVhxDJXzJqSLQ90q1cCyb3EWFZ1LF4eWOBYxFypw7I/NFVBNXKNqxv1bqLathhYvdJ6DC+th3A==",
-      "requires": {
-        "d3-array": ">=2.5"
-      }
-    },
-    "d3-hierarchy": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz",
-      "integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw=="
-    },
-    "d3-interpolate": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
-      "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
-      "requires": {
-        "d3-color": "1 - 2"
-      }
-    },
-    "d3-path": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz",
-      "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA=="
-    },
-    "d3-polygon": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz",
-      "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ=="
-    },
-    "d3-quadtree": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz",
-      "integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw=="
-    },
-    "d3-random": {
-      "version": "2.2.2",
-      "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-2.2.2.tgz",
-      "integrity": "sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw=="
-    },
-    "d3-scale": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz",
-      "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==",
-      "requires": {
-        "d3-array": "^2.3.0",
-        "d3-format": "1 - 2",
-        "d3-interpolate": "1.2.0 - 2",
-        "d3-time": "1 - 2",
-        "d3-time-format": "2 - 3"
-      }
-    },
-    "d3-scale-chromatic": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz",
-      "integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==",
-      "requires": {
-        "d3-color": "1 - 2",
-        "d3-interpolate": "1 - 2"
-      }
-    },
-    "d3-selection": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz",
-      "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA=="
-    },
-    "d3-shape": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.0.0.tgz",
-      "integrity": "sha512-djpGlA779ua+rImicYyyjnOjeubyhql1Jyn1HK0bTyawuH76UQRWXd+pftr67H6Fa8hSwetkgb/0id3agKWykw==",
-      "requires": {
-        "d3-path": "1 - 2"
-      }
-    },
-    "d3-time": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.0.0.tgz",
-      "integrity": "sha512-2mvhstTFcMvwStWd9Tj3e6CEqtOivtD8AUiHT8ido/xmzrI9ijrUUihZ6nHuf/vsScRBonagOdj0Vv+SEL5G3Q=="
-    },
-    "d3-time-format": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz",
-      "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==",
-      "requires": {
-        "d3-time": "1 - 2"
-      }
-    },
-    "d3-timer": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz",
-      "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA=="
-    },
-    "d3-transition": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz",
-      "integrity": "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==",
-      "requires": {
-        "d3-color": "1 - 2",
-        "d3-dispatch": "1 - 2",
-        "d3-ease": "1 - 2",
-        "d3-interpolate": "1 - 2",
-        "d3-timer": "1 - 2"
-      }
-    },
-    "d3-voronoi": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
-      "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
-    },
-    "d3-zoom": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz",
-      "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==",
-      "requires": {
-        "d3-dispatch": "1 - 2",
-        "d3-drag": "2",
-        "d3-interpolate": "1 - 2",
-        "d3-selection": "2",
-        "d3-transition": "2"
-      }
-    },
-    "debounce": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
-      "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
-    },
-    "delaunator": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
-      "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag=="
-    },
-    "iconv-lite": {
-      "version": "0.4.24",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-      "requires": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      }
-    },
-    "internmap": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.0.tgz",
-      "integrity": "sha512-SdoDWwNOTE2n4JWUsLn4KXZGuZPjPF9yyOGc8bnfWnBQh7BD/l80rzSznKc/r4Y0aQ7z3RTk9X+tV4tHBpu+dA=="
-    },
-    "js-tokens": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
-    },
-    "lodash": {
-      "version": "4.17.20",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
-    },
-    "loose-envify": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
-      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-      "requires": {
-        "js-tokens": "^3.0.0 || ^4.0.0"
-      }
-    },
-    "math-expression-evaluator": {
-      "version": "1.3.7",
-      "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.3.7.tgz",
-      "integrity": "sha512-nrbaifCl42w37hYd6oRLvoymFK42tWB+WQTMFtksDGQMi5GvlJwnz/CsS30FFAISFLtX+A0csJ0xLiuuyyec7w=="
-    },
-    "object-assign": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
-    },
-    "prop-types": {
-      "version": "15.7.2",
-      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
-      "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
-      "requires": {
-        "loose-envify": "^1.4.0",
-        "object-assign": "^4.1.1",
-        "react-is": "^16.8.1"
-      }
-    },
-    "react-is": {
-      "version": "16.13.1",
-      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
-      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
-    },
-    "react-use-measure": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.0.1.tgz",
-      "integrity": "sha512-lFfHiqcXbJ2/6aUkZwt8g5YYM7EGqNVxJhMqMPqv1BVXRKp8D7jYLlmma0SvhRY4WYxxkZpCdbJvhDylb5gcEA==",
-      "requires": {
-        "debounce": "^1.2.0"
-      }
-    },
-    "reduce-css-calc": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz",
-      "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=",
-      "requires": {
-        "balanced-match": "^0.4.2",
-        "math-expression-evaluator": "^1.2.14",
-        "reduce-function-call": "^1.0.1"
-      }
-    },
-    "reduce-function-call": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz",
-      "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==",
-      "requires": {
-        "balanced-match": "^1.0.0"
-      },
-      "dependencies": {
-        "balanced-match": {
-          "version": "1.0.0",
-          "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
-          "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
-        }
-      }
-    },
-    "resize-observer-polyfill": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
-      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
-    },
-    "rw": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
-      "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
-    },
-    "safer-buffer": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    "prettier": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
+      "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
+      "dev": true
     }
     }
   }
   }
 }
 }

+ 0 - 3
server/api/cluster_handler.go

@@ -2,7 +2,6 @@ package api
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"net/http"
 	"strconv"
 	"strconv"
 
 
@@ -58,8 +57,6 @@ func (app *App) HandleCreateProjectCluster(w http.ResponseWriter, r *http.Reques
 
 
 	clusterExt := cluster.Externalize()
 	clusterExt := cluster.Externalize()
 
 
-	fmt.Println("CLUSTER EXTERNAL PROJECT ID", clusterExt.ProjectID, cluster.ProjectID)
-
 	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
 	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 		return

+ 2 - 2
server/api/git_action_handler.go

@@ -121,7 +121,7 @@ func (app *App) createGitActionFromForm(
 	})
 	})
 
 
 	if err != nil {
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		app.handleErrorInternal(err, w)
 		return nil
 		return nil
 	}
 	}
 
 
@@ -143,7 +143,7 @@ func (app *App) createGitActionFromForm(
 	_, err = gaRunner.Setup()
 	_, err = gaRunner.Setup()
 
 
 	if err != nil {
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		app.handleErrorInternal(err, w)
 		return nil
 		return nil
 	}
 	}
 
 

+ 0 - 2
server/api/git_repo_handler.go

@@ -134,7 +134,6 @@ func (app *App) HandleGetBranches(w http.ResponseWriter, r *http.Request) {
 	// List all branches for a specified repo
 	// List all branches for a specified repo
 	branches, _, err := client.Repositories.ListBranches(context.Background(), owner, name, nil)
 	branches, _, err := client.Repositories.ListBranches(context.Background(), owner, name, nil)
 	if err != nil {
 	if err != nil {
-		fmt.Println(err)
 		return
 		return
 	}
 	}
 
 
@@ -185,7 +184,6 @@ func (app *App) HandleGetBranchContents(w http.ResponseWriter, r *http.Request)
 
 
 	// Ret2: recursively traverse all dirs to create config bundle (case on type == dir)
 	// Ret2: recursively traverse all dirs to create config bundle (case on type == dir)
 	// https://api.github.com/repos/porter-dev/porter/contents?ref=frontend-graph
 	// https://api.github.com/repos/porter-dev/porter/contents?ref=frontend-graph
-	// fmt.Println(res)
 	json.NewEncoder(w).Encode(res)
 	json.NewEncoder(w).Encode(res)
 }
 }
 
 

+ 3 - 9
server/api/integration_handler.go

@@ -66,15 +66,13 @@ func (app *App) HandleListRepoIntegrations(w http.ResponseWriter, r *http.Reques
 
 
 // HandleCreateGCPIntegration creates a new GCP integration in the DB
 // HandleCreateGCPIntegration creates a new GCP integration in the DB
 func (app *App) HandleCreateGCPIntegration(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleCreateGCPIntegration(w http.ResponseWriter, r *http.Request) {
-	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+	userID, err := app.getUserIDFromRequest(r)
 
 
 	if err != nil {
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 		return
 	}
 	}
 
 
-	userID, _ := session.Values["user_id"].(uint)
-
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 
 	if err != nil || projID == 0 {
 	if err != nil || projID == 0 {
@@ -129,15 +127,13 @@ func (app *App) HandleCreateGCPIntegration(w http.ResponseWriter, r *http.Reques
 
 
 // HandleCreateAWSIntegration creates a new AWS integration in the DB
 // HandleCreateAWSIntegration creates a new AWS integration in the DB
 func (app *App) HandleCreateAWSIntegration(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleCreateAWSIntegration(w http.ResponseWriter, r *http.Request) {
-	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+	userID, err := app.getUserIDFromRequest(r)
 
 
 	if err != nil {
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 		return
 	}
 	}
 
 
-	userID, _ := session.Values["user_id"].(uint)
-
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 
 	if err != nil || projID == 0 {
 	if err != nil || projID == 0 {
@@ -192,15 +188,13 @@ func (app *App) HandleCreateAWSIntegration(w http.ResponseWriter, r *http.Reques
 
 
 // HandleCreateBasicAuthIntegration creates a new basic auth integration in the DB
 // HandleCreateBasicAuthIntegration creates a new basic auth integration in the DB
 func (app *App) HandleCreateBasicAuthIntegration(w http.ResponseWriter, r *http.Request) {
 func (app *App) HandleCreateBasicAuthIntegration(w http.ResponseWriter, r *http.Request) {
-	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+	userID, err := app.getUserIDFromRequest(r)
 
 
 	if err != nil {
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 		return
 	}
 	}
 
 
-	userID, _ := session.Values["user_id"].(uint)
-
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 
 	if err != nil || projID == 0 {
 	if err != nil || projID == 0 {

+ 0 - 5
server/api/integration_handler_test.go

@@ -2,7 +2,6 @@ package api_test
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"net/http"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
@@ -269,8 +268,6 @@ func publicIntBodyValidator(c *publicIntTest, tester *tester, t *testing.T) {
 
 
 	bytes := tester.rr.Body.Bytes()
 	bytes := tester.rr.Body.Bytes()
 
 
-	fmt.Println(string(bytes))
-
 	json.Unmarshal(bytes, &gotBody)
 	json.Unmarshal(bytes, &gotBody)
 	json.Unmarshal([]byte(c.expBody), &expBody)
 	json.Unmarshal([]byte(c.expBody), &expBody)
 
 
@@ -312,8 +309,6 @@ func basicIntBodyValidator(c *publicIntTest, tester *tester, t *testing.T) {
 
 
 	bytes := tester.rr.Body.Bytes()
 	bytes := tester.rr.Body.Bytes()
 
 
-	fmt.Println(string(bytes))
-
 	json.Unmarshal(bytes, &gotBody)
 	json.Unmarshal(bytes, &gotBody)
 	json.Unmarshal([]byte(c.expBody), &expBody)
 	json.Unmarshal([]byte(c.expBody), &expBody)
 
 

+ 45 - 28
server/api/registry_handler.go

@@ -1,6 +1,7 @@
 package api
 package api
 
 
 import (
 import (
+	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"net/http"
 	"net/http"
 	"strconv"
 	"strconv"
@@ -174,6 +175,50 @@ func (app *App) HandleGetProjectRegistryECRToken(w http.ResponseWriter, r *http.
 	}
 	}
 }
 }
 
 
+// HandleGetProjectRegistryDockerhubToken gets a Dockerhub token for a registry
+func (app *App) HandleGetProjectRegistryDockerhubToken(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// list registries and find one that matches the region
+	regs, err := app.Repo.Registry.ListRegistriesByProjectID(uint(projID))
+	var token string
+	var expiresAt *time.Time
+
+	for _, reg := range regs {
+		if reg.BasicIntegrationID != 0 && strings.Contains(reg.URL, "index.docker.io") {
+			basic, err := app.Repo.BasicIntegration.ReadBasicIntegration(reg.BasicIntegrationID)
+
+			if err != nil {
+				app.handleErrorDataRead(err, w)
+				return
+			}
+
+			token = base64.StdEncoding.EncodeToString([]byte(string(basic.Username) + ":" + string(basic.Password)))
+
+			// we'll just set an arbitrary 30-day expiry time (this is not enforced)
+			timeExpires := time.Now().Add(30 * 24 * 3600 * time.Second)
+			expiresAt = &timeExpires
+		}
+	}
+
+	resp := &RegTokenResponse{
+		Token:     token,
+		ExpiresAt: expiresAt,
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(resp); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 type GCRTokenRequestBody struct {
 type GCRTokenRequestBody struct {
 	ServerURL string `json:"server_url"`
 	ServerURL string `json:"server_url"`
 }
 }
@@ -441,32 +486,4 @@ func (app *App) HandleListImages(w http.ResponseWriter, r *http.Request) {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 		return
 	}
 	}
-
-	// ref, err := name.ParseReference("gcr.io/google-containers/pause")
-	// if err != nil {
-	// 	fmt.Println(err)
-	// 	return
-	// }
-
-	// img, err := remote.Image(ref)
-	// if err != nil {
-	// 	fmt.Println(err)
-	// 	return
-	// }
-	// fmt.Println(img.Size())
-
-	// ctx := r.Context()
-	// reg, err := name.NewRegistry("index.docker.io")
-	// if err != nil {
-	// 	fmt.Println("fuk")
-	// 	fmt.Println(err)
-	// 	return
-	// }
-
-	// stuff, err := remote.Catalog(ctx, reg, remote.WithAuthFromKeychain(authn.DefaultKeychain))
-	// if err != nil {
-	// 	fmt.Println(err)
-	// 	return
-	// }
-	// fmt.Println(stuff[0])
 }
 }

+ 0 - 3
server/api/release_handler_test.go

@@ -2,7 +2,6 @@ package api_test
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"net/http"
 	"net/http/httptest"
 	"net/http/httptest"
 	"net/url"
 	"net/url"
@@ -362,8 +361,6 @@ var rollbackReleaseTests = []*releaseTest{
 
 
 				expBodyJSON := releaseStubToReleaseJSON(releaseStub{"wordpress", "default", 3, "1.0.1", release.StatusDeployed})
 				expBodyJSON := releaseStubToReleaseJSON(releaseStub{"wordpress", "default", 3, "1.0.1", release.StatusDeployed})
 
 
-				fmt.Println(rr2.Body.String())
-
 				json.Unmarshal(rr2.Body.Bytes(), gotBody)
 				json.Unmarshal(rr2.Body.Bytes(), gotBody)
 				json.Unmarshal([]byte(expBodyJSON), expBody)
 				json.Unmarshal([]byte(expBodyJSON), expBody)
 
 

+ 19 - 0
server/api/user_handler.go

@@ -480,3 +480,22 @@ func (app *App) sendUser(w http.ResponseWriter, userID uint, email, redirect str
 	}
 	}
 	return nil
 	return nil
 }
 }
+
+func (app *App) getUserIDFromRequest(r *http.Request) (uint, error) {
+	session, err := app.Store.Get(r, app.ServerConf.CookieName)
+
+	if err != nil {
+		return 0, err
+	}
+
+	// first, check for token
+	tok := app.getTokenFromRequest(r)
+
+	if tok != nil {
+		return tok.IBy, nil
+	}
+
+	userID, _ := session.Values["user_id"].(uint)
+
+	return userID, nil
+}

+ 1 - 3
server/router/middleware/auth.go

@@ -670,8 +670,6 @@ func (auth *Auth) isLoggedIn(w http.ResponseWriter, r *http.Request) bool {
 
 
 	tok := auth.getTokenFromRequest(r)
 	tok := auth.getTokenFromRequest(r)
 
 
-	fmt.Println("CHECKED TOKEN FROM REQUEST", tok)
-
 	if tok != nil {
 	if tok != nil {
 		return true
 		return true
 	}
 	}
@@ -680,7 +678,7 @@ func (auth *Auth) isLoggedIn(w http.ResponseWriter, r *http.Request) bool {
 	if err != nil {
 	if err != nil {
 		session.Values["authenticated"] = false
 		session.Values["authenticated"] = false
 		if err := session.Save(r, w); err != nil {
 		if err := session.Save(r, w); err != nil {
-			fmt.Println("error while saving session in isLoggedIn", err)
+			return false
 		}
 		}
 		return false
 		return false
 	}
 	}

+ 10 - 0
server/router/router.go

@@ -753,6 +753,16 @@ func New(a *api.App) *chi.Mux {
 			),
 			),
 		)
 		)
 
 
+		r.Method(
+			"GET",
+			"/projects/{project_id}/registries/dockerhub/token",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleGetProjectRegistryDockerhubToken, l),
+				mw.URLParam,
+				mw.WriteAccess,
+			),
+		)
+
 		r.Method(
 		r.Method(
 			"GET",
 			"GET",
 			"/projects/{project_id}/registries/docr/token",
 			"/projects/{project_id}/registries/docr/token",

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است