Просмотр исходного кода

Merge branch 'belanger/agent-v3-integration' into dev

Alexander Belanger 3 лет назад
Родитель
Сommit
c9593ad3e7
72 измененных файлов с 3186 добавлено и 374 удалено
  1. 73 0
      api/server/handlers/namespace/stream_pod_logs_loki.go
  2. 34 0
      api/server/router/namespace.go
  3. 1 0
      api/types/incident.go
  4. 484 9
      dashboard/package-lock.json
  5. 3 0
      dashboard/package.json
  6. 1 0
      dashboard/src/App.tsx
  7. 0 4
      dashboard/src/assets/Iconly/Bulk/Info Square.svg
  8. 4 0
      dashboard/src/assets/down-arrow.svg
  9. 3 0
      dashboard/src/assets/filter-outline.svg
  10. 4 0
      dashboard/src/assets/folder-outline.svg
  11. 4 0
      dashboard/src/assets/last-run.svg
  12. 6 0
      dashboard/src/assets/sort.svg
  13. 6 0
      dashboard/src/assets/tag.svg
  14. 4 0
      dashboard/src/assets/time.svg
  15. 3 1
      dashboard/src/components/Boilerplate.tsx
  16. 126 0
      dashboard/src/components/CheckboxList.tsx
  17. 73 0
      dashboard/src/components/CheckboxRow.tsx
  18. 2 1
      dashboard/src/components/MultiSelectFilter.tsx
  19. 220 0
      dashboard/src/components/RadioFilter.tsx
  20. 3 3
      dashboard/src/components/Table.tsx
  21. 76 0
      dashboard/src/components/date-time-picker/DateTimePicker.tsx
  22. 885 0
      dashboard/src/components/date-time-picker/react-datepicker.css
  23. 1 1
      dashboard/src/components/form-components/InputRow.tsx
  24. 1 1
      dashboard/src/components/porter-form/FormDebugger.tsx
  25. 2 2
      dashboard/src/main/home/Home.tsx
  26. 89 56
      dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx
  27. 1 1
      dashboard/src/main/home/cluster-dashboard/DashboardHeader.tsx
  28. 10 16
      dashboard/src/main/home/cluster-dashboard/LastRunStatusSelector.tsx
  29. 10 15
      dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx
  30. 9 11
      dashboard/src/main/home/cluster-dashboard/SortSelector.tsx
  31. 15 19
      dashboard/src/main/home/cluster-dashboard/TagFilter.tsx
  32. 1 1
      dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx
  33. 2 2
      dashboard/src/main/home/cluster-dashboard/chart/JobRunTable.tsx
  34. 1 1
      dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx
  35. 1 1
      dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx
  36. 1 1
      dashboard/src/main/home/cluster-dashboard/databases/DatabasesList.tsx
  37. 1 1
      dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx
  38. 1 1
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx
  39. 26 18
      dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx
  40. 131 10
      dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx
  41. 145 113
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  42. 2 2
      dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx
  43. 1 1
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx
  44. 3 3
      dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx
  45. 419 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx
  46. 110 0
      dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts
  47. 3 4
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx
  48. 2 7
      dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx
  49. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx
  50. 1 0
      dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx
  51. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx
  52. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx
  53. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PullRequestCard.tsx
  54. 1 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx
  55. 0 1
      dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx
  56. 93 44
      dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx
  57. 1 1
      dashboard/src/main/home/cluster-dashboard/stacks/components/NewEnvGroupForm.tsx
  58. 2 3
      dashboard/src/main/home/cluster-dashboard/stacks/launch/components/styles.tsx
  59. 1 1
      dashboard/src/main/home/dashboard/ClusterList.tsx
  60. 1 1
      dashboard/src/main/home/infrastructure/InfrastructureList.tsx
  61. 1 1
      dashboard/src/main/home/integrations/IntegrationList.tsx
  62. 1 1
      dashboard/src/main/home/integrations/IntegrationRow.tsx
  63. 1 1
      dashboard/src/main/home/launch/TemplateList.tsx
  64. 1 1
      dashboard/src/main/home/launch/launch-flow/SourcePage.tsx
  65. 0 1
      dashboard/src/main/home/new-project/NewProject.tsx
  66. 3 1
      dashboard/src/main/home/new-project/WelcomeForm.tsx
  67. 1 1
      dashboard/src/main/home/provisioner/ProvisionerSettings.tsx
  68. 54 0
      dashboard/src/shared/api.tsx
  69. 7 1
      dashboard/webpack.config.js
  70. 1 1
      docs/deploy/addons/strapi.md
  71. 6 3
      internal/kubernetes/agent.go
  72. 4 0
      internal/kubernetes/porter_agent/v2/agent_server.go

+ 73 - 0
api/server/handlers/namespace/stream_pod_logs_loki.go

@@ -0,0 +1,73 @@
+package namespace
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/websocket"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type StreamPodLogsLokiHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewStreamPodLogsLokiHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *StreamPodLogsLokiHandler {
+	return &StreamPodLogsLokiHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *StreamPodLogsLokiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	request := &types.GetLogRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	safeRW := r.Context().Value(types.RequestCtxWebsocketKey).(*websocket.WebsocketSafeReadWriter)
+
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	agent, err := c.GetAgent(r, cluster, "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if request.StartRange == nil {
+		dayAgo := time.Now().Add(-24 * time.Hour)
+		request.StartRange = &dayAgo
+	}
+
+	startTime, err := request.StartRange.MarshalText()
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	err = agent.StreamPorterAgentLokiLog([]string{
+		fmt.Sprintf("pod=%s", request.PodSelector),
+		fmt.Sprintf("namespace=%s", request.Namespace),
+	}, string(startTime), request.SearchParam, 0, safeRW)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+}

+ 34 - 0
api/server/router/namespace.go

@@ -420,6 +420,40 @@ func getNamespaceRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/logs/loki -> namespace.NewStreamPodLogsLokiHandler
+	streamPodLogsLokiEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf(
+					"%s/logs/loki",
+					relPath,
+				),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+			},
+			IsWebsocket: true,
+		},
+	)
+
+	streamPodLogsLokiHandler := namespace.NewStreamPodLogsLokiHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: streamPodLogsLokiEndpoint,
+		Handler:  streamPodLogsLokiHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/jobs/stream -> namespace.NewStreamJobRunsHandler
 	streamJobRunsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 1 - 0
api/types/incident.go

@@ -97,6 +97,7 @@ type GetLogRequest struct {
 	Limit       uint       `schema:"limit"`
 	StartRange  *time.Time `schema:"start_range"`
 	EndRange    *time.Time `schema:"end_range"`
+	SearchParam string     `schema:"search_param"`
 	PodSelector string     `schema:"pod_selector" form:"required"`
 	Namespace   string     `schema:"namespace" form:"required"`
 }

+ 484 - 9
dashboard/package-lock.json

@@ -50,6 +50,7 @@
         "react": "^16.13.1",
         "react-ace": "^9.1.3",
         "react-color": "^2.19.3",
+        "react-datepicker": "^4.8.0",
         "react-dom": "^16.13.1",
         "react-error-boundary": "^3.1.3",
         "react-infinite-scroll-component": "^6.1.0",
@@ -88,6 +89,7 @@
         "@types/random-words": "^1.1.0",
         "@types/react": "^16.14.14",
         "@types/react-color": "^3.0.6",
+        "@types/react-datepicker": "^4.4.2",
         "@types/react-dom": "^16.9.8",
         "@types/react-modal": "^3.10.6",
         "@types/react-router": "^5.1.8",
@@ -100,6 +102,7 @@
         "babel-loader": "^8.2.2",
         "babel-plugin-lodash": "^3.3.4",
         "babel-plugin-styled-components": "^1.13.3",
+        "css-loader": "^5.2.7",
         "file-loader": "^6.1.0",
         "html-webpack-plugin": "^4.5.0",
         "prettier": "2.2.1",
@@ -2213,6 +2216,15 @@
       "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
       "dev": true
     },
+    "node_modules/@popperjs/core": {
+      "version": "2.11.6",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
+      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
     "node_modules/@sentry/browser": {
       "version": "6.15.0",
       "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.15.0.tgz",
@@ -2847,6 +2859,18 @@
         "@types/reactcss": "*"
       }
     },
+    "node_modules/@types/react-datepicker": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.4.2.tgz",
+      "integrity": "sha512-g8DhWvYmaIMLzVrIEVLXncylyImyBaoPsEUr3yR13JDaaHoebhDorqnVv4tLkNGa8SjBB8SAOQvxD5jaPNBX8A==",
+      "dev": true,
+      "dependencies": {
+        "@popperjs/core": "^2.9.2",
+        "@types/react": "*",
+        "date-fns": "^2.0.1",
+        "react-popper": "^2.2.5"
+      }
+    },
     "node_modules/@types/react-dom": {
       "version": "16.9.14",
       "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.14.tgz",
@@ -5274,6 +5298,66 @@
         "node": ">=4"
       }
     },
+    "node_modules/css-loader": {
+      "version": "5.2.7",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz",
+      "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==",
+      "dev": true,
+      "dependencies": {
+        "icss-utils": "^5.1.0",
+        "loader-utils": "^2.0.0",
+        "postcss": "^8.2.15",
+        "postcss-modules-extract-imports": "^3.0.0",
+        "postcss-modules-local-by-default": "^4.0.0",
+        "postcss-modules-scope": "^3.0.0",
+        "postcss-modules-values": "^4.0.0",
+        "postcss-value-parser": "^4.1.0",
+        "schema-utils": "^3.0.0",
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^4.27.0 || ^5.0.0"
+      }
+    },
+    "node_modules/css-loader/node_modules/loader-utils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
+      "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
+      "dev": true,
+      "dependencies": {
+        "big.js": "^5.2.2",
+        "emojis-list": "^3.0.0",
+        "json5": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=8.9.0"
+      }
+    },
+    "node_modules/css-loader/node_modules/schema-utils": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
+      "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+      "dev": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.8",
+        "ajv": "^6.12.5",
+        "ajv-keywords": "^3.5.2"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
     "node_modules/css-select": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
@@ -5336,6 +5420,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/csstype": {
       "version": "2.6.19",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz",
@@ -5419,6 +5515,18 @@
         "d3-time": "1 - 2"
       }
     },
+    "node_modules/date-fns": {
+      "version": "2.29.3",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
+      "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
+      "engines": {
+        "node": ">=0.11"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/date-fns"
+      }
+    },
     "node_modules/debounce": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -7470,6 +7578,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/icss-utils": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+      "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+      "dev": true,
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
     "node_modules/ieee754": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -8689,6 +8809,7 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
       "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
+      "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
       "dependencies": {
         "@babel/runtime": "^7.12.1",
         "tiny-warning": "^1.0.3"
@@ -9048,6 +9169,18 @@
       "dev": true,
       "optional": true
     },
+    "node_modules/nanoid": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
+      "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
+      "dev": true,
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
     "node_modules/nanomatch": {
       "version": "1.2.13",
       "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -9805,6 +9938,102 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/postcss": {
+      "version": "8.4.17",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz",
+      "integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.4",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-modules-extract-imports": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
+      "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+      "dev": true,
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-local-by-default": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz",
+      "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==",
+      "dev": true,
+      "dependencies": {
+        "icss-utils": "^5.0.0",
+        "postcss-selector-parser": "^6.0.2",
+        "postcss-value-parser": "^4.1.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-scope": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
+      "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
+      "dev": true,
+      "dependencies": {
+        "postcss-selector-parser": "^6.0.4"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-values": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+      "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+      "dev": true,
+      "dependencies": {
+        "icss-utils": "^5.0.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.0.10",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+      "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+      "dev": true,
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/postcss-value-parser": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
@@ -10130,6 +10359,23 @@
         "react": "*"
       }
     },
+    "node_modules/react-datepicker": {
+      "version": "4.8.0",
+      "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.8.0.tgz",
+      "integrity": "sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==",
+      "dependencies": {
+        "@popperjs/core": "^2.9.2",
+        "classnames": "^2.2.6",
+        "date-fns": "^2.24.0",
+        "prop-types": "^15.7.2",
+        "react-onclickoutside": "^6.12.0",
+        "react-popper": "^2.2.5"
+      },
+      "peerDependencies": {
+        "react": "^16.9.0 || ^17 || ^18",
+        "react-dom": "^16.9.0 || ^17 || ^18"
+      }
+    },
     "node_modules/react-dom": {
       "version": "16.14.0",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
@@ -10159,6 +10405,11 @@
         "react": ">=16.13.1"
       }
     },
+    "node_modules/react-fast-compare": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
+      "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
+    },
     "node_modules/react-infinite-scroll-component": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
@@ -10198,6 +10449,33 @@
         "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17"
       }
     },
+    "node_modules/react-onclickoutside": {
+      "version": "6.12.2",
+      "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
+      "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==",
+      "funding": {
+        "type": "individual",
+        "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
+      },
+      "peerDependencies": {
+        "react": "^15.5.x || ^16.x || ^17.x || ^18.x",
+        "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
+      }
+    },
+    "node_modules/react-popper": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
+      "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
+      "dependencies": {
+        "react-fast-compare": "^3.0.1",
+        "warning": "^4.0.2"
+      },
+      "peerDependencies": {
+        "@popperjs/core": "^2.0.0",
+        "react": "^16.8.0 || ^17 || ^18",
+        "react-dom": "^16.8.0 || ^17 || ^18"
+      }
+    },
     "node_modules/react-refresh": {
       "version": "0.10.0",
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz",
@@ -11296,6 +11574,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/source-map-loader": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz",
@@ -15504,7 +15791,8 @@
     "@icons/material": {
       "version": "0.2.4",
       "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
-      "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw=="
+      "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==",
+      "requires": {}
     },
     "@ironplans/api": {
       "version": "0.4.1",
@@ -15633,7 +15921,8 @@
     "@material-ui/types": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
-      "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A=="
+      "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==",
+      "requires": {}
     },
     "@material-ui/utils": {
       "version": "4.11.3",
@@ -15701,6 +15990,11 @@
       "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
       "dev": true
     },
+    "@popperjs/core": {
+      "version": "2.11.6",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
+      "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
+    },
     "@sentry/browser": {
       "version": "6.15.0",
       "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.15.0.tgz",
@@ -15932,7 +16226,8 @@
       "version": "7.2.1",
       "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz",
       "integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "@types/body-parser": {
       "version": "1.19.2",
@@ -16272,6 +16567,18 @@
         "@types/reactcss": "*"
       }
     },
+    "@types/react-datepicker": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.4.2.tgz",
+      "integrity": "sha512-g8DhWvYmaIMLzVrIEVLXncylyImyBaoPsEUr3yR13JDaaHoebhDorqnVv4tLkNGa8SjBB8SAOQvxD5jaPNBX8A==",
+      "dev": true,
+      "requires": {
+        "@popperjs/core": "^2.9.2",
+        "@types/react": "*",
+        "date-fns": "^2.0.1",
+        "react-popper": "^2.2.5"
+      }
+    },
     "@types/react-dom": {
       "version": "16.9.14",
       "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.14.tgz",
@@ -17006,13 +17313,15 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
       "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "ajv-keywords": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
       "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "anser": {
       "version": "2.1.0",
@@ -17971,7 +18280,8 @@
     "cohere-sentry": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/cohere-sentry/-/cohere-sentry-1.0.1.tgz",
-      "integrity": "sha512-OHdKcc8LED8X/JQKlMD0Zapb4rcOkPu0m11+okHouMDep1/MvyOG4JXcK4Mo3sabJT65yozc9Uo+nJfSWzaFcg=="
+      "integrity": "sha512-OHdKcc8LED8X/JQKlMD0Zapb4rcOkPu0m11+okHouMDep1/MvyOG4JXcK4Mo3sabJT65yozc9Uo+nJfSWzaFcg==",
+      "requires": {}
     },
     "collection-visit": {
       "version": "1.0.0",
@@ -18353,6 +18663,48 @@
       "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
       "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
     },
+    "css-loader": {
+      "version": "5.2.7",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz",
+      "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==",
+      "dev": true,
+      "requires": {
+        "icss-utils": "^5.1.0",
+        "loader-utils": "^2.0.0",
+        "postcss": "^8.2.15",
+        "postcss-modules-extract-imports": "^3.0.0",
+        "postcss-modules-local-by-default": "^4.0.0",
+        "postcss-modules-scope": "^3.0.0",
+        "postcss-modules-values": "^4.0.0",
+        "postcss-value-parser": "^4.1.0",
+        "schema-utils": "^3.0.0",
+        "semver": "^7.3.5"
+      },
+      "dependencies": {
+        "loader-utils": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
+          "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
+          "dev": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
+        "schema-utils": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
+          "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
+          "dev": true,
+          "requires": {
+            "@types/json-schema": "^7.0.8",
+            "ajv": "^6.12.5",
+            "ajv-keywords": "^3.5.2"
+          }
+        }
+      }
+    },
     "css-select": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
@@ -18397,6 +18749,12 @@
       "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=",
       "dev": true
     },
+    "cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true
+    },
     "csstype": {
       "version": "2.6.19",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz",
@@ -18480,6 +18838,11 @@
         "d3-time": "1 - 2"
       }
     },
+    "date-fns": {
+      "version": "2.29.3",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
+      "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
+    },
     "debounce": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -20131,6 +20494,13 @@
         "safer-buffer": ">= 2.1.2 < 3.0.0"
       }
     },
+    "icss-utils": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+      "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+      "dev": true,
+      "requires": {}
+    },
     "ieee754": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -20912,7 +21282,8 @@
     "markdown-to-jsx": {
       "version": "7.1.3",
       "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.3.tgz",
-      "integrity": "sha512-jtQ6VyT7rMT5tPV0g2EJakEnXLiPksnvlYtwQsVVZ611JsWGN8bQ1tVSDX4s6JllfEH6wmsYxNjTUAMrPmNA8w=="
+      "integrity": "sha512-jtQ6VyT7rMT5tPV0g2EJakEnXLiPksnvlYtwQsVVZ611JsWGN8bQ1tVSDX4s6JllfEH6wmsYxNjTUAMrPmNA8w==",
+      "requires": {}
     },
     "material-colors": {
       "version": "1.2.6",
@@ -21323,6 +21694,12 @@
       "dev": true,
       "optional": true
     },
+    "nanoid": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
+      "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
+      "dev": true
+    },
     "nanomatch": {
       "version": "1.2.13",
       "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -21940,6 +22317,63 @@
       "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
       "dev": true
     },
+    "postcss": {
+      "version": "8.4.17",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz",
+      "integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==",
+      "dev": true,
+      "requires": {
+        "nanoid": "^3.3.4",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      }
+    },
+    "postcss-modules-extract-imports": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
+      "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+      "dev": true,
+      "requires": {}
+    },
+    "postcss-modules-local-by-default": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz",
+      "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==",
+      "dev": true,
+      "requires": {
+        "icss-utils": "^5.0.0",
+        "postcss-selector-parser": "^6.0.2",
+        "postcss-value-parser": "^4.1.0"
+      }
+    },
+    "postcss-modules-scope": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
+      "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
+      "dev": true,
+      "requires": {
+        "postcss-selector-parser": "^6.0.4"
+      }
+    },
+    "postcss-modules-values": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+      "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+      "dev": true,
+      "requires": {
+        "icss-utils": "^5.0.0"
+      }
+    },
+    "postcss-selector-parser": {
+      "version": "6.0.10",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+      "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+      "dev": true,
+      "requires": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      }
+    },
     "postcss-value-parser": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
@@ -22222,6 +22656,19 @@
         "tinycolor2": "^1.4.1"
       }
     },
+    "react-datepicker": {
+      "version": "4.8.0",
+      "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.8.0.tgz",
+      "integrity": "sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==",
+      "requires": {
+        "@popperjs/core": "^2.9.2",
+        "classnames": "^2.2.6",
+        "date-fns": "^2.24.0",
+        "prop-types": "^15.7.2",
+        "react-onclickoutside": "^6.12.0",
+        "react-popper": "^2.2.5"
+      }
+    },
     "react-dom": {
       "version": "16.14.0",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
@@ -22241,6 +22688,11 @@
         "@babel/runtime": "^7.12.5"
       }
     },
+    "react-fast-compare": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
+      "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
+    },
     "react-infinite-scroll-component": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz",
@@ -22270,6 +22722,21 @@
         "warning": "^4.0.3"
       }
     },
+    "react-onclickoutside": {
+      "version": "6.12.2",
+      "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
+      "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==",
+      "requires": {}
+    },
+    "react-popper": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
+      "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
+      "requires": {
+        "react-fast-compare": "^3.0.1",
+        "warning": "^4.0.2"
+      }
+    },
     "react-refresh": {
       "version": "0.10.0",
       "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz",
@@ -22317,7 +22784,8 @@
     "react-table": {
       "version": "7.7.0",
       "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.7.0.tgz",
-      "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA=="
+      "integrity": "sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==",
+      "requires": {}
     },
     "react-transition-group": {
       "version": "4.4.2",
@@ -23204,6 +23672,12 @@
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
       "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
     },
+    "source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "dev": true
+    },
     "source-map-loader": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz",
@@ -25470,7 +25944,8 @@
       "version": "7.5.5",
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
       "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "xtend": {
       "version": "4.0.2",

+ 3 - 0
dashboard/package.json

@@ -45,6 +45,7 @@
     "react": "^16.13.1",
     "react-ace": "^9.1.3",
     "react-color": "^2.19.3",
+    "react-datepicker": "^4.8.0",
     "react-dom": "^16.13.1",
     "react-error-boundary": "^3.1.3",
     "react-infinite-scroll-component": "^6.1.0",
@@ -89,6 +90,7 @@
     "@types/random-words": "^1.1.0",
     "@types/react": "^16.14.14",
     "@types/react-color": "^3.0.6",
+    "@types/react-datepicker": "^4.4.2",
     "@types/react-dom": "^16.9.8",
     "@types/react-modal": "^3.10.6",
     "@types/react-router": "^5.1.8",
@@ -101,6 +103,7 @@
     "babel-loader": "^8.2.2",
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-styled-components": "^1.13.3",
+    "css-loader": "^5.2.7",
     "file-loader": "^6.1.0",
     "html-webpack-plugin": "^4.5.0",
     "prettier": "2.2.1",

+ 1 - 0
dashboard/src/App.tsx

@@ -24,6 +24,7 @@ const GlobalStyle = createGlobalStyle`
   * {
     box-sizing: border-box;
     font-family: 'Work Sans', sans-serif;
+    color-scheme: dark;
   }
   
   body {

+ 0 - 4
dashboard/src/assets/Iconly/Bulk/Info Square.svg

@@ -1,4 +0,0 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path opacity="0.4" d="M16.34 1.9998H7.67C4.28 1.9998 2 4.3798 2 7.9198V16.0898C2 19.6198 4.28 21.9998 7.67 21.9998H16.34C19.73 21.9998 22 19.6198 22 16.0898V7.9198C22 4.3798 19.73 1.9998 16.34 1.9998Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1247 8.1893C11.1247 8.6713 11.5157 9.0643 11.9947 9.0643C12.4877 9.0643 12.8797 8.6713 12.8797 8.1893C12.8797 7.7073 12.4877 7.3143 12.0047 7.3143C11.5197 7.3143 11.1247 7.7073 11.1247 8.1893ZM12.8697 11.3621C12.8697 10.8801 12.4767 10.4871 11.9947 10.4871C11.5127 10.4871 11.1197 10.8801 11.1197 11.3621V15.7821C11.1197 16.2641 11.5127 16.6571 11.9947 16.6571C12.4767 16.6571 12.8697 16.2641 12.8697 15.7821V11.3621Z" fill="white"/>
-</svg>

+ 4 - 0
dashboard/src/assets/down-arrow.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.2743 19.75V4.75" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M18.2987 13.7002L12.2747 19.7502L6.24969 13.7002" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
dashboard/src/assets/filter-outline.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29332 22L14.0696 19.7519V13.8603L21.5593 6.26456C21.8416 5.97995 22 5.58933 22 5.18027V3.51754C22 2.67869 21.3417 2 20.5295 2H3.47049C2.65826 2 2 2.67869 2 3.51754V5.2183C2 5.60431 2.14169 5.97534 2.39719 6.2565L9.29332 13.8603V22Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
dashboard/src/assets/folder-outline.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M21.4446 15.7579C21.4446 19.336 19.336 21.4446 15.7579 21.4446H7.97172C4.38443 21.4446 2.27588 19.336 2.27588 15.7579V7.9626C2.27588 4.38444 3.5903 2.27588 7.16846 2.27588H9.16749C9.88576 2.27588 10.5621 2.61406 10.9931 3.18868L11.9059 4.40269C12.3378 4.97618 13.0135 5.31406 13.7315 5.31549H16.5611C20.1484 5.31549 21.472 7.14108 21.472 10.7923L21.4446 15.7579Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.05893 14.4891H16.6524" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
dashboard/src/assets/last-run.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.3002 12.2513L20.2502 12.2513" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3002 7.25031L3.36317 12.2513L11.3002 17.2523L11.3002 7.25031Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 6 - 0
dashboard/src/assets/sort.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.8396 20.1642V6.54645" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M20.9172 16.0681L16.8394 20.1648L12.7617 16.0681" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.91112 3.83289V17.4507" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.83344 7.929L6.91121 3.83234L10.989 7.929" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 6 - 0
dashboard/src/assets/tag.svg

@@ -0,0 +1,6 @@
+<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.8055 18.9994V3" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.19465 3.00064V19" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.00065 14.8054L19 14.8054" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M18.9994 7.19465L3.00004 7.19465" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
dashboard/src/assets/time.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M21.2498 12.0005C21.2498 17.1095 17.1088 21.2505 11.9998 21.2505C6.8908 21.2505 2.7498 17.1095 2.7498 12.0005C2.7498 6.89149 6.8908 2.75049 11.9998 2.75049C17.1088 2.75049 21.2498 6.89149 21.2498 12.0005Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M15.4314 14.9429L11.6614 12.6939V7.84686" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 1
dashboard/src/components/Boilerplate.tsx

@@ -4,10 +4,12 @@ import styled from "styled-components";
 
 type Props = {};
 
-export const Boilerplate: React.FC<Props> = (props) => {
+const Boilerplate: React.FC<Props> = (props) => {
   const [someState, setSomeState] = useState("");
 
   return <StyledBoilerplate></StyledBoilerplate>;
 };
 
+export default Boilerplate;
+
 const StyledBoilerplate = styled.div``;

+ 126 - 0
dashboard/src/components/CheckboxList.tsx

@@ -0,0 +1,126 @@
+import React, { useEffect } from "react";
+import styled from "styled-components";
+
+type PropsType = {
+  label?: string;
+  options: { disabled?: boolean; value: any; label: string }[];
+  selected: { value: any; label: string }[];
+  setSelected: (x: { value: any; label: string }[]) => void;
+};
+
+const arraysEqual = (a: any, b: any) => {
+  if (a === b) return true;
+  if (a == null || b == null) return false;
+  if (a.length !== b.length) return false;
+
+  // If you don't care about the order of the elements inside
+  // the array, you should sort both arrays here.
+  // Please note that calling sort on an array will modify that array.
+  // you might want to clone your array first.
+
+  for (var i = 0; i < a.length; ++i) {
+    if (a[i] !== b[i]) return false;
+  }
+  return true;
+};
+
+const CheckboxList = ({ label, options, selected, setSelected }: PropsType) => {
+  let onSelectOption = (option: { value: any; label: string }) => {
+    const tmp = [...selected];
+    if (
+      tmp.filter(
+        (e) => e.value === option.value || arraysEqual(e.value, option.value)
+      ).length === 0
+    ) {
+      setSelected([...tmp, option]);
+    } else {
+      tmp.forEach((x, i) => {
+        if (x.value === option.value || arraysEqual(x.value, option.value)) {
+          tmp.splice(i, 1);
+        }
+      });
+      setSelected(tmp);
+    }
+  };
+
+  return (
+    <StyledCheckboxList>
+      {label && <Label>{label}</Label>}
+      {options.map((option: { value: any; label: string }, i: number) => {
+        return (
+          <CheckboxOption
+            isLast={i === options.length - 1}
+            onClick={() => onSelectOption(option)}
+            key={i}
+          >
+            <Checkbox
+              checked={
+                selected.filter(
+                  (e) =>
+                    e.value === option.value ||
+                    arraysEqual(e.value, option.value)
+                ).length > 0
+              }
+            >
+              <i className="material-icons">done</i>
+            </Checkbox>
+            <Text>{option.label}</Text>
+          </CheckboxOption>
+        );
+      })}
+    </StyledCheckboxList>
+  );
+};
+export default CheckboxList;
+
+const Text = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  word-break: anywhere;
+  margin-right: 10px;
+`;
+
+const Checkbox = styled.div`
+  width: 14px;
+  height: 14px;
+  min-width: 14px;
+  border: 1px solid #ffffff55;
+  margin: 1px 10px 0px 1px;
+  border-radius: 3px;
+  background: ${(props: { checked: boolean }) =>
+    props.checked ? "#ffffff22" : "#ffffff11"};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 12px;
+    padding-left: 0px;
+    display: ${(props: { checked: boolean }) => (props.checked ? "" : "none")};
+  }
+`;
+
+const CheckboxOption = styled.div<{ isLast: boolean }>`
+  width: 100%;
+  height: 35px;
+  padding-left: 10px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  font-size: 13px;
+
+  :hover {
+    background: #ffffff18;
+  }
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+`;
+
+const StyledCheckboxList = styled.div`
+  border-radius: 3px;
+  padding: 0;
+`;

+ 73 - 0
dashboard/src/components/CheckboxRow.tsx

@@ -0,0 +1,73 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+
+type PropsType = {
+  label: string;
+  checked: boolean;
+  toggle: () => void;
+  isRequired?: boolean;
+  disabled?: boolean;
+};
+
+type StateType = {};
+
+export default class CheckboxRow extends Component<PropsType, StateType> {
+  render() {
+    return (
+      <StyledCheckboxRow>
+        <CheckboxWrapper
+          disabled={this.props.disabled}
+          onClick={!this.props.disabled ? this.props.toggle : undefined}
+        >
+          <Checkbox checked={this.props.checked}>
+            <i className="material-icons">done</i>
+          </Checkbox>
+          {this.props.label}
+          {this.props.isRequired && <Required>*</Required>}
+        </CheckboxWrapper>
+      </StyledCheckboxRow>
+    );
+  }
+}
+
+const Required = styled.section`
+  margin-left: 8px;
+  color: #fc4976;
+`;
+
+const CheckboxWrapper = styled.div<{ disabled?: boolean }>`
+  display: flex;
+  align-items: center;
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
+  font-size: 13px;
+  :hover {
+    > div {
+      background: #ffffff22;
+    }
+  }
+`;
+
+const Checkbox = styled.div<{ checked: boolean }>`
+  width: 16px;
+  height: 16px;
+  border: 1px solid #ffffff55;
+  margin: 1px 10px 0px 1px;
+  border-radius: 3px;
+  background: ${(props) => (props.checked ? "#ffffff22" : "#ffffff11")};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 12px;
+    padding-left: 0px;
+    display: ${(props) => (props.checked ? "" : "none")};
+  }
+`;
+
+const StyledCheckboxRow = styled.div`
+  display: flex;
+  align-items: center;
+  margin-bottom: 15px;
+  margin-top: 20px;
+`;

+ 2 - 1
dashboard/src/components/MultiSelectFilter.tsx

@@ -3,7 +3,7 @@ import React, { useEffect, useState, useRef } from "react";
 import styled from "styled-components";
 import arrow from "assets/arrow-down.svg";
 
-import CheckboxList from "components/form-components/CheckboxList";
+import CheckboxList from "./CheckboxList";
 
 type Props = {
   name: string;
@@ -169,6 +169,7 @@ const Dropdown = styled.div`
   overflow-y: auto;
   margin-bottom: 20px;
   background: #2f3135;
+  padding: 0;
   border-radius: 5px;
   border: 1px solid #aaaabb33;
 `;

+ 220 - 0
dashboard/src/components/RadioFilter.tsx

@@ -0,0 +1,220 @@
+import React, { useEffect, useState, useRef } from "react";
+
+import styled from "styled-components";
+import arrow from "assets/arrow-down.svg";
+
+type Props = {
+  name: string;
+  icon?: any;
+  options: { value: any; label: string }[];
+  selected: any;
+  setSelected: any;
+  noMargin?: boolean;
+  dropdownAlignRight?: boolean;
+};
+
+const RadioFilter: React.FC<Props> = (props) => {
+  const [expanded, setExpanded] = useState(false);
+
+  const wrapperRef = useRef<HTMLInputElement>(null);
+  const parentRef = useRef<HTMLInputElement>(null);
+
+  useEffect(() => {
+    document.addEventListener("mousedown", handleClickOutside.bind(this));
+    return () =>
+      document.removeEventListener("mousedown", handleClickOutside.bind(this));
+  }, []);
+
+  const handleClickOutside = (event: any) => {
+    if (
+      wrapperRef &&
+      wrapperRef.current &&
+      !wrapperRef.current.contains(event.target) &&
+      parentRef &&
+      parentRef.current &&
+      !parentRef.current.contains(event.target)
+    ) {
+      setExpanded(false);
+    }
+  };
+
+  const getLabel = (value: string): any => {
+    let tgt = props.options.find(
+      (element: { value: string; label: string }) => element.value === value
+    );
+    if (tgt) {
+      return tgt.label;
+    }
+  };
+
+  const renderDropdown = () => {
+    let { options } = props;
+    if (expanded) {
+      return (
+        <DropdownWrapper dropdownAlignRight={props.dropdownAlignRight}>
+          <Dropdown ref={wrapperRef}>
+            {options?.length > 0 ? (
+              <ScrollableWrapper>
+                {options.map(
+                  (option: { value: any; label: string }, i: number) => {
+                    return (
+                      <OptionRow
+                        isLast={i === options.length - 1}
+                        onClick={() => {
+                          props.setSelected(option.value);
+                          setExpanded(false);
+                        }}
+                        key={i}
+                        selected={props.selected === option.value}
+                      >
+                        <Text>{option.label}</Text>
+                      </OptionRow>
+                    );
+                  }
+                )}
+              </ScrollableWrapper>
+            ) : (
+              <Placeholder>No options found</Placeholder>
+            )}
+          </Dropdown>
+        </DropdownWrapper>
+      );
+    }
+  };
+
+  return (
+    <Relative>
+      <StyledRadioFilter
+        onClick={() => setExpanded(!expanded)}
+        ref={parentRef}
+        noMargin={props.noMargin}
+      >
+        {props.icon && <FilterIcon src={props.icon} />}
+        <TextAlt>{props.name}</TextAlt>
+        <Bar />
+        <Selected>
+          {props.selected
+            ? props.selected === ""
+              ? "All"
+              : getLabel(props.selected)
+            : ""}
+        </Selected>
+        <DropdownIcon src={arrow} />
+      </StyledRadioFilter>
+      {renderDropdown()}
+    </Relative>
+  );
+};
+
+export default RadioFilter;
+
+const Bar = styled.div`
+  width: 1px;
+  height: calc(18px);
+  background: #494b4f;
+  margin: 0 8px;
+  margin-left: 0;
+`;
+
+const Selected = styled.div`
+  color: #aaaaaa;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  max-width: 120px;
+`;
+
+const Text = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  word-break: anywhere;
+  margin-right: 10px;
+`;
+
+const TextAlt = styled(Text)`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  word-break: anywhere;
+`;
+
+const OptionRow = styled.div<{ isLast: boolean; selected?: boolean }>`
+  width: 100%;
+  height: 35px;
+  padding-left: 10px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  font-size: 13px;
+  background: ${(props) => (props.selected ? "#ffffff11" : "")};
+
+  :hover {
+    background: #ffffff18;
+  }
+`;
+
+const Placeholder = styled.div`
+  color: #aaaabb88;
+  font-size: 12px;
+  width: 100%;
+  height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+`;
+
+const ScrollableWrapper = styled.div`
+  overflow-y: auto;
+  max-height: 350px;
+`;
+
+const Relative = styled.div`
+  position: relative;
+`;
+
+const DropdownWrapper = styled.div<{ dropdownAlignRight?: boolean }>`
+  position: absolute;
+  left: ${(props) => (props.dropdownAlignRight ? "" : "0")};
+  right: ${(props) => (props.dropdownAlignRight ? "0" : "")};
+  z-index: 1;
+  top: calc(100% + 5px);
+`;
+
+const Dropdown = styled.div`
+  width: 260px;
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  background: #2f3135;
+  padding: 0;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const DropdownIcon = styled.img`
+  width: 8px;
+  margin-left: 12px;
+`;
+
+const FilterIcon = styled.img`
+  width: 14px;
+  margin-right: 9px;
+`;
+
+const StyledRadioFilter = styled.div<{ noMargin?: boolean }>`
+  height: 30px;
+  font-size: 13px;
+  position: relative;
+  padding: 10px;
+  background: #26292e;
+  border-radius: 5px;
+  display: flex;
+  align-items: center;
+  margin-right: ${(props) => (props.noMargin ? "" : "10px")};
+  cursor: pointer;
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;

+ 3 - 3
dashboard/src/components/Table.tsx

@@ -365,7 +365,7 @@ const SearchInput = styled.input`
   width: 100%;
   color: white;
   padding: 0;
-  height: 20px;
+  height: 21px;
 `;
 
 const SearchRow = styled.div`
@@ -376,7 +376,7 @@ const SearchRow = styled.div`
   border-radius: 4px;
   user-select: none;
   align-items: center;
-  padding: 10px 0px;
+  padding: 7px 0px;
   min-width: 300px;
   max-width: min-content;
   background: #ffffff11;
@@ -386,7 +386,7 @@ const SearchRow = styled.div`
     height: 18px;
     margin-left: 12px;
     margin-right: 12px;
-    font-size: 20px;
+    font-size: 18px;
   }
 `;
 

+ 76 - 0
dashboard/src/components/date-time-picker/DateTimePicker.tsx

@@ -0,0 +1,76 @@
+import React, { useState } from "react";
+
+import DatePicker from "react-datepicker";
+import time from "assets/time.svg";
+
+import styled from "styled-components";
+
+type Props = {
+  startDate: any;
+  setStartDate: any;
+};
+
+const DateTimePicker: React.FC<Props> = ({ startDate, setStartDate }) => {
+  return (
+    <DateTimePickerWrapper>
+      <TimeIcon src={time} />
+      <link
+        rel="stylesheet"
+        href="https://cdnjs.cloudflare.com/ajax/libs/react-datepicker/2.14.1/react-datepicker.min.css"
+      />
+      <Bar />
+      <StyledDatePicker
+        selected={startDate}
+        onChange={(date: any) => setStartDate(date)}
+        showTimeSelect
+        dateFormat="MMMM d, yyyy h:mm aa"
+      />
+    </DateTimePickerWrapper>
+  );
+};
+
+export default DateTimePicker;
+
+const Bar = styled.div`
+  width: 1px;
+  height: calc(18px);
+  background: #494b4f;
+  margin-left: 8px;
+`;
+
+const TimeIcon = styled.img`
+  width: 16px;
+  height: 16px;
+  z-index: 999;
+`;
+
+const Div = styled.div`
+  display: block;
+`;
+
+const DateTimePickerWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding-right: 42px;
+  margin-right: 10px;
+  padding-left: 8px;
+  height: 30px;
+  border-radius: 5px;
+  border: 1px solid #494b4f;
+  height: 30px;
+  background: #26292e;
+`;
+
+const StyledDatePicker = styled(DatePicker)`
+  border: 0;
+  width: calc(100% + 42px);
+  position: relative;
+  border: none;
+  margin-bottom: 3px;
+  outline-width: 0;
+  background: transparent;
+  text-align: center;
+  padding: 0 15px;
+  font-size: 13px;
+`;

+ 885 - 0
dashboard/src/components/date-time-picker/react-datepicker.css

@@ -0,0 +1,885 @@
+.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle,
+.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow {
+  margin-left: -8px;
+  position: absolute;
+}
+
+.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle,
+.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow,
+.react-datepicker-popper[data-placement^="bottom"]
+  .react-datepicker__triangle::before,
+.react-datepicker-popper[data-placement^="top"]
+  .react-datepicker__triangle::before,
+.react-datepicker__year-read-view--down-arrow::before,
+.react-datepicker__month-read-view--down-arrow::before,
+.react-datepicker__month-year-read-view--down-arrow::before {
+  box-sizing: content-box;
+  position: absolute;
+  border: 8px solid transparent;
+  height: 0;
+  width: 1px;
+}
+
+.react-datepicker-popper[data-placement^="bottom"]
+  .react-datepicker__triangle::before,
+.react-datepicker-popper[data-placement^="top"]
+  .react-datepicker__triangle::before,
+.react-datepicker__year-read-view--down-arrow::before,
+.react-datepicker__month-read-view--down-arrow::before,
+.react-datepicker__month-year-read-view--down-arrow::before {
+  content: "";
+  z-index: -1;
+  border-width: 8px;
+  left: -8px;
+  border-bottom-color: #aeaeae;
+}
+
+.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle {
+  top: 0;
+  margin-top: -8px;
+}
+
+.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle,
+.react-datepicker-popper[data-placement^="bottom"]
+  .react-datepicker__triangle::before {
+  border-top: none;
+  border-bottom-color: #f0f0f0;
+}
+
+.react-datepicker-popper[data-placement^="bottom"]
+  .react-datepicker__triangle::before {
+  top: -1px;
+  border-bottom-color: #aeaeae;
+}
+
+.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow {
+  bottom: 0;
+  margin-bottom: -8px;
+}
+
+.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow,
+.react-datepicker-popper[data-placement^="top"]
+  .react-datepicker__triangle::before,
+.react-datepicker__year-read-view--down-arrow::before,
+.react-datepicker__month-read-view--down-arrow::before,
+.react-datepicker__month-year-read-view--down-arrow::before {
+  border-bottom: none;
+  border-top-color: #fff;
+}
+
+.react-datepicker-popper[data-placement^="top"]
+  .react-datepicker__triangle::before,
+.react-datepicker__year-read-view--down-arrow::before,
+.react-datepicker__month-read-view--down-arrow::before,
+.react-datepicker__month-year-read-view--down-arrow::before {
+  bottom: -1px;
+  border-top-color: #aeaeae;
+}
+
+.react-datepicker-wrapper {
+  display: inline-block;
+  padding: 0;
+  border: 0;
+}
+
+.react-datepicker {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-size: 0.8rem;
+  background-color: #fff;
+  color: #000;
+  border: 1px solid #aeaeae;
+  border-radius: 0.3rem;
+  display: inline-block;
+  position: relative;
+}
+
+.react-datepicker--time-only .react-datepicker__triangle {
+  left: 35px;
+}
+
+.react-datepicker--time-only .react-datepicker__time-container {
+  border-left: 0;
+}
+
+.react-datepicker--time-only .react-datepicker__time {
+  border-radius: 0.3rem;
+}
+
+.react-datepicker--time-only .react-datepicker__time-box {
+  border-radius: 0.3rem;
+}
+
+.react-datepicker__triangle {
+  position: absolute;
+  left: 50px;
+}
+
+.react-datepicker-popper {
+  z-index: 1;
+}
+
+.react-datepicker-popper[data-placement^="bottom"] {
+  margin-top: 10px;
+}
+
+.react-datepicker-popper[data-placement="bottom-end"]
+  .react-datepicker__triangle,
+.react-datepicker-popper[data-placement="top-end"] .react-datepicker__triangle {
+  left: auto;
+  right: 50px;
+}
+
+.react-datepicker-popper[data-placement^="top"] {
+  margin-bottom: 10px;
+}
+
+.react-datepicker-popper[data-placement^="right"] {
+  margin-left: 8px;
+}
+
+.react-datepicker-popper[data-placement^="right"] .react-datepicker__triangle {
+  left: auto;
+  right: 42px;
+}
+
+.react-datepicker-popper[data-placement^="left"] {
+  margin-right: 8px;
+}
+
+.react-datepicker-popper[data-placement^="left"] .react-datepicker__triangle {
+  left: 42px;
+  right: auto;
+}
+
+.react-datepicker__header {
+  text-align: center;
+  background-color: #f0f0f0;
+  border-bottom: 1px solid #aeaeae;
+  border-top-left-radius: 0.3rem;
+  border-top-right-radius: 0.3rem;
+  padding-top: 8px;
+  position: relative;
+}
+
+.react-datepicker__header--time {
+  padding-bottom: 8px;
+  padding-left: 5px;
+  padding-right: 5px;
+}
+
+.react-datepicker__year-dropdown-container--select,
+.react-datepicker__month-dropdown-container--select,
+.react-datepicker__month-year-dropdown-container--select,
+.react-datepicker__year-dropdown-container--scroll,
+.react-datepicker__month-dropdown-container--scroll,
+.react-datepicker__month-year-dropdown-container--scroll {
+  display: inline-block;
+  margin: 0 2px;
+}
+
+.react-datepicker__current-month,
+.react-datepicker-time__header,
+.react-datepicker-year-header {
+  margin-top: 0;
+  color: #000;
+  font-weight: bold;
+  font-size: 0.944rem;
+}
+
+.react-datepicker-time__header {
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+.react-datepicker__navigation {
+  background: none;
+  line-height: 1.7rem;
+  text-align: center;
+  cursor: pointer;
+  position: absolute;
+  top: 10px;
+  width: 0;
+  padding: 0;
+  border: 0.45rem solid transparent;
+  z-index: 1;
+  height: 10px;
+  width: 10px;
+  text-indent: -999em;
+  overflow: hidden;
+}
+
+.react-datepicker__navigation--previous {
+  left: 10px;
+  border-right-color: #ccc;
+}
+
+.react-datepicker__navigation--previous:hover {
+  border-right-color: #b3b3b3;
+}
+
+.react-datepicker__navigation--previous--disabled,
+.react-datepicker__navigation--previous--disabled:hover {
+  border-right-color: #e6e6e6;
+  cursor: default;
+}
+
+.react-datepicker__navigation--next {
+  right: 10px;
+  border-left-color: #ccc;
+}
+
+.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button) {
+  right: 80px;
+}
+
+.react-datepicker__navigation--next:hover {
+  border-left-color: #b3b3b3;
+}
+
+.react-datepicker__navigation--next--disabled,
+.react-datepicker__navigation--next--disabled:hover {
+  border-left-color: #e6e6e6;
+  cursor: default;
+}
+
+.react-datepicker__navigation--years {
+  position: relative;
+  top: 0;
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.react-datepicker__navigation--years-previous {
+  top: 4px;
+  border-top-color: #ccc;
+}
+
+.react-datepicker__navigation--years-previous:hover {
+  border-top-color: #b3b3b3;
+}
+
+.react-datepicker__navigation--years-upcoming {
+  top: -4px;
+  border-bottom-color: #ccc;
+}
+
+.react-datepicker__navigation--years-upcoming:hover {
+  border-bottom-color: #b3b3b3;
+}
+
+.react-datepicker__month-container {
+  float: left;
+}
+
+.react-datepicker__month {
+  margin: 0.4rem;
+  text-align: center;
+}
+
+.react-datepicker__month .react-datepicker__month-text,
+.react-datepicker__month .react-datepicker__quarter-text {
+  display: inline-block;
+  width: 4rem;
+  margin: 2px;
+}
+
+.react-datepicker__input-time-container {
+  clear: both;
+  width: 100%;
+  float: left;
+  margin: 5px 0 10px 15px;
+  text-align: left;
+}
+
+.react-datepicker__input-time-container .react-datepicker-time__caption {
+  display: inline-block;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container {
+  display: inline-block;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input {
+  display: inline-block;
+  margin-left: 10px;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input
+  input {
+  width: 85px;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input
+  input[type="time"]::-webkit-inner-spin-button,
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input
+  input[type="time"]::-webkit-outer-spin-button {
+  -webkit-appearance: none;
+  margin: 0;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__input
+  input[type="time"] {
+  -moz-appearance: textfield;
+}
+
+.react-datepicker__input-time-container
+  .react-datepicker-time__input-container
+  .react-datepicker-time__delimiter {
+  margin-left: 5px;
+  display: inline-block;
+}
+
+.react-datepicker__time-container {
+  float: right;
+  border-left: 1px solid #aeaeae;
+  width: 85px;
+}
+
+.react-datepicker__time-container--with-today-button {
+  display: inline;
+  border: 1px solid #aeaeae;
+  border-radius: 0.3rem;
+  position: absolute;
+  right: -72px;
+  top: 0;
+}
+
+.react-datepicker__time-container .react-datepicker__time {
+  position: relative;
+  background: white;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box {
+  width: 85px;
+  overflow-x: hidden;
+  margin: 0 auto;
+  text-align: center;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list {
+  list-style: none;
+  margin: 0;
+  height: calc(195px + (1.7rem / 2));
+  overflow-y: scroll;
+  padding-right: 0px;
+  padding-left: 0px;
+  width: 100%;
+  box-sizing: content-box;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item {
+  height: 30px;
+  padding: 5px 10px;
+  white-space: nowrap;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item:hover {
+  cursor: pointer;
+  background-color: #f0f0f0;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item--selected {
+  background-color: #216ba5;
+  color: white;
+  font-weight: bold;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item--selected:hover {
+  background-color: #216ba5;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item--disabled {
+  color: #ccc;
+}
+
+.react-datepicker__time-container
+  .react-datepicker__time
+  .react-datepicker__time-box
+  ul.react-datepicker__time-list
+  li.react-datepicker__time-list-item--disabled:hover {
+  cursor: default;
+  background-color: transparent;
+}
+
+.react-datepicker__week-number {
+  color: #ccc;
+  display: inline-block;
+  width: 1.7rem;
+  line-height: 1.7rem;
+  text-align: center;
+  margin: 0.166rem;
+}
+
+.react-datepicker__week-number.react-datepicker__week-number--clickable {
+  cursor: pointer;
+}
+
+.react-datepicker__week-number.react-datepicker__week-number--clickable:hover {
+  border-radius: 0.3rem;
+  background-color: #f0f0f0;
+}
+
+.react-datepicker__day-names,
+.react-datepicker__week {
+  white-space: nowrap;
+}
+
+.react-datepicker__day-name,
+.react-datepicker__day,
+.react-datepicker__time-name {
+  color: #000;
+  display: inline-block;
+  width: 1.7rem;
+  line-height: 1.7rem;
+  text-align: center;
+  margin: 0.166rem;
+}
+
+.react-datepicker__month--selected,
+.react-datepicker__month--in-selecting-range,
+.react-datepicker__month--in-range,
+.react-datepicker__quarter--selected,
+.react-datepicker__quarter--in-selecting-range,
+.react-datepicker__quarter--in-range {
+  border-radius: 0.3rem;
+  background-color: #216ba5;
+  color: #fff;
+}
+
+.react-datepicker__month--selected:hover,
+.react-datepicker__month--in-selecting-range:hover,
+.react-datepicker__month--in-range:hover,
+.react-datepicker__quarter--selected:hover,
+.react-datepicker__quarter--in-selecting-range:hover,
+.react-datepicker__quarter--in-range:hover {
+  background-color: #1d5d90;
+}
+
+.react-datepicker__month--disabled,
+.react-datepicker__quarter--disabled {
+  color: #ccc;
+  pointer-events: none;
+}
+
+.react-datepicker__month--disabled:hover,
+.react-datepicker__quarter--disabled:hover {
+  cursor: default;
+  background-color: transparent;
+}
+
+.react-datepicker__day,
+.react-datepicker__month-text,
+.react-datepicker__quarter-text {
+  cursor: pointer;
+}
+
+.react-datepicker__day:hover,
+.react-datepicker__month-text:hover,
+.react-datepicker__quarter-text:hover {
+  border-radius: 0.3rem;
+  background-color: #f0f0f0;
+}
+
+.react-datepicker__day--today,
+.react-datepicker__month-text--today,
+.react-datepicker__quarter-text--today {
+  font-weight: bold;
+}
+
+.react-datepicker__day--highlighted,
+.react-datepicker__month-text--highlighted,
+.react-datepicker__quarter-text--highlighted {
+  border-radius: 0.3rem;
+  background-color: #3dcc4a;
+  color: #fff;
+}
+
+.react-datepicker__day--highlighted:hover,
+.react-datepicker__month-text--highlighted:hover,
+.react-datepicker__quarter-text--highlighted:hover {
+  background-color: #32be3f;
+}
+
+.react-datepicker__day--highlighted-custom-1,
+.react-datepicker__month-text--highlighted-custom-1,
+.react-datepicker__quarter-text--highlighted-custom-1 {
+  color: magenta;
+}
+
+.react-datepicker__day--highlighted-custom-2,
+.react-datepicker__month-text--highlighted-custom-2,
+.react-datepicker__quarter-text--highlighted-custom-2 {
+  color: green;
+}
+
+.react-datepicker__day--selected,
+.react-datepicker__day--in-selecting-range,
+.react-datepicker__day--in-range,
+.react-datepicker__month-text--selected,
+.react-datepicker__month-text--in-selecting-range,
+.react-datepicker__month-text--in-range,
+.react-datepicker__quarter-text--selected,
+.react-datepicker__quarter-text--in-selecting-range,
+.react-datepicker__quarter-text--in-range {
+  border-radius: 0.3rem;
+  background-color: #216ba5;
+  color: #fff;
+}
+
+.react-datepicker__day--selected:hover,
+.react-datepicker__day--in-selecting-range:hover,
+.react-datepicker__day--in-range:hover,
+.react-datepicker__month-text--selected:hover,
+.react-datepicker__month-text--in-selecting-range:hover,
+.react-datepicker__month-text--in-range:hover,
+.react-datepicker__quarter-text--selected:hover,
+.react-datepicker__quarter-text--in-selecting-range:hover,
+.react-datepicker__quarter-text--in-range:hover {
+  background-color: #1d5d90;
+}
+
+.react-datepicker__day--keyboard-selected,
+.react-datepicker__month-text--keyboard-selected,
+.react-datepicker__quarter-text--keyboard-selected {
+  border-radius: 0.3rem;
+  background-color: #2a87d0;
+  color: #fff;
+}
+
+.react-datepicker__day--keyboard-selected:hover,
+.react-datepicker__month-text--keyboard-selected:hover,
+.react-datepicker__quarter-text--keyboard-selected:hover {
+  background-color: #1d5d90;
+}
+
+.react-datepicker__day--in-selecting-range,
+.react-datepicker__month-text--in-selecting-range,
+.react-datepicker__quarter-text--in-selecting-range {
+  background-color: rgba(33, 107, 165, 0.5);
+}
+
+.react-datepicker__month--selecting-range .react-datepicker__day--in-range,
+.react-datepicker__month--selecting-range
+  .react-datepicker__month-text--in-range,
+.react-datepicker__month--selecting-range
+  .react-datepicker__quarter-text--in-range {
+  background-color: #f0f0f0;
+  color: #000;
+}
+
+.react-datepicker__day--disabled,
+.react-datepicker__month-text--disabled,
+.react-datepicker__quarter-text--disabled {
+  cursor: default;
+  color: #ccc;
+}
+
+.react-datepicker__day--disabled:hover,
+.react-datepicker__month-text--disabled:hover,
+.react-datepicker__quarter-text--disabled:hover {
+  background-color: transparent;
+}
+
+.react-datepicker__month-text.react-datepicker__month--selected:hover,
+.react-datepicker__month-text.react-datepicker__month--in-range:hover,
+.react-datepicker__month-text.react-datepicker__quarter--selected:hover,
+.react-datepicker__month-text.react-datepicker__quarter--in-range:hover,
+.react-datepicker__quarter-text.react-datepicker__month--selected:hover,
+.react-datepicker__quarter-text.react-datepicker__month--in-range:hover,
+.react-datepicker__quarter-text.react-datepicker__quarter--selected:hover,
+.react-datepicker__quarter-text.react-datepicker__quarter--in-range:hover {
+  background-color: #216ba5;
+}
+
+.react-datepicker__month-text:hover,
+.react-datepicker__quarter-text:hover {
+  background-color: #f0f0f0;
+}
+
+.react-datepicker__input-container {
+  position: relative;
+  display: inline-block;
+  width: 100%;
+}
+
+.react-datepicker__year-read-view,
+.react-datepicker__month-read-view,
+.react-datepicker__month-year-read-view {
+  border: 1px solid transparent;
+  border-radius: 0.3rem;
+}
+
+.react-datepicker__year-read-view:hover,
+.react-datepicker__month-read-view:hover,
+.react-datepicker__month-year-read-view:hover {
+  cursor: pointer;
+}
+
+.react-datepicker__year-read-view:hover
+  .react-datepicker__year-read-view--down-arrow,
+.react-datepicker__year-read-view:hover
+  .react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-read-view:hover
+  .react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view:hover
+  .react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view:hover
+  .react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-year-read-view:hover
+  .react-datepicker__month-read-view--down-arrow {
+  border-top-color: #b3b3b3;
+}
+
+.react-datepicker__year-read-view--down-arrow,
+.react-datepicker__month-read-view--down-arrow,
+.react-datepicker__month-year-read-view--down-arrow {
+  border-top-color: #ccc;
+  float: right;
+  margin-left: 20px;
+  top: 8px;
+  position: relative;
+  border-width: 0.45rem;
+}
+
+.react-datepicker__year-dropdown,
+.react-datepicker__month-dropdown,
+.react-datepicker__month-year-dropdown {
+  background-color: #f0f0f0;
+  position: absolute;
+  width: 50%;
+  left: 25%;
+  top: 30px;
+  z-index: 1;
+  text-align: center;
+  border-radius: 0.3rem;
+  border: 1px solid #aeaeae;
+}
+
+.react-datepicker__year-dropdown:hover,
+.react-datepicker__month-dropdown:hover,
+.react-datepicker__month-year-dropdown:hover {
+  cursor: pointer;
+}
+
+.react-datepicker__year-dropdown--scrollable,
+.react-datepicker__month-dropdown--scrollable,
+.react-datepicker__month-year-dropdown--scrollable {
+  height: 150px;
+  overflow-y: scroll;
+}
+
+.react-datepicker__year-option,
+.react-datepicker__month-option,
+.react-datepicker__month-year-option {
+  line-height: 20px;
+  width: 100%;
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.react-datepicker__year-option:first-of-type,
+.react-datepicker__month-option:first-of-type,
+.react-datepicker__month-year-option:first-of-type {
+  border-top-left-radius: 0.3rem;
+  border-top-right-radius: 0.3rem;
+}
+
+.react-datepicker__year-option:last-of-type,
+.react-datepicker__month-option:last-of-type,
+.react-datepicker__month-year-option:last-of-type {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  border-bottom-left-radius: 0.3rem;
+  border-bottom-right-radius: 0.3rem;
+}
+
+.react-datepicker__year-option:hover,
+.react-datepicker__month-option:hover,
+.react-datepicker__month-year-option:hover {
+  background-color: #ccc;
+}
+
+.react-datepicker__year-option:hover
+  .react-datepicker__navigation--years-upcoming,
+.react-datepicker__month-option:hover
+  .react-datepicker__navigation--years-upcoming,
+.react-datepicker__month-year-option:hover
+  .react-datepicker__navigation--years-upcoming {
+  border-bottom-color: #b3b3b3;
+}
+
+.react-datepicker__year-option:hover
+  .react-datepicker__navigation--years-previous,
+.react-datepicker__month-option:hover
+  .react-datepicker__navigation--years-previous,
+.react-datepicker__month-year-option:hover
+  .react-datepicker__navigation--years-previous {
+  border-top-color: #b3b3b3;
+}
+
+.react-datepicker__year-option--selected,
+.react-datepicker__month-option--selected,
+.react-datepicker__month-year-option--selected {
+  position: absolute;
+  left: 15px;
+}
+
+.react-datepicker__close-icon {
+  cursor: pointer;
+  background-color: transparent;
+  border: 0;
+  outline: 0;
+  padding: 0px 6px 0px 0px;
+  position: absolute;
+  top: 0;
+  right: 0;
+  height: 100%;
+  display: table-cell;
+  vertical-align: middle;
+}
+
+.react-datepicker__close-icon::after {
+  cursor: pointer;
+  background-color: #216ba5;
+  color: #fff;
+  border-radius: 50%;
+  height: 16px;
+  width: 16px;
+  padding: 2px;
+  font-size: 12px;
+  line-height: 1;
+  text-align: center;
+  display: table-cell;
+  vertical-align: middle;
+  content: "\00d7";
+}
+
+.react-datepicker__today-button {
+  background: #f0f0f0;
+  border-top: 1px solid #aeaeae;
+  cursor: pointer;
+  text-align: center;
+  font-weight: bold;
+  padding: 5px 0;
+  clear: left;
+}
+
+.react-datepicker__portal {
+  position: fixed;
+  width: 100vw;
+  height: 100vh;
+  background-color: rgba(0, 0, 0, 0.8);
+  left: 0;
+  top: 0;
+  justify-content: center;
+  align-items: center;
+  display: flex;
+  z-index: 2147483647;
+}
+
+.react-datepicker__portal .react-datepicker__day-name,
+.react-datepicker__portal .react-datepicker__day,
+.react-datepicker__portal .react-datepicker__time-name {
+  width: 3rem;
+  line-height: 3rem;
+}
+
+@media (max-width: 400px), (max-height: 550px) {
+  .react-datepicker__portal .react-datepicker__day-name,
+  .react-datepicker__portal .react-datepicker__day,
+  .react-datepicker__portal .react-datepicker__time-name {
+    width: 2rem;
+    line-height: 2rem;
+  }
+}
+
+.react-datepicker__portal .react-datepicker__current-month,
+.react-datepicker__portal .react-datepicker-time__header {
+  font-size: 1.44rem;
+}
+
+.react-datepicker__portal .react-datepicker__navigation {
+  border: 0.81rem solid transparent;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--previous {
+  border-right-color: #ccc;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--previous:hover {
+  border-right-color: #b3b3b3;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--previous--disabled,
+.react-datepicker__portal
+  .react-datepicker__navigation--previous--disabled:hover {
+  border-right-color: #e6e6e6;
+  cursor: default;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--next {
+  border-left-color: #ccc;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--next:hover {
+  border-left-color: #b3b3b3;
+}
+
+.react-datepicker__portal .react-datepicker__navigation--next--disabled,
+.react-datepicker__portal .react-datepicker__navigation--next--disabled:hover {
+  border-left-color: #e6e6e6;
+  cursor: default;
+}

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

@@ -121,7 +121,7 @@ const Input = styled.input<{ disabled: boolean; width: string }>`
   font-size: 13px;
   background: #ffffff11;
   cursor: ${(props) => (props.disabled ? "not-allowed" : "")};
-  width: ${(props) => (props.width ? props.width : "270px")};
+  width: ${(props) => (props.width ? props.width : "100%")};
   color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
   padding: 5px 10px;
   height: 35px;

+ 1 - 1
dashboard/src/components/porter-form/FormDebugger.tsx

@@ -410,7 +410,7 @@ tabs:
   - name: env_vars
     contents:
     - type: heading
-      label: Environment Variables
+      label: Environment variables
     - type: subtitle
       label: Set environment variables for your secrets and environment-specific configuration.
     - type: env-key-value-array

+ 2 - 2
dashboard/src/main/home/Home.tsx

@@ -400,7 +400,7 @@ class Home extends Component<PropsType, StateType> {
               Join Our Discord
             </DiscordButton>
             {/* This should only be shown on the first render of the app */}
-            {this.state.showWelcomeForm &&
+            {/* this.state.showWelcomeForm &&
               localStorage.getItem("welcomed") != "true" &&
               projects?.length === 0 && (
                 <>
@@ -412,7 +412,7 @@ class Home extends Component<PropsType, StateType> {
                     currentView={this.props.currentRoute} // For form feedback
                   />
                 </>
-              )}
+              ) */}
           </>
         )}
 

+ 89 - 56
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -29,7 +29,6 @@ import LastRunStatusSelector from "./LastRunStatusSelector";
 import loadable from "@loadable/component";
 import Loading from "components/Loading";
 import JobRunTable from "./chart/JobRunTable";
-import TabSelector from "components/TabSelector";
 import TagFilter from "./TagFilter";
 
 // @ts-ignore
@@ -149,11 +148,6 @@ class ClusterDashboard extends Component<PropsType, StateType> {
 
     return (
       <>
-        <TagFilter
-          onSelect={(newSelectedTag) =>
-            this.setState({ selectedTag: newSelectedTag })
-          }
-        />
         <NamespaceSelector
           setNamespace={(namespace) =>
             this.setState({ namespace }, () => {
@@ -165,10 +159,10 @@ class ClusterDashboard extends Component<PropsType, StateType> {
           }
           namespace={this.state.namespace}
         />
-        <SortSelector
-          setSortType={(sortType) => this.setState({ sortType })}
-          sortType={this.state.sortType}
-          currentView={currentView}
+        <TagFilter
+          onSelect={(newSelectedTag) =>
+            this.setState({ selectedTag: newSelectedTag })
+          }
         />
       </>
     );
@@ -185,16 +179,23 @@ class ClusterDashboard extends Component<PropsType, StateType> {
     return (
       <>
         <ControlRow>
-          <SortFilterWrapper>{this.renderCommonFilters()}</SortFilterWrapper>
-          {isAuthorizedToAdd && (
-            <Button
-              onClick={() =>
-                pushFiltered(this.props, "/launch", ["project_id"])
-              }
-            >
-              <i className="material-icons">add</i> Launch template
-            </Button>
-          )}
+          <FilterWrapper>{this.renderCommonFilters()}</FilterWrapper>
+          <Flex>
+            <SortSelector
+              setSortType={(sortType) => this.setState({ sortType })}
+              sortType={this.state.sortType}
+              currentView={currentView}
+            />
+            {isAuthorizedToAdd && (
+              <Button
+                onClick={() =>
+                  pushFiltered(this.props, "/launch", ["project_id"])
+                }
+              >
+                <i className="material-icons">add</i> Launch template
+              </Button>
+            )}
+          </Flex>
         </ControlRow>
 
         <ChartList
@@ -219,31 +220,8 @@ class ClusterDashboard extends Component<PropsType, StateType> {
 
     return (
       <>
-        <TabSelector
-          currentTab={this.state.showRuns ? "job_runs" : "chart_list"}
-          options={[
-            { label: "Jobs", value: "chart_list" },
-            { label: "Runs", value: "job_runs" },
-          ]}
-          setCurrentTab={(value) => {
-            if (value === "job_runs") {
-              this.setState({ showRuns: true });
-            } else {
-              this.setState({ showRuns: false });
-            }
-          }}
-        />
         <ControlRow style={{ marginTop: "35px" }}>
-          {isAuthorizedToAdd && (
-            <Button
-              onClick={() =>
-                pushFiltered(this.props, "/launch", ["project_id"])
-              }
-            >
-              <i className="material-icons">add</i> Launch template
-            </Button>
-          )}
-          <SortFilterWrapper>
+          <FilterWrapper>
             <LastRunStatusSelector
               lastRunStatus={this.state.lastRunStatus}
               setLastRunStatus={(lastRunStatus: JobStatusType) => {
@@ -251,7 +229,33 @@ class ClusterDashboard extends Component<PropsType, StateType> {
               }}
             />
             {this.renderCommonFilters()}
-          </SortFilterWrapper>
+          </FilterWrapper>
+          <Flex>
+            <ToggleButton>
+              <ToggleOption
+                onClick={() => this.setState({ showRuns: false })}
+                selected={!this.state.showRuns}
+              >
+                Jobs
+              </ToggleOption>
+              <ToggleOption
+                nudgeLeft
+                onClick={() => this.setState({ showRuns: true })}
+                selected={this.state.showRuns}
+              >
+                Runs
+              </ToggleOption>
+            </ToggleButton>
+            {isAuthorizedToAdd && (
+              <Button
+                onClick={() =>
+                  pushFiltered(this.props, "/launch", ["project_id"])
+                }
+              >
+                <i className="material-icons">add</i> Launch template
+              </Button>
+            )}
+          </Flex>
         </ControlRow>
         <HidableElement show={this.state.showRuns}>
           <JobRunTable
@@ -316,6 +320,7 @@ class ClusterDashboard extends Component<PropsType, StateType> {
             image={monoweb}
             title={currentView}
             description="Continuously running web services, workers, and add-ons."
+            disableLineBreak
           />
 
           {this.renderBodyForApps()}
@@ -343,36 +348,65 @@ ClusterDashboard.contextType = Context;
 
 export default withRouter(withAuth(ClusterDashboard));
 
+const ToggleOption = styled.div<{ selected: boolean; nudgeLeft?: boolean }>`
+  padding: 0 10px;
+  color: ${(props) => (props.selected ? "" : "#494b4f")};
+  border: 1px solid #494b4f;
+  height: 100%;
+  display: flex;
+  margin-left: ${(props) => (props.nudgeLeft ? "-1px" : "")};
+  align-items: center;
+  border-radius: ${(props) =>
+    props.nudgeLeft ? "0 5px 5px 0" : "5px 0 0 5px"};
+  :hover {
+    border: 1px solid #7a7b80;
+    z-index: 999;
+  }
+`;
+
+const ToggleButton = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  font-size: 13px;
+  height: 30px;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+`;
+
 const HidableElement = styled.div<{ show: boolean }>`
   display: ${(props) => (props.show ? "unset" : "none")};
 `;
 
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  border-bottom: 30px solid transparent;
+`;
+
 const ControlRow = styled.div`
   display: flex;
-  margin-left: auto;
   justify-content: space-between;
   align-items: center;
   flex-wrap: wrap;
-  padding-left: 0px;
 `;
 
 const Button = styled.div`
   display: flex;
   flex-direction: row;
   align-items: center;
+  margin-left: 10px;
   justify-content: space-between;
   font-size: 13px;
   cursor: pointer;
   font-family: "Work Sans", sans-serif;
   border-radius: 5px;
+  font-weight: 500;
   color: white;
-  height: 35px;
-  margin-bottom: 35px;
-  padding: 0px 8px;
+  height: 30px;
+  padding: 0 8px;
   min-width: 155px;
-  padding-bottom: 1px;
-  font-weight: 500;
-  padding-right: 15px;
+  padding-right: 13px;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
@@ -401,11 +435,10 @@ const Button = styled.div`
   }
 `;
 
-const SortFilterWrapper = styled.div`
+const FilterWrapper = styled.div`
   display: flex;
   justify-content: space-between;
-  margin-bottom: 35px;
+  border-bottom: 30px solid transparent;
   > div:not(:first-child) {
-    margin-left: 30px;
   }
 `;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/DashboardHeader.tsx

@@ -58,7 +58,7 @@ const LineBreak = styled.div`
   height: 1px;
   background: #494b4f;
   width: 100%;
-  margin: 10px 0px 35px;
+  margin: 10px 0px 15px;
 `;
 
 const TopRow = styled.div`

+ 10 - 16
dashboard/src/main/home/cluster-dashboard/LastRunStatusSelector.tsx

@@ -1,9 +1,11 @@
 import React from "react";
 import styled from "styled-components";
 
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
 import { JobStatusType } from "shared/types";
 
+import last_run from "assets/last-run.svg";
+
 type PropsType = {
   lastRunStatus: JobStatusType;
   setLastRunStatus: (lastRunStatus: JobStatusType) => void;
@@ -23,21 +25,13 @@ const LastRunStatusSelector = (props: PropsType) => {
   );
 
   return (
-    <StyledLastRunStatusSelector>
-      <Label>
-        <i className="material-icons">filter_alt</i>
-        Last Run Status
-      </Label>
-      <Selector
-        activeValue={props.lastRunStatus}
-        setActiveValue={props.setLastRunStatus}
-        options={options}
-        dropdownLabel="Last Run Status"
-        width="150px"
-        dropdownWidth="230px"
-        closeOverlay={true}
-      />
-    </StyledLastRunStatusSelector>
+    <RadioFilter
+      selected={props.lastRunStatus}
+      setSelected={props.setLastRunStatus}
+      options={options}
+      name="Last run status"
+      icon={last_run}
+    />
   );
 };
 

+ 10 - 15
dashboard/src/main/home/cluster-dashboard/NamespaceSelector.tsx

@@ -1,10 +1,12 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 
+import folder from "assets/folder-outline.svg";
+
 import { Context } from "shared/Context";
 import api from "shared/api";
 
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
 
 type Props = {
   setNamespace: (x: string) => void;
@@ -102,20 +104,13 @@ export const NamespaceSelector: React.FunctionComponent<Props> = ({
   };
 
   return (
-    <StyledNamespaceSelector>
-      <Label>
-        <i className="material-icons">filter_alt</i> Namespace
-      </Label>
-      <Selector
-        activeValue={namespace}
-        setActiveValue={handleSetActive}
-        options={namespaceOptions}
-        dropdownLabel="Namespace"
-        width="150px"
-        dropdownWidth="230px"
-        closeOverlay={true}
-      />
-    </StyledNamespaceSelector>
+    <RadioFilter
+      icon={folder}
+      selected={namespace}
+      setSelected={handleSetActive}
+      options={namespaceOptions}
+      name="Namespace"
+    />
   );
 };
 

+ 9 - 11
dashboard/src/main/home/cluster-dashboard/SortSelector.tsx

@@ -3,7 +3,8 @@ import styled from "styled-components";
 
 import { Context } from "shared/Context";
 
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
+import sort from "assets/sort.svg";
 
 type PropsType = {
   setSortType: (x: string) => void;
@@ -46,17 +47,14 @@ export default class SortSelector extends Component<PropsType, StateType> {
   render() {
     return (
       <StyledSortSelector>
-        <Label>
-          <i className="material-icons">sort</i> Sort
-        </Label>
-        <Selector
-          activeValue={this.props.sortType}
-          setActiveValue={(sortType) => this.props.setSortType(sortType)}
+        <RadioFilter
+          selected={this.props.sortType}
+          setSelected={(sortType: any) => this.props.setSortType(sortType)}
           options={this.getSortOptions()}
-          dropdownLabel="Sort By"
-          width="150px"
-          dropdownWidth="230px"
-          closeOverlay={true}
+          name="Sort"
+          icon={sort}
+          dropdownAlignRight={true}
+          noMargin
         />
       </StyledSortSelector>
     );

+ 15 - 19
dashboard/src/main/home/cluster-dashboard/TagFilter.tsx

@@ -1,9 +1,11 @@
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
 import React, { useContext, useEffect, useState } from "react";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import styled from "styled-components";
 
+import tag from "assets/tag.svg";
+
 const TagFilter = ({ onSelect }: { onSelect: (tag: any) => void }) => {
   const { currentProject, currentCluster } = useContext(Context);
   const [selectedTag, setSelectedTag] = useState("none");
@@ -30,24 +32,18 @@ const TagFilter = ({ onSelect }: { onSelect: (tag: any) => void }) => {
   }, [selectedTag]);
 
   return (
-    <StyledTagSelector>
-      <Label>
-        <i className="material-icons">tag</i>
-        Tag
-      </Label>
-      <Selector
-        activeValue={selectedTag}
-        options={[{ label: "No tag selected", value: "none" }].concat(
-          tags.map((tag) => ({
-            value: tag.name,
-            label: tag.name,
-          }))
-        )}
-        setActiveValue={(newVal) => setSelectedTag(newVal)}
-        width={"150px"}
-        dropdownWidth="fit-content"
-      />
-    </StyledTagSelector>
+    <RadioFilter
+      selected={selectedTag}
+      options={[{ label: "All", value: "none" }].concat(
+        tags.map((tag) => ({
+          value: tag.name,
+          label: tag.name,
+        }))
+      )}
+      setSelected={(newVal: any) => setSelectedTag(newVal)}
+      name="Tag"
+      icon={tag}
+    />
   );
 };
 

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -369,7 +369,7 @@ const StyledChart = styled.div`
   width: calc(100% + 2px);
   height: calc(100% + 2px);
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/chart/JobRunTable.tsx

@@ -253,7 +253,7 @@ const JobRunTable: React.FC<Props> = ({
         },
       },
       {
-        Header: "Commit/Image tag",
+        Header: "Image tag",
         id: "commit_or_image_tag",
         accessor: (originalRow) => {
           const container = originalRow.spec?.template?.spec?.containers[0];
@@ -419,7 +419,7 @@ const CommandString = styled.div`
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-  max-width: 300px;
+  max-width: 160px;
   color: #ffffff55;
   margin-right: 27px;
   font-family: monospace;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/NamespaceList.tsx

@@ -301,7 +301,7 @@ const StyledCard = styled.div`
     }
   }
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/dashboard/NodeList.tsx

@@ -156,7 +156,7 @@ const StyledChart = styled.div`
     margin-bottom: 25px;
   }
   border-radius: 8px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
 `;
 

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/databases/DatabasesList.tsx

@@ -256,7 +256,7 @@ const StyledTableWrapper = styled.div`
   padding: 14px;
   position: relative;
   border-radius: 8px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   width: 100%;
   height: 100%;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx

@@ -193,7 +193,7 @@ export default class CreateEnvGroup extends Component<PropsType, StateType> {
             />
           </DestinationSection>
 
-          <Heading>Environment Variables</Heading>
+          <Heading>Environment variables</Heading>
           <Helper>
             Set environment variables for your secrets and environment-specific
             configuration.

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx

@@ -203,7 +203,7 @@ const StyledEnvGroup = styled.div`
   width: calc(100% + 2px);
   height: calc(100% + 2px);
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;

+ 26 - 18
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupDashboard.tsx

@@ -69,21 +69,23 @@ class EnvGroupDashboard extends Component<PropsType, StateType> {
                 }
                 namespace={this.state.namespace}
               />
+            </SortFilterWrapper>
+            <Flex>
               <SortSelector
                 currentView="env-groups"
                 setSortType={(sortType) => this.setState({ sortType })}
                 sortType={this.state.sortType}
               />
-            </SortFilterWrapper>
-            {isAuthorizedToAdd && (
-              <Button
-                onClick={() =>
-                  this.setState({ createEnvMode: !this.state.createEnvMode })
-                }
-              >
-                <i className="material-icons">add</i> Create env group
-              </Button>
-            )}
+              {isAuthorizedToAdd && (
+                <Button
+                  onClick={() =>
+                    this.setState({ createEnvMode: !this.state.createEnvMode })
+                  }
+                >
+                  <i className="material-icons">add</i> Create env group
+                </Button>
+              )}
+            </Flex>
           </ControlRow>
 
           <EnvGroupList
@@ -129,6 +131,7 @@ class EnvGroupDashboard extends Component<PropsType, StateType> {
             image={sliders}
             title="Environment Groups"
             description="Groups of environment variables for storing secrets and configuration."
+            disableLineBreak
           />
           {this.renderBody()}
         </>
@@ -145,11 +148,17 @@ EnvGroupDashboard.contextType = Context;
 
 export default withRouter(withAuth(EnvGroupDashboard));
 
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  border-bottom: 30px solid transparent;
+`;
+
 const SortFilterWrapper = styled.div`
   display: flex;
   justify-content: space-between;
+  border-bottom: 30px solid transparent;
   > div:not(:first-child) {
-    margin-left: 30px;
   }
 `;
 
@@ -162,12 +171,12 @@ const ControlRow = styled.div`
     return "flex-end";
   }};
   align-items: center;
-  margin-bottom: 35px;
-  padding-left: 0px;
+  flex-wrap: wrap;
 `;
 
 const Button = styled.div`
   display: flex;
+  margin-left: 10px;
   flex-direction: row;
   align-items: center;
   justify-content: space-between;
@@ -176,11 +185,10 @@ const Button = styled.div`
   font-family: "Work Sans", sans-serif;
   border-radius: 5px;
   color: white;
-  height: 35px;
-  padding: 0px 8px;
-  padding-bottom: 1px;
-  font-weight: 500;
-  padding-right: 15px;
+  height: 30px;
+  padding: 0 8px;
+  min-width: 155px;
+  padding-right: 13px;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;

+ 131 - 10
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -91,7 +91,7 @@ export const ExpandedEnvGroupFC = ({
 
   const tabOptions = useMemo(() => {
     if (!isAuthorized("env_group", "", ["get", "delete"])) {
-      return [{ value: "variables-editor", label: "Environment Variables" }];
+      return [{ value: "variables-editor", label: "Environment variables" }];
     }
 
     if (
@@ -99,21 +99,21 @@ export const ExpandedEnvGroupFC = ({
       currentEnvGroup?.applications?.length
     ) {
       return [
-        { value: "variables-editor", label: "Environment Variables" },
-        { value: "applications", label: "Linked Applications" },
+        { value: "variables-editor", label: "Environment variables" },
+        { value: "applications", label: "Linked applications" },
       ];
     }
 
     if (currentEnvGroup?.applications?.length) {
       return [
-        { value: "variables-editor", label: "Environment Variables" },
-        { value: "applications", label: "Linked Applications" },
+        { value: "variables-editor", label: "Environment variables" },
+        { value: "applications", label: "Linked applications" },
         { value: "settings", label: "Settings" },
       ];
     }
 
     return [
-      { value: "variables-editor", label: "Environment Variables" },
+      { value: "variables-editor", label: "Environment variables" },
       { value: "settings", label: "Settings" },
     ];
   }, [currentEnvGroup]);
@@ -402,6 +402,7 @@ export const ExpandedEnvGroupFC = ({
       default:
         return (
           <EnvGroupSettings
+            namespace={namespace}
             envGroup={currentEnvGroup}
             handleDeleteEnvGroup={handleDeleteEnvGroup}
           />
@@ -478,7 +479,7 @@ const EnvGroupVariablesEditor = ({
   return (
     <TabWrapper>
       <InnerWrapper>
-        <Heading>Environment Variables</Heading>
+        <Heading>Environment variables</Heading>
         <Helper>
           Set environment variables for your secrets and environment-specific
           configuration.
@@ -515,12 +516,22 @@ const EnvGroupVariablesEditor = ({
 const EnvGroupSettings = ({
   envGroup,
   handleDeleteEnvGroup,
+  namespace,
 }: {
   envGroup: EditableEnvGroup;
   handleDeleteEnvGroup: () => void;
+  namespace?: string;
 }) => {
-  const { setCurrentOverlay } = useContext(Context);
+  const {
+    setCurrentOverlay,
+    currentProject,
+    currentCluster,
+    setCurrentError,
+  } = useContext(Context);
   const [isAuthorized] = useAuth();
+  const [name, setName] = useState(null);
+  const [cloneNamespace, setCloneNamespace] = useState(null);
+  const [cloneSuccess, setCloneSuccess] = useState(false);
 
   const canDelete = useMemo(() => {
     // add a case for when applications is null - in this case this is a deprecated env group version
@@ -531,6 +542,30 @@ const EnvGroupSettings = ({
     return envGroup?.applications?.length === 0;
   }, [envGroup]);
 
+  const cloneEnvGroup = async () => {
+    setCloneSuccess(false);
+    try {
+      await api.cloneEnvGroup(
+        "<token>",
+        {
+          name: envGroup.name,
+          namespace: cloneNamespace,
+          clone_name: name,
+          version: envGroup.version,
+        },
+        {
+          id: currentProject.id,
+          cluster_id: currentCluster.id,
+          namespace: namespace,
+        }
+      );
+      setCloneSuccess(true);
+    } catch (error) {
+      console.log(error);
+      setCurrentError(error);
+    }
+  };
+
   return (
     <TabWrapper>
       {isAuthorized("env_group", "", ["get", "delete"]) && (
@@ -561,7 +596,7 @@ const EnvGroupSettings = ({
 
                 <DarkMatter /> */}
 
-          <Heading>Manage Environment Group</Heading>
+          <Heading>Manage environment group</Heading>
           <Helper>
             Permanently delete this set of environment variables. This action
             cannot be undone.
@@ -573,7 +608,6 @@ const EnvGroupSettings = ({
               applications to delete.
             </Helper>
           )}
-
           <Button
             color="#b91133"
             onClick={() => {
@@ -587,6 +621,34 @@ const EnvGroupSettings = ({
           >
             Delete {envGroup.name}
           </Button>
+          <DarkMatter />
+          <Heading>Clone environment group</Heading>
+          <Helper>
+            Clone this set of environment variables into a new env group.
+          </Helper>
+          <InputRow
+            type="string"
+            value={name}
+            setValue={(x: string) => setName(x)}
+            label="New env group name"
+            placeholder="ex: my-cloned-env-group"
+          />
+          <InputRow
+            type="string"
+            value={cloneNamespace}
+            setValue={(x: string) => setCloneNamespace(x)}
+            label="New env group namespace"
+            placeholder="ex: default"
+          />
+          <FlexAlt>
+            <Button onClick={cloneEnvGroup}>Clone {envGroup.name}</Button>
+            {cloneSuccess && (
+              <StatusWrapper position="right" successful={true}>
+                <i className="material-icons">done</i>
+                <StatusTextWrapper>Successfully cloned</StatusTextWrapper>
+              </StatusWrapper>
+            )}
+          </FlexAlt>
         </InnerWrapper>
       )}
     </TabWrapper>
@@ -632,6 +694,65 @@ const ApplicationsList = ({ envGroup }: { envGroup: EditableEnvGroup }) => {
   );
 };
 
+const FlexAlt = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: 20px;
+`;
+
+const StatusTextWrapper = styled.p`
+  display: -webkit-box;
+  line-clamp: 2;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  line-height: 19px;
+  margin: 0;
+`;
+
+const StatusWrapper = styled.div<{
+  successful: boolean;
+  position: "right" | "left";
+}>`
+  display: flex;
+  align-items: center;
+  max-width: 170px;
+  font-family: "Work Sans", sans-serif;
+  font-size: 13px;
+  color: #ffffff55;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin-top: 5px;
+  margin-bottom: 30px;
+  height: 35px;
+  margin-left: 15px;
+
+  > i {
+    font-size: 18px;
+    margin-right: 10px;
+    float: left;
+    color: ${(props) => (props.successful ? "#4797ff" : "#fcba03")};
+  }
+
+  animation-fill-mode: forwards;
+
+  @keyframes statusFloatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;
+
+const DarkMatter = styled.div`
+  width: 100%;
+  height: 1px;
+  margin-top: -20px;
+`;
+
 const ArrowIcon = styled.img`
   width: 15px;
   margin-right: 8px;

+ 145 - 113
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -14,6 +14,7 @@ import RevisionSection from "./RevisionSection";
 import ValuesYaml from "./ValuesYaml";
 import GraphSection from "./GraphSection";
 import MetricsSection from "./metrics/MetricsSection";
+import LogsSection from "./logs-section/LogsSection";
 import ListSection from "./ListSection";
 import StatusSection from "./status/StatusSection";
 import SettingsSection from "./SettingsSection";
@@ -74,6 +75,7 @@ const ExpandedChart: React.FC<Props> = (props) => {
   const [showRepoTooltip, setShowRepoTooltip] = useState(false);
   const [isAuthorized] = useAuth();
   const [fullScreenLogs, setFullScreenLogs] = useState<boolean>(false);
+  const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
 
   const {
     isStack,
@@ -415,6 +417,14 @@ const ExpandedChart: React.FC<Props> = (props) => {
     let chart = currentChart;
     // console.log("CONTROLLERS", controllers);
     switch (currentTab) {
+      case "logs":
+        return (
+          <LogsSection
+            currentChart={chart}
+            isFullscreen={isFullscreen}
+            setIsFullscreen={setIsFullscreen}
+          />
+        );
       case "metrics":
         return <MetricsSection currentChart={chart} />;
       case "incidents":
@@ -528,6 +538,14 @@ const ExpandedChart: React.FC<Props> = (props) => {
     // Collate non-form tabs
     let rightTabOptions = [] as any[];
     let leftTabOptions = [] as any[];
+    if (
+      currentChart.chart.metadata.home === "https://getporter.dev/" &&
+      (currentChart.chart.metadata.name === "web" ||
+        currentChart.chart.metadata.name === "worker" ||
+        currentChart.chart.metadata.name === "job")
+    ) {
+      leftTabOptions.push({ label: "Logs", value: "logs" });
+    }
     leftTabOptions.push({ label: "Status", value: "status" });
 
     /* Temporarily disable incident detection
@@ -759,134 +777,149 @@ const ExpandedChart: React.FC<Props> = (props) => {
           setFullScreenLogs={() => setFullScreenLogs(false)}
         />
       ) : (
-        <StyledExpandedChart>
-          <BreadcrumbRow>
-            <Breadcrumb onClick={props.closeChart}>
-              <ArrowIcon src={leftArrow} />
-              <Wrap>Back</Wrap>
-            </Breadcrumb>
-          </BreadcrumbRow>
-          <HeaderWrapper>
-            <TitleSection
-              icon={currentChart.chart.metadata.icon}
-              iconWidth="33px"
-            >
-              {currentChart.name}
-              <DeploymentType currentChart={currentChart} />
-              <TagWrapper>
-                Namespace <NamespaceTag>{currentChart.namespace}</NamespaceTag>
-              </TagWrapper>
-            </TitleSection>
-
-            {currentChart.chart.metadata.name != "worker" &&
-              currentChart.chart.metadata.name != "job" &&
-              renderUrl()}
-            <InfoWrapper>
-              <StatusIndicator
-                controllers={controllers}
-                status={currentChart.info.status}
-                margin_left={"0px"}
-              />
-              <LastDeployed>
-                <Dot>•</Dot>Last deployed
-                {" " + getReadableDate(currentChart.info.last_deployed)}
-              </LastDeployed>
-            </InfoWrapper>
-          </HeaderWrapper>
-          {deleting ? (
-            <>
-              <LineBreak />
-              <Placeholder>
-                <TextWrap>
-                  <Header>
-                    <Spinner src={loadingSrc} /> Deleting "{currentChart.name}"
-                  </Header>
-                  You will be automatically redirected after deletion is
-                  complete.
-                </TextWrap>
-              </Placeholder>
-            </>
+        <>
+          {isFullscreen ? (
+            <LogsSection
+              isFullscreen={true}
+              setIsFullscreen={setIsFullscreen}
+              currentChart={currentChart}
+            />
           ) : (
-            <>
-              <RevisionSection
-                showRevisions={showRevisions}
-                toggleShowRevisions={() => {
-                  setShowRevisions(!showRevisions);
-                }}
-                chart={currentChart}
-                refreshChart={() => getChartData(currentChart)}
-                setRevision={setRevision}
-                forceRefreshRevisions={forceRefreshRevisions}
-                refreshRevisionsOff={() => setForceRefreshRevisions(false)}
-                shouldUpdate={
-                  currentChart.latest_version &&
-                  currentChart.latest_version !==
-                    currentChart.chart.metadata.version
-                }
-                latestVersion={currentChart.latest_version}
-                upgradeVersion={handleUpgradeVersion}
-              />
-              {isStack && isLoadingStackEnvGroups ? (
+            <StyledExpandedChart>
+              <BreadcrumbRow>
+                <Breadcrumb onClick={props.closeChart}>
+                  <ArrowIcon src={leftArrow} />
+                  <Wrap>Back</Wrap>
+                </Breadcrumb>
+              </BreadcrumbRow>
+              <HeaderWrapper>
+                <TitleSection
+                  icon={currentChart.chart.metadata.icon}
+                  iconWidth="33px"
+                >
+                  {currentChart.name}
+                  <DeploymentType currentChart={currentChart} />
+                  <TagWrapper>
+                    Namespace{" "}
+                    <NamespaceTag>{currentChart.namespace}</NamespaceTag>
+                  </TagWrapper>
+                </TitleSection>
+
+                {currentChart.chart.metadata.name != "worker" &&
+                  currentChart.chart.metadata.name != "job" &&
+                  renderUrl()}
+                <InfoWrapper>
+                  <StatusIndicator
+                    controllers={controllers}
+                    status={currentChart.info.status}
+                    margin_left={"0px"}
+                  />
+                  <LastDeployed>
+                    <Dot>•</Dot>Last deployed
+                    {" " + getReadableDate(currentChart.info.last_deployed)}
+                  </LastDeployed>
+                </InfoWrapper>
+              </HeaderWrapper>
+              {deleting ? (
                 <>
                   <LineBreak />
                   <Placeholder>
                     <TextWrap>
                       <Header>
-                        <Spinner src={loadingSrc} />
+                        <Spinner src={loadingSrc} /> Deleting "
+                        {currentChart.name}"
                       </Header>
+                      You will be automatically redirected after deletion is
+                      complete.
                     </TextWrap>
                   </Placeholder>
                 </>
               ) : (
                 <>
-                  {(isPreview || leftTabOptions.length > 0) && (
-                    <BodyWrapper>
-                      <PorterFormWrapper
-                        formData={cloneDeep(currentChart.form)}
-                        valuesToOverride={{
-                          namespace: props.namespace,
-                          clusterId: currentCluster.id,
-                        }}
-                        renderTabContents={renderTabContents}
-                        isReadOnly={
-                          isPreview ||
-                          imageIsPlaceholder ||
-                          !isAuthorized("application", "", ["get", "update"])
-                        }
-                        onSubmit={onSubmit}
-                        includeMetadata
-                        rightTabOptions={rightTabOptions}
-                        leftTabOptions={leftTabOptions}
-                        color={isPreview ? "#f5cb42" : null}
-                        addendum={
-                          <TabButton
-                            onClick={toggleDevOpsMode}
-                            devOpsMode={devOpsMode}
-                          >
-                            <i className="material-icons">offline_bolt</i>{" "}
-                            DevOps Mode
-                          </TabButton>
-                        }
-                        saveValuesStatus={saveValuesStatus}
-                        injectedProps={{
-                          "key-value-array": {
-                            availableSyncEnvGroups:
-                              isStack && !isPreview
-                                ? stackEnvGroups
-                                : undefined,
-                          },
-                          "url-link": {
-                            chart: currentChart,
-                          },
-                        }}
-                      />
-                    </BodyWrapper>
+                  <RevisionSection
+                    showRevisions={showRevisions}
+                    toggleShowRevisions={() => {
+                      setShowRevisions(!showRevisions);
+                    }}
+                    chart={currentChart}
+                    refreshChart={() => getChartData(currentChart)}
+                    setRevision={setRevision}
+                    forceRefreshRevisions={forceRefreshRevisions}
+                    refreshRevisionsOff={() => setForceRefreshRevisions(false)}
+                    shouldUpdate={
+                      currentChart.latest_version &&
+                      currentChart.latest_version !==
+                        currentChart.chart.metadata.version
+                    }
+                    latestVersion={currentChart.latest_version}
+                    upgradeVersion={handleUpgradeVersion}
+                  />
+                  {isStack && isLoadingStackEnvGroups ? (
+                    <>
+                      <LineBreak />
+                      <Placeholder>
+                        <TextWrap>
+                          <Header>
+                            <Spinner src={loadingSrc} />
+                          </Header>
+                        </TextWrap>
+                      </Placeholder>
+                    </>
+                  ) : (
+                    <>
+                      {(isPreview || leftTabOptions.length > 0) && (
+                        <BodyWrapper>
+                          <PorterFormWrapper
+                            formData={cloneDeep(currentChart.form)}
+                            valuesToOverride={{
+                              namespace: props.namespace,
+                              clusterId: currentCluster.id,
+                            }}
+                            renderTabContents={renderTabContents}
+                            isReadOnly={
+                              isPreview ||
+                              imageIsPlaceholder ||
+                              !isAuthorized("application", "", [
+                                "get",
+                                "update",
+                              ])
+                            }
+                            onSubmit={onSubmit}
+                            includeMetadata
+                            rightTabOptions={rightTabOptions}
+                            leftTabOptions={leftTabOptions}
+                            color={isPreview ? "#f5cb42" : null}
+                            addendum={
+                              <TabButton
+                                onClick={toggleDevOpsMode}
+                                devOpsMode={devOpsMode}
+                              >
+                                <i className="material-icons">offline_bolt</i>{" "}
+                                DevOps Mode
+                              </TabButton>
+                            }
+                            saveValuesStatus={saveValuesStatus}
+                            injectedProps={{
+                              "key-value-array": {
+                                availableSyncEnvGroups:
+                                  isStack && !isPreview
+                                    ? stackEnvGroups
+                                    : undefined,
+                              },
+                              "url-link": {
+                                chart: currentChart,
+                              },
+                            }}
+                          />
+                        </BodyWrapper>
+                      )}
+                    </>
                   )}
                 </>
               )}
-            </>
+            </StyledExpandedChart>
           )}
-        </StyledExpandedChart>
+        </>
       )}
     </>
   );
@@ -939,7 +972,6 @@ const LineBreak = styled.div`
 
 const BodyWrapper = styled.div`
   position: relative;
-  margin-bottom: 50px;
 `;
 
 const Header = styled.div`

+ 2 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/BuildSettingsTab.tsx

@@ -329,7 +329,7 @@ const BuildSettingsTab: React.FC<Props> = ({
           </AlertCardAction>
         </AlertCard>
       ) : null} */}
-        <Heading isAtTop>Build Environment Variables</Heading>
+        <Heading isAtTop>Build environment variables</Heading>
         <KeyValueArray
           values={envVariables}
           envLoader
@@ -342,7 +342,7 @@ const BuildSettingsTab: React.FC<Props> = ({
           }}
         ></KeyValueArray>
 
-        <Heading>Select Default Branch</Heading>
+        <Heading>Select default branch</Heading>
         <Helper>
           Change the default branch the deployments will be made from.
         </Helper>

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/ExpandedJobRun.tsx

@@ -181,7 +181,7 @@ const ExpandedJobRun = ({
             <KeyValueArray
               envLoader={true}
               values={envObject}
-              label="Environment Variables:"
+              label="Environment variables:"
               disabled={true}
             />
             <DarkMatter />

+ 3 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/jobs/JobResource.tsx

@@ -201,7 +201,7 @@ export default class JobResource extends Component<PropsType, StateType> {
                 <KeyValueArray
                   envLoader={true}
                   values={envObject}
-                  label="Environment Variables:"
+                  label="Environment variables:"
                   disabled={true}
                 />
                 <DarkMatter />
@@ -497,7 +497,7 @@ const StyledJob = styled.div`
   margin-bottom: 20px;
   overflow: hidden;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;
@@ -560,7 +560,7 @@ const Subtitle = styled.div`
 `;
 
 const JobLogsWrapper = styled.div`
-  height: 250px;
+  max-height: 500px;
   width: 100%;
   background-color: black;
   overflow-y: auto;

+ 419 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/LogsSection.tsx

@@ -0,0 +1,419 @@
+import React, { useContext, useEffect, useState } from "react";
+
+import styled from "styled-components";
+import RadioFilter from "components/RadioFilter";
+
+import filterOutline from "assets/filter-outline.svg";
+import downArrow from "assets/down-arrow.svg";
+import { Context } from "shared/Context";
+import api from "shared/api";
+import { useLogs } from "./useAgentLogs";
+import Anser from "anser";
+import { flatMap } from "lodash";
+import time from "assets/time.svg";
+import DateTimePicker from "components/date-time-picker/DateTimePicker";
+
+type Props = {
+  currentChart?: any;
+  isFullscreen: boolean;
+  setIsFullscreen: (x: boolean) => void;
+};
+
+const escapeRegExp = (str: string) => {
+  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+};
+
+const LogsSection: React.FC<Props> = ({
+  currentChart,
+  isFullscreen,
+  setIsFullscreen,
+}) => {
+  const { currentProject, currentCluster } = useContext(Context);
+  const [podFilter, setPodFilter] = useState();
+  const [podFilterOpts, setPodFilterOpts] = useState<string[]>();
+  const [scrollToBottom, setScrollToBottom] = useState(true);
+  const [searchText, setSearchText] = useState("");
+  const [enteredSearchText, setEnteredSearchText] = useState("");
+  const [startDate, setStartDate] = useState(new Date());
+
+  var d = new Date(startDate);
+  d.setDate(d.getDate() - 1);
+  var start = d.toISOString();
+
+  const { logs, refresh } = useLogs(
+    podFilter,
+    currentChart.namespace,
+    enteredSearchText,
+    start
+  );
+
+  useEffect(() => {
+    api
+      .getLogPodValues(
+        "<TOKEN>",
+        {
+          match_prefix: currentChart.name,
+        },
+        {
+          project_id: currentProject.id,
+          cluster_id: currentCluster.id,
+        }
+      )
+      .then((res) => {
+        console.log(res.data);
+        setPodFilterOpts(res.data);
+        setPodFilter(res.data[0]);
+      });
+
+    console.log(currentChart);
+  }, []);
+
+  const renderLogs = () => {
+    return logs?.map((log, i) => {
+      return (
+        <Log key={i}>
+          {log.map((ansi, j) => {
+            if (ansi.clearLine) {
+              return null;
+            }
+
+            return (
+              <LogSpan key={i + "." + j} ansi={ansi}>
+                {ansi.content.replace(/ /g, "\u00a0")}
+              </LogSpan>
+            );
+          })}
+        </Log>
+      );
+    });
+  };
+
+  const renderContents = () => {
+    return (
+      <>
+        <FlexRow isFullscreen={isFullscreen}>
+          <Flex>
+            <SearchRowWrapper>
+              <SearchBarWrapper>
+                <i className="material-icons">search</i>
+                <SearchInput
+                  value={searchText}
+                  onChange={(e: any) => {
+                    setSearchText(e.target.value);
+                  }}
+                  onKeyPress={(event) => {
+                    if (event.key === "Enter") {
+                      setEnteredSearchText(escapeRegExp(searchText));
+                    }
+                  }}
+                  placeholder="Search logs..."
+                />
+              </SearchBarWrapper>
+            </SearchRowWrapper>
+            <DateTimePicker startDate={startDate} setStartDate={setStartDate} />
+            <RadioFilter
+              icon={filterOutline}
+              selected={podFilter}
+              setSelected={setPodFilter}
+              options={podFilterOpts?.map((name) => {
+                return {
+                  value: name,
+                  label: name,
+                };
+              })}
+              name="Filter logs"
+            />
+          </Flex>
+          <Flex>
+            <Button onClick={() => setScrollToBottom(!scrollToBottom)}>
+              <Checkbox checked={scrollToBottom}>
+                <i className="material-icons">done</i>
+              </Checkbox>
+              Scroll to bottom
+            </Button>
+            <Spacer />
+            <Button>
+              <i className="material-icons">autorenew</i>
+              Refresh
+            </Button>
+            {!isFullscreen && (
+              <>
+                <Spacer />
+                <Icon onClick={() => setIsFullscreen(true)}>
+                  <i className="material-icons">open_in_full</i>
+                </Icon>
+              </>
+            )}
+          </Flex>
+        </FlexRow>
+        <StyledLogsSection isFullscreen={isFullscreen}>
+          {renderLogs()}
+          {/* <Message>
+            
+            No matching logs found.
+            <Highlight onClick={() => {}}>
+              <i className="material-icons">autorenew</i>
+              Refresh
+            </Highlight>
+          </Message> */}
+        </StyledLogsSection>
+      </>
+    );
+  };
+
+  return (
+    <>
+      {isFullscreen ? (
+        <Fullscreen>
+          <AbsoluteTitle>
+            <BackButton onClick={() => setIsFullscreen(false)}>
+              <i className="material-icons">navigate_before</i>
+            </BackButton>
+            Logs ({currentChart.name})
+          </AbsoluteTitle>
+          {renderContents()}
+        </Fullscreen>
+      ) : (
+        <>{renderContents()}</>
+      )}
+    </>
+  );
+};
+
+export default LogsSection;
+
+const BackButton = styled.div`
+  display: flex;
+  width: 30px;
+  z-index: 999;
+  cursor: pointer;
+  height: 30px;
+  align-items: center;
+  margin-right: 15px;
+  justify-content: center;
+  cursor: pointer;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  background: #ffffff11;
+
+  > i {
+    font-size: 18px;
+  }
+
+  :hover {
+    background: #ffffff22;
+    > img {
+      opacity: 1;
+    }
+  }
+`;
+
+const AbsoluteTitle = styled.div`
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  width: 100%;
+  height: 60px;
+  display: flex;
+  align-items: center;
+  padding-left: 20px;
+  font-size: 18px;
+  font-weight: 500;
+  user-select: text;
+`;
+
+const Fullscreen = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  padding-top: 60px;
+`;
+
+const Icon = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  height: 30px;
+  width: 30px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  justify-content: center;
+  > i {
+    font-size: 14px;
+  }
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;
+
+const Checkbox = styled.div<{ checked: boolean }>`
+  width: 16px;
+  height: 16px;
+  border: 1px solid #ffffff55;
+  margin: 1px 10px 0px 1px;
+  border-radius: 3px;
+  background: ${(props) => (props.checked ? "#ffffff22" : "#ffffff11")};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  > i {
+    font-size: 12px;
+    padding-left: 0px;
+    display: ${(props) => (props.checked ? "" : "none")};
+  }
+`;
+
+const Spacer = styled.div<{ width?: string }>`
+  height: 100%;
+  width: ${(props) => props.width || "10px"};
+`;
+
+const Button = styled.div`
+  background: #26292e;
+  border-radius: 5px;
+  height: 30px;
+  font-size: 13px;
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  padding: 10px;
+  padding-left: 8px;
+  > i {
+    font-size: 16px;
+    margin-right: 5px;
+  }
+  border: 1px solid #494b4f;
+  :hover {
+    border: 1px solid #7a7b80;
+  }
+`;
+
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+  border-bottom: 25px solid transparent;
+`;
+
+const Message = styled.div`
+  display: flex;
+  height: 100%;
+  width: calc(100% - 150px);
+  align-items: center;
+  justify-content: center;
+  margin-left: 75px;
+  text-align: center;
+  color: #ffffff44;
+  font-size: 13px;
+`;
+
+const Highlight = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-left: 8px;
+  color: #8590ff;
+  cursor: pointer;
+
+  > i {
+    font-size: 16px;
+    margin-right: 3px;
+  }
+`;
+
+const FlexRow = styled.div<{ isFullscreen?: boolean }>`
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  margin-top: ${(props) => (props.isFullscreen ? "10px" : "")};
+  padding: ${(props) => (props.isFullscreen ? "0 20px" : "")};
+`;
+
+const SearchBarWrapper = styled.div`
+  display: flex;
+  flex: 1;
+
+  > i {
+    color: #aaaabb;
+    padding-top: 1px;
+    margin-left: 8px;
+    font-size: 16px;
+    margin-right: 8px;
+  }
+`;
+
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  height: 100%;
+`;
+
+const SearchRow = styled.div`
+  display: flex;
+  align-items: center;
+  height: 30px;
+  margin-right: 10px;
+  background: #26292e;
+  border-radius: 5px;
+  border: 1px solid #aaaabb33;
+`;
+
+const SearchRowWrapper = styled(SearchRow)`
+  border-radius: 5px;
+  width: 250px;
+`;
+
+const StyledLogsSection = styled.div<{ isFullscreen: boolean }>`
+  width: 100%;
+  min-height: 400px;
+  height: ${(props) =>
+    props.isFullscreen ? "calc(100vh - 125px)" : "calc(100vh - 460px)"};
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  font-size: 13px;
+  border-radius: ${(props) => (props.isFullscreen ? "" : "8px")};
+  border: ${(props) => (props.isFullscreen ? "" : "1px solid #ffffff33")};
+  border-top: ${(props) => (props.isFullscreen ? "1px solid #ffffff33" : "")};
+  padding: 18px 22px;
+  background: #121318;
+  animation: floatIn 0.3s;
+  animation-timing-function: ease-out;
+  animation-fill-mode: forwards;
+  overflow-y: auto;
+  overflow-wrap: break-word;
+  @keyframes floatIn {
+    from {
+      opacity: 0;
+      transform: translateY(10px);
+    }
+    to {
+      opacity: 1;
+      transform: translateY(0px);
+    }
+  }
+`;
+
+const Log = styled.div`
+  font-family: monospace;
+  user-select: text;
+`;
+
+const LogSpan = styled.span`
+  font-family: monospace, sans-serif;
+  font-size: 12px;
+  font-weight: ${(props: { ansi: Anser.AnserJsonEntry }) =>
+    props.ansi?.decoration && props.ansi?.decoration == "bold" ? "700" : "400"};
+  color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
+    props.ansi?.fg ? `rgb(${props.ansi?.fg})` : "white"};
+  background-color: ${(props: { ansi: Anser.AnserJsonEntry }) =>
+    props.ansi?.bg ? `rgb(${props.ansi?.bg})` : "transparent"};
+`;

+ 110 - 0
dashboard/src/main/home/cluster-dashboard/expanded-chart/logs-section/useAgentLogs.ts

@@ -0,0 +1,110 @@
+import Anser from "anser";
+import { flatMap } from "lodash";
+import { useContext, useEffect, useMemo, useRef, useState } from "react";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { useWebsockets, NewWebsocketOptions } from "shared/hooks/useWebsockets";
+
+const MAX_LOGS = 250;
+
+export const useLogs = (
+  currentPod: string,
+  namespace: string,
+  searchParam: string,
+  startDate: string,
+  scroll?: (smooth: boolean) => void
+) => {
+  const { currentCluster, currentProject } = useContext(Context);
+  const [logs, setLogs] = useState<Anser.AnserJsonEntry[][]>([]);
+  const [initialized, setInitialized] = useState(false);
+  const {
+    newWebsocket,
+    openWebsocket,
+    closeAllWebsockets,
+    getWebsocket,
+    closeWebsocket,
+  } = useWebsockets();
+
+  useEffect(() => {
+    refresh();
+  }, [currentPod, namespace, searchParam, startDate]);
+
+  const setupWebsocket = (websocketKey: string) => {
+    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/logs/loki?pod_selector=${currentPod}&namespace=${namespace}&search_param=${searchParam}`;
+
+    const config: NewWebsocketOptions = {
+      onopen: () => {
+        console.log("Opened websocket:", websocketKey);
+      },
+      onmessage: (evt: MessageEvent) => {
+        let newLogs: Anser.AnserJsonEntry[][] = [];
+
+        evt?.data?.split("\n").forEach((logLine: string) => {
+          if (logLine) {
+            var parsedLine = JSON.parse(logLine);
+
+            let ansiLog = Anser.ansiToJson(parsedLine.log);
+            newLogs.push(ansiLog);
+          }
+        });
+
+        setLogs((prevLogs) => {
+          return prevLogs.concat(newLogs);
+        });
+      },
+      onclose: () => {
+        console.log("Closed websocket:", websocketKey);
+      },
+    };
+
+    newWebsocket(websocketKey, endpoint, config);
+    openWebsocket(websocketKey);
+  };
+
+  const refresh = () => {
+    if (!currentPod) {
+      return;
+    }
+
+    const websocketKey = `${currentPod}-${namespace}-websocket`;
+
+    api
+      .getLogs(
+        "<token>",
+        {
+          pod_selector: currentPod,
+          namespace: namespace,
+          search_param: searchParam,
+          start_range: startDate,
+          // end_range: startDate,
+          limit: 100,
+        },
+        {
+          cluster_id: currentCluster.id,
+          project_id: currentProject.id,
+        }
+      )
+      .then((res) => {
+        var initLogs: Anser.AnserJsonEntry[][] = [];
+        res.data.logs?.forEach((logLine: any) => {
+          if (logLine) {
+            var parsedLine = JSON.parse(logLine.line);
+
+            let ansiLog = Anser.ansiToJson(parsedLine.log);
+            initLogs.push(ansiLog);
+          }
+        });
+
+        setLogs(initLogs.reverse());
+        setInitialized(true);
+        closeWebsocket(websocketKey);
+
+        setupWebsocket(websocketKey);
+      });
+  };
+
+  return {
+    logs,
+    refresh,
+  };
+};

+ 3 - 4
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from "react";
 import styled from "styled-components";
 import Anser from "anser";
 import CommandLineIcon from "assets/command-line-icon";
-import ConnectToLogsInstructionModal from "./ConnectToLogsInstructionModal";
 import { SelectedPodType } from "./types";
 import { useLogs } from "./useLogs";
 
@@ -180,7 +179,7 @@ const LogsFC: React.FC<{
             checked={isScrollToBottomEnabled}
             onChange={() => {}}
           />
-          Scroll to Bottom
+          Scroll to bottom
         </Scroll>
         {Array.isArray(previousLogs) && previousLogs.length > 0 && (
           <Scroll
@@ -193,7 +192,7 @@ const LogsFC: React.FC<{
               checked={showPreviousLogs}
               onChange={() => {}}
             />
-            Show previous Logs
+            Show previous logs
           </Scroll>
         )}
         <Refresh
@@ -294,7 +293,7 @@ const Refresh = styled.div`
 const LogTabs = styled.div`
   width: 100%;
   height: 25px;
-  background: #121318;
+  margin-top: -25px;
   display: flex;
   flex-direction: row;
   align-items: center;

+ 2 - 7
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/StatusSection.tsx

@@ -3,9 +3,8 @@ import styled from "styled-components";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
-import { ChartType, StorageType } from "shared/types";
+import { ChartType } from "shared/types";
 import Loading from "components/Loading";
-import backArrow from "assets/back_arrow.png";
 
 import Logs from "./Logs";
 import ControllerTab from "./ControllerTab";
@@ -33,6 +32,7 @@ const StatusSectionFC: React.FunctionComponent<Props> = ({
   );
 
   useEffect(() => {
+    console.log(currentChart);
     let isSubscribed = true;
     api
       .getChartControllers(
@@ -211,11 +211,6 @@ const BackButton = styled.div`
   }
 `;
 
-const BackButtonImg = styled.img`
-  width: 12px;
-  opacity: 0.75;
-`;
-
 const AbsoluteTitle = styled.div`
   position: absolute;
   top: 0px;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx

@@ -125,7 +125,7 @@ const Button = styled(DynamicLink)`
   font-family: "Work Sans", sans-serif;
   border-radius: 5px;
   color: white;
-  height: 35px;
+  height: 30px;
   padding: 0px 8px;
   padding-bottom: 1px;
   margin-right: 10px;

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/preview-environments/components/PreviewEnvironmentsHeader.tsx

@@ -26,6 +26,7 @@ export const PreviewEnvironmentsHeader = () => {
         image={PullRequestIcon}
         title="Preview Environments"
         description="Create full-stack preview environments for your pull requests."
+        disableLineBreak
       />
       {githubStatus != "no active incidents" ? (
         <AlertCard>

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentCard.tsx

@@ -316,7 +316,7 @@ const DeploymentCardWrapper = styled.div`
   padding: 12px;
   padding-left: 14px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
 
   animation: fadeIn 0.5s;

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/DeploymentDetail.tsx

@@ -128,7 +128,6 @@ const DeploymentDetail = () => {
         </Flex>
         <LinkToActionsWrapper></LinkToActionsWrapper>
       </HeaderWrapper>
-      <LineBreak />
       <ChartListWrapper>
         <ChartList
           currentCluster={context.currentCluster}

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/deployments/PullRequestCard.tsx

@@ -141,7 +141,7 @@ const DeploymentCardWrapper = styled.div`
   padding: 12px;
   padding-left: 14px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
 
   animation: fadeIn 0.5s;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentCard.tsx

@@ -167,7 +167,7 @@ const EnvironmentCardWrapper = styled(DynamicLink)`
   padding: 12px;
   padding-left: 14px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;

+ 0 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/environments/EnvironmentsList.tsx

@@ -139,7 +139,6 @@ const Placeholder = styled.div`
 
 const FloatingPlaceholder = styled(Placeholder)`
   position: absolute;
-  background: #3d3f42;
   width: 100%;
   height: 100%;
   margin-top: 0px;

+ 93 - 44
dashboard/src/main/home/cluster-dashboard/stacks/Dashboard.tsx

@@ -1,13 +1,12 @@
 import DynamicLink from "components/DynamicLink";
-import Selector from "components/Selector";
+import RadioFilter from "components/RadioFilter";
 import React, { useEffect, useState } from "react";
 import { useHistory, useLocation } from "react-router";
 import { useRouting } from "shared/routing";
 import styled from "styled-components";
 import DashboardHeader from "../DashboardHeader";
 import { NamespaceSelector } from "../NamespaceSelector";
-import SortSelector from "../SortSelector";
-import { Action } from "./components/styles";
+import sort from "assets/sort.svg";
 import StackList from "./_StackList";
 const Dashboard = () => {
   const [currentNamespace, setCurrentNamespace] = useState("default");
@@ -38,46 +37,44 @@ const Dashboard = () => {
         image={"lan"}
         title="Stacks"
         description="Groups of applications deployed from a shared source."
+        disableLineBreak
       />
-      <Action.Row>
+      <ControlRow>
         <FilterWrapper>
-          <StyledSortSelector>
-            <Label>
-              <i className="material-icons">sort</i> Sort
-            </Label>
-            <Selector
-              activeValue={currentSort}
-              setActiveValue={(sortType) => setCurrentSort(sortType as any)}
-              options={[
-                {
-                  value: "created_at",
-                  label: "Created At",
-                },
-                {
-                  value: "updated_at",
-                  label: "Last Updated",
-                },
-                {
-                  value: "alphabetical",
-                  label: "Alphabetical",
-                },
-              ]}
-              dropdownLabel="Sort By"
-              width="150px"
-              dropdownWidth="230px"
-              closeOverlay={true}
-            />
-          </StyledSortSelector>
           <NamespaceSelector
             namespace={currentNamespace}
             setNamespace={handleNamespaceChange}
           />
         </FilterWrapper>
-        <Action.Button to={"/stacks/launch"}>
-          <i className="material-icons">add</i>
-          Create stack
-        </Action.Button>
-      </Action.Row>
+        <Flex>
+          <RadioFilter
+            selected={currentSort}
+            noMargin
+            dropdownAlignRight={true}
+            setSelected={(sortType: any) => setCurrentSort(sortType as any)}
+            options={[
+              {
+                value: "created_at",
+                label: "Created at",
+              },
+              {
+                value: "updated_at",
+                label: "Last updated",
+              },
+              {
+                value: "alphabetical",
+                label: "Alphabetical",
+              },
+            ]}
+            name="Sort"
+            icon={sort}
+          />
+          <Button to={"/stacks/launch"}>
+            <i className="material-icons">add</i>
+            Create stack
+          </Button>
+        </Flex>
+      </ControlRow>
       <StackList namespace={currentNamespace} sortBy={currentSort} />
     </>
   );
@@ -85,24 +82,76 @@ const Dashboard = () => {
 
 export default Dashboard;
 
-const Label = styled.div`
+const Flex = styled.div`
   display: flex;
   align-items: center;
-  margin-right: 12px;
+  border-bottom: 30px solid transparent;
+`;
+
+const Button = styled(DynamicLink)`
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  cursor: pointer;
+  font-family: "Work Sans", sans-serif;
+  border-radius: 5px;
+  color: white;
+  margin-left: 10px;
+  height: 30px;
+  padding: 0 8px;
+  padding-right: 13px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  box-shadow: 0 5px 8px 0px #00000010;
+  cursor: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "not-allowed" : "pointer"};
+
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
 
   > i {
-    margin-right: 8px;
-    font-size: 18px;
+    color: white;
+    width: 18px;
+    height: 18px;
+    font-weight: 600;
+    font-size: 12px;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    margin-right: 5px;
+    justify-content: center;
   }
 `;
 
-const StyledSortSelector = styled.div`
+const FilterWrapper = styled.div`
   display: flex;
+  justify-content: space-between;
+  border-bottom: 30px solid transparent;
+  > div:not(:first-child) {
+  }
+`;
+
+const ControlRow = styled.div`
+  display: flex;
+  justify-content: space-between;
   align-items: center;
-  font-size: 13px;
-  margin-right: 30px;
+  flex-wrap: wrap;
 `;
 
-const FilterWrapper = styled.div`
+const Label = styled.div`
   display: flex;
+  align-items: center;
+  margin-right: 12px;
+
+  > i {
+    margin-right: 8px;
+    font-size: 18px;
+  }
 `;

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/stacks/components/NewEnvGroupForm.tsx

@@ -103,7 +103,7 @@ const NewEnvGroupForm = (props: {
         width="100%"
       />
 
-      <Heading>Environment Variables</Heading>
+      <Heading>Environment variables</Heading>
       <Helper>
         Set environment variables for your secrets and environment-specific
         configuration.

+ 2 - 3
dashboard/src/main/home/cluster-dashboard/stacks/launch/components/styles.tsx

@@ -3,10 +3,9 @@ import styled from "styled-components";
 
 export const Card = {
   Grid: styled.div`
-    margin-top: 32px;
     margin-bottom: 32px;
     display: grid;
-    grid-row-gap: 25px;
+    grid-row-gap: 15px;
   `,
   Wrapper: styled.div<{ variant?: "clickable" | "unclickable" }>`
     display: flex;
@@ -17,7 +16,7 @@ export const Card = {
     padding-left: 14px;
     align-items: center;
     border-radius: 5px;
-    background: #262a30;
+    background: #26292e;
     border: 1px solid #494b4f;
 
     ${(props) => {

+ 1 - 1
dashboard/src/main/home/dashboard/ClusterList.tsx

@@ -242,7 +242,7 @@ const TemplateBlock = styled.div`
   color: #ffffff;
   position: relative;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;

+ 1 - 1
dashboard/src/main/home/infrastructure/InfrastructureList.tsx

@@ -226,7 +226,7 @@ const StyledTableWrapper = styled.div`
   padding: 14px;
   position: relative;
   border-radius: 8px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   width: 100%;
   height: 100%;

+ 1 - 1
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -233,7 +233,7 @@ const Integration = styled.div`
     props.disabled ? "not-allowed" : "pointer"};
   margin-bottom: 20px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;

+ 1 - 1
dashboard/src/main/home/integrations/IntegrationRow.tsx

@@ -133,7 +133,7 @@ const Integration = styled.div`
     props.disabled ? "not-allowed" : "pointer"};
   margin-bottom: 15px;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;

+ 1 - 1
dashboard/src/main/home/launch/TemplateList.tsx

@@ -207,7 +207,7 @@ const TemplateBlock = styled.div`
   color: #ffffff;
   position: relative;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: 1px solid #7a7b80;

+ 1 - 1
dashboard/src/main/home/launch/launch-flow/SourcePage.tsx

@@ -417,7 +417,7 @@ const Block = styled.div<{ disabled?: boolean }>`
   position: relative;
 
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
   }

+ 0 - 1
dashboard/src/main/home/new-project/NewProject.tsx

@@ -110,7 +110,6 @@ export const NewProjectFC = () => {
 
   const renderContents = () => {
     let version = capabilities?.version;
-    alert(user.email);
     if (version !== "production" || user.email === "support@porter.run") {
       return (
         <>

+ 3 - 1
dashboard/src/main/home/new-project/WelcomeForm.tsx

@@ -78,7 +78,9 @@ const WelcomeForm = (props: any) => {
         ) : (
           <form name="demo" onSubmit={submitForm}>
             <Title>Book a Demo</Title>
-            <Subtitle>Just two things and we'll be in touch.</Subtitle>
+            <Subtitle>
+              Talk to an expert to determine if Porter is a right fit for you.
+            </Subtitle>
             <SubtitleAlt>
               <Num>1</Num> What is your work email? *
             </SubtitleAlt>

+ 1 - 1
dashboard/src/main/home/provisioner/ProvisionerSettings.tsx

@@ -366,7 +366,7 @@ const Block = styled.div<{ disabled?: boolean }>`
   color: #ffffff;
   position: relative;
   border-radius: 5px;
-  background: #262a30;
+  background: #26292e;
   border: 1px solid #494b4f;
   :hover {
     border: ${(props) => (props.disabled ? "" : "1px solid #7a7b80")};

+ 54 - 0
dashboard/src/shared/api.tsx

@@ -1411,6 +1411,22 @@ const createEnvGroup = baseApi<
   return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/namespaces/${pathParams.namespace}/envgroup/create`;
 });
 
+const cloneEnvGroup = baseApi<
+  {
+    name: string;
+    namespace: string;
+    clone_name: string;
+    version: number;
+  },
+  {
+    id: number;
+    namespace: string;
+    cluster_id: number;
+  }
+>("POST", (pathParams) => {
+  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/namespaces/${pathParams.namespace}/envgroup/clone`;
+});
+
 const updateEnvGroup = baseApi<
   {
     name: string;
@@ -2029,6 +2045,41 @@ const getGitlabFolderContent = baseApi<
     `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/${repo_owner}/${repo_name}/${branch}/contents`
 );
 
+const getLogPodValues = baseApi<
+  {
+    match_prefix?: string;
+    start_range?: string;
+    end_range?: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+  }
+>(
+  "GET",
+  ({ project_id, cluster_id }) =>
+    `/api/projects/${project_id}/clusters/${cluster_id}/logs/pod_values`
+);
+
+const getLogs = baseApi<
+  {
+    limit?: number;
+    start_range?: string;
+    end_range?: string;
+    pod_selector: string;
+    namespace: string;
+    search_param?: string;
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+  }
+>(
+  "GET",
+  ({ project_id, cluster_id }) =>
+    `/api/projects/${project_id}/clusters/${cluster_id}/logs`
+);
+
 // STACKS
 
 const createStack = baseApi<
@@ -2438,6 +2489,7 @@ export default {
   getLogBucketLogs,
   getCanCreateProject,
   createEnvGroup,
+  cloneEnvGroup,
   updateEnvGroup,
   listEnvGroups,
   getEnvGroup,
@@ -2464,6 +2516,8 @@ export default {
   getGitlabRepos,
   getGitlabBranches,
   getGitlabFolderContent,
+  getLogPodValues,
+  getLogs,
 
   // STACKS
   listStacks,

+ 7 - 1
dashboard/webpack.config.js

@@ -88,7 +88,13 @@ module.exports = () => {
           test: /\.(png|svg|jpg|gif|mp3)$/,
           use: ["file-loader"],
         },
-        { test: /\.css$/, use: ["css-loader"] },
+        {
+          test: /\.css$/i,
+          loader: "css-loader",
+          options: {
+            import: true,
+          },
+        },
         {
           test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
           use: [

+ 1 - 1
docs/deploy/addons/strapi.md

@@ -12,7 +12,7 @@ This is a quick guide on how to deploy Strapi to a Kubernetes cluster in AWS/GCP
 
 ## Deploying PostgresDB
 1. Strapi instance deployed through Porter connects to PostgresDB. You can connect Strapi instance deployed on Porter to any external database, but it is also possible to use a database that is also deployed on Porter. Follow [this guide to deploy a PostgresDB instance to your cluster in one click](https://docs.getporter.dev/docs/postgresdb).
-2. After the database has been deployed, navigate to the **Environment Variables** tab of your deployed Strapi instance. Configure the following environment variables:
+2. After the database has been deployed, navigate to the **Environment variables** tab of your deployed Strapi instance. Configure the following environment variables:
 ```
 NODE_ENV=production
 DATABASE_HOST=

+ 6 - 3
internal/kubernetes/agent.go

@@ -1758,6 +1758,7 @@ func (a *Agent) StreamHelmReleases(namespace string, chartList []string, selecto
 func (a *Agent) StreamPorterAgentLokiLog(
 	labels []string,
 	startTime string,
+	searchParam string,
 	limit uint32,
 	rw *websocket.WebsocketSafeReadWriter,
 ) error {
@@ -1805,7 +1806,7 @@ func (a *Agent) StreamPorterAgentLokiLog(
 			defer wg.Done()
 
 			podList, err := a.Clientset.CoreV1().Pods("porter-agent-system").List(context.Background(), metav1.ListOptions{
-				LabelSelector: "app.kubernetes.io/instance=porter-agent",
+				LabelSelector: "control-plane=controller-manager",
 			})
 
 			if err != nil {
@@ -1835,8 +1836,6 @@ func (a *Agent) StreamPorterAgentLokiLog(
 				SubResource("exec")
 
 			cmd := []string{
-				"sh",
-				"-c",
 				"/porter/agent-cli",
 				"--start",
 				startTime,
@@ -1846,6 +1845,10 @@ func (a *Agent) StreamPorterAgentLokiLog(
 				cmd = append(cmd, "--label", label)
 			}
 
+			if searchParam != "" {
+				cmd = append(cmd, "--search", searchParam)
+			}
+
 			if limit > 0 {
 				cmd = append(cmd, "--limit", fmt.Sprintf("%d", limit))
 			}

+ 4 - 0
internal/kubernetes/porter_agent/v2/agent_server.go

@@ -173,6 +173,10 @@ func GetHistoricalLogs(
 	vals["pod_selector"] = req.PodSelector
 	vals["namespace"] = req.Namespace
 
+	if req.SearchParam != "" {
+		vals["search_param"] = req.SearchParam
+	}
+
 	resp := clientset.CoreV1().Services(service.Namespace).ProxyGet(
 		"http",
 		service.Name,