git.iko.soy/typeable/octopod
on master help sign in
57 files changed+1412−883
since splitsnapshots
unifiedsplit
.github/workflows/build.yaml+1−1 .nova/Configuration.json+5 .vscode/settings.json+6−1 cabal.project+6 charts/octopod/Chart.yaml+2−2 charts/octopod/index.yaml+37 charts/octopod/templates/ingress-app.yaml+11−5 charts/octopod/templates/ingress-powerapp.yaml+11−5 charts/octopod/templates/ingress-ui.yaml+11−5 charts/octopod/templates/ingress-ws.yaml+11−5 charts/octopod/values.yaml+1−1 dev/default.nix+36−4 helm-control-scripts/src/config_check.rs+6−2 helm-control-scripts/src/lib.rs+8−4 octo-cli/octo-cli.cabal−1 octo-cli/src/Octopod/CLI.hs+8−21 octo-cli/src/Octopod/CLI/Args.hs+2−1 octopod-api/src/Octopod/API/WebSocket.hs+1 octopod-backend/octopod-backend.cabal−1 octopod-backend/src/Control/CacheMap.hs+2−1 octopod-backend/src/Octopod/Server.hs+7−7 octopod-backend/src/Octopod/Server/ControlScriptUtils.hs+2−2 octopod-backend/src/Octopod/Server/Posix.hs+2−2 octopod-common/octopod-common.cabal+3−1 octopod-common/src/Common/Orphans.hs+14−4 octopod-common/src/Common/Types.hs+43−44 octopod-common/src/Control/Searchable.hs+20−1 octopod-common/src/Data/ConfigTree.hs+165 octopod-common/src/Data/Map/Ordered/Strict/Extra.hs−15 octopod-css/development/layouts/staging.html+6 octopod-css/development/markups/_keys-and-values.html+100−1 octopod-css/development/styles/_collapse.css+6−4 octopod-css/development/styles/_key-deleted.css+6 octopod-css/development/styles/_table.css+1−1 octopod-css/development/styles/_value-deleted.css+5 octopod-css/development/styles/_value-unknown.css+12 octopod-css/development/styles/style.css+5 octopod-frontend/octopod-frontend.cabal+4 octopod-frontend/src/Data/Text/Search.hs+14 octopod-frontend/src/Data/WorkingOverrides.hs+104−76 octopod-frontend/src/Frontend/UIKit.hs+281−115 octopod-frontend/src/Frontend/UIKit/Button/Action.hs+3−3 octopod-frontend/src/Frontend/UIKit/Button/Common.hs+64−12 octopod-frontend/src/Frontend/UIKit/Button/Dash.hs+2−2 octopod-frontend/src/Frontend/UIKit/Button/Expander.hs+3−2 octopod-frontend/src/Frontend/UIKit/Button/Large.hs+3−3 octopod-frontend/src/Frontend/UIKit/Button/Sort.hs+2−2 octopod-frontend/src/Frontend/UIKit/Button/Static.hs+25−12 octopod-frontend/src/Frontend/UIKit/Button/Tree.hs+84 octopod-frontend/src/Frontend/Utils.hs+158−392 octopod-frontend/src/Page/ClassicPopup.hs+3−2 octopod-frontend/src/Page/Deployment.hs+39−34 octopod-frontend/src/Page/Deployments.hs+36−72 octopod-frontend/src/Page/Elements/Links.hs+20−11 octopod-frontend/src/Page/Popup/EditDeployment.hs+1−1 octopod-frontend/src/Page/Popup/NewDeployment.hs+1−1 stack.yaml+3−1
.github/workflows/build.yaml+1−1
@@ -29,7 +29,7 @@ jobs:
2929 - uses: cachix/install-nix-action@v12
3030 - name: Run fourmolu
3131 run: |
32− nix-shell nix/ci.nix -j auto --run "git ls-files '*.hs' | xargs fourmolu -m inplace --ghc-opt -XRecursiveDo --ghc-opt -XTypeApplications"
32+ nix-shell nix/ci.nix -j auto --run "git ls-files '*.hs' | xargs fourmolu -m inplace --ghc-opt -XRecursiveDo --ghc-opt -XTypeApplications --ghc-opt -XPatternSynonyms"
3333 git diff --exit-code
3434
3535 octo-macOS:
.nova/Configuration.jsonnew file+5
@@ -0,0 +1,5 @@
1+{
2+ "env-generator" : "nix-shell --run env",
3+ "haskell.checkProject" : false,
4+ "haskell.formattingProvider" : "fourmolu"
5+}
.vscode/settings.json+6−1
@@ -1,4 +1,9 @@
11{
22 "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix",
3− "haskell.formattingProvider": "fourmolu"
3+ "haskell.formattingProvider": "fourmolu",
4+ "haskell.plugin.hlint.diagnosticsOn": false,
5+ "haskell.hlint.logLevel": "none",
6+ "haskell.hlint.run": "never",
7+ "haskell.serverExecutablePath": "haskell-language-server",
8+ "editor.formatOnSave": true
49}
cabal.project+6
@@ -31,5 +31,11 @@ source-repository-package
3131 subdir: lib/executable-config/lookup
3232 --sha256: 0dlk8y6rxc87crw7764zq2py7nqn38lw496ca1y893m9gdq8qdkz
3333
34+source-repository-package
35+ type: git
36+ location: https://github.com/typeable/ordered-containers.git
37+ tag: 3eb05fb2f44fe482e9092aff447e9c68fbd6a7f6
38+ --sha256: 0q3cp3nar41bqh2ldl1ykzaycf0bjxh0n58za5g19x4g7ab8h9gm
39+
3440package reflex-dom
3541 flags: +use-warp
charts/octopod/Chart.yaml+2−2
@@ -2,8 +2,8 @@ apiVersion: v2
22name: octopod
33description: An opensource self-hosted solution for managing multiple deployments in a Kubernetes cluster.
44type: application
5−version: 0.6.6
6−appVersion: 1.4.2
5+version: 0.6.11
6+appVersion: 1.5.2
77keywords:
88 - kubernetes
99 - octopod
charts/octopod/index.yamlnew file+37
@@ -0,0 +1,37 @@
1+apiVersion: v1
2+entries:
3+ postgresql:
4+ - annotations:
5+ category: Database
6+ apiVersion: v2
7+ appVersion: 11.12.0
8+ created: "2022-08-23T14:43:53.959078+03:00"
9+ dependencies:
10+ - name: common
11+ repository: https://charts.bitnami.com/bitnami
12+ version: 1.x.x
13+ description: Chart for PostgreSQL, an object-relational database management system
14+ (ORDBMS) with an emphasis on extensibility and on standards-compliance.
15+ digest: afece60469d73b9cc4b578fff9b7627dd6ae609be50a04dabac5ad0be5105f33
16+ home: https://github.com/bitnami/charts/tree/master/bitnami/postgresql
17+ icon: https://bitnami.com/assets/stacks/postgresql/img/postgresql-stack-220x234.png
18+ keywords:
19+ - postgresql
20+ - postgres
21+ - database
22+ - sql
23+ - replication
24+ - cluster
25+ maintainers:
26+ - email: containers@bitnami.com
27+ name: Bitnami
28+ - email: cedric@desaintmartin.fr
29+ name: desaintmartin
30+ name: postgresql
31+ sources:
32+ - https://github.com/bitnami/bitnami-docker-postgresql
33+ - https://www.postgresql.org/
34+ urls:
35+ - charts/postgresql-10.5.3.tgz
36+ version: 10.5.3
37+generated: "2022-08-23T14:43:53.955624+03:00"
charts/octopod/templates/ingress-app.yaml+11−5
@@ -1,10 +1,13 @@
11{{- if .Values.ingress.enabled -}}
22{{- $fullName := include "octopod.fullname" . -}}
3−{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
3+{{- $kubeVersion := .Capabilities.KubeVersion.Version }}
4+{{ if semverCompare ">= 1.19.0-0" $kubeVersion }}
5+apiVersion: networking.k8s.io/v1
6+{{ else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }}
47apiVersion: networking.k8s.io/v1beta1
5−{{- else -}}
8+{{ else }}
69apiVersion: extensions/v1beta1
7−{{- end }}
10+{{ end }}
811kind: Ingress
912metadata:
1013 name: {{ $fullName }}-app
@@ -41,7 +44,10 @@ spec:
4144 http:
4245 paths:
4346 - path: /
47+ pathType: ImplementationSpecific
4448 backend:
45− serviceName: {{ $fullName }}
46− servicePort: {{ .Values.service.ports.app }}
49+ service:
50+ name: {{ $fullName }}
51+ port:
52+ number: {{ .Values.service.ports.app }}
4753{{- end }}
charts/octopod/templates/ingress-powerapp.yaml+11−5
@@ -1,10 +1,13 @@
11{{- if .Values.ingress.enabled -}}
22{{- $fullName := include "octopod.fullname" . -}}
3−{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
3+{{- $kubeVersion := .Capabilities.KubeVersion.Version }}
4+{{ if semverCompare ">= 1.19.0-0" $kubeVersion }}
5+apiVersion: networking.k8s.io/v1
6+{{ else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }}
47apiVersion: networking.k8s.io/v1beta1
5−{{- else -}}
8+{{ else }}
69apiVersion: extensions/v1beta1
7−{{- end }}
10+{{ end }}
811kind: Ingress
912metadata:
1013 name: {{ $fullName }}-powerapp
@@ -35,7 +38,10 @@ spec:
3538 http:
3639 paths:
3740 - path: /
41+ pathType: ImplementationSpecific
3842 backend:
39− serviceName: {{ $fullName }}
40− servicePort: {{ .Values.service.ports.powerApp }}
43+ service:
44+ name: {{ $fullName }}
45+ port:
46+ number: {{ .Values.service.ports.powerApp }}
4147{{- end }}
charts/octopod/templates/ingress-ui.yaml+11−5
@@ -1,10 +1,13 @@
11{{- if .Values.ingress.enabled -}}
22{{- $fullName := include "octopod.fullname" . -}}
3−{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
3+{{- $kubeVersion := .Capabilities.KubeVersion.Version }}
4+{{ if semverCompare ">= 1.19.0-0" $kubeVersion }}
5+apiVersion: networking.k8s.io/v1
6+{{ else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }}
47apiVersion: networking.k8s.io/v1beta1
5−{{- else -}}
8+{{ else }}
69apiVersion: extensions/v1beta1
7−{{- end }}
10+{{ end }}
811kind: Ingress
912metadata:
1013 name: {{ $fullName }}-ui
@@ -35,7 +38,10 @@ spec:
3538 http:
3639 paths:
3740 - path: /
41+ pathType: ImplementationSpecific
3842 backend:
39− serviceName: {{ $fullName }}
40− servicePort: {{ .Values.service.ports.ui }}
43+ service:
44+ name: {{ $fullName }}
45+ port:
46+ number: {{ .Values.service.ports.ui }}
4147{{- end }}
charts/octopod/templates/ingress-ws.yaml+11−5
@@ -1,10 +1,13 @@
11{{- if .Values.ingress.enabled -}}
22{{- $fullName := include "octopod.fullname" . -}}
3−{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
3+{{- $kubeVersion := .Capabilities.KubeVersion.Version }}
4+{{ if semverCompare ">= 1.19.0-0" $kubeVersion }}
5+apiVersion: networking.k8s.io/v1
6+{{ else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }}
47apiVersion: networking.k8s.io/v1beta1
5−{{- else -}}
8+{{ else }}
69apiVersion: extensions/v1beta1
7−{{- end }}
10+{{ end }}
811kind: Ingress
912metadata:
1013 name: {{ $fullName }}-ws
@@ -35,7 +38,10 @@ spec:
3538 http:
3639 paths:
3740 - path: /
41+ pathType: ImplementationSpecific
3842 backend:
39− serviceName: {{ $fullName }}
40− servicePort: {{ .Values.service.ports.ws }}
43+ service:
44+ name: {{ $fullName }}
45+ port:
46+ number: {{ .Values.service.ports.ws }}
4147{{- end }}
charts/octopod/values.yaml+1−1
@@ -99,7 +99,7 @@ controlScripts:
9999 image:
100100 repository: typeable/octopod-helm-control-scripts
101101 pullPolicy: IfNotPresent
102− tag: 0.2.3
102+ tag: 0.2.4
103103sqitch:
104104 image:
105105 repository: typeable/sqitch
dev/default.nix+36−4
@@ -104,6 +104,7 @@ in
104104 #!${pkgs.bash}/bin/bash
105105
106106 echo "You did something wrong :("
107+ echo "You did something wrong twice :("
107108
108109 1>&2 echo "You did something wrong, but you shouldn't see this."
109110
@@ -115,9 +116,31 @@ in
115116
116117 sleep 4
117118
119+ echo "foo.vat1.fjij.key1,"
120+
121+ for i in {1..10}
122+ do
123+ for j in {1..10}
124+ do
125+ echo "foo.vat$i.fjij.key$j,value"
126+ done
127+ done
128+
129+ for i in {1..10}
130+ do
131+ echo "fjij.key$i,value"
132+ done
133+
134+ exit 0
135+ '';
136+ smallInfoScript = pkgs.writeScript "info.sh" ''
137+ #!${pkgs.bash}/bin/bash
138+
139+ sleep 4
140+
118141 for i in {1..10}
119142 do
120− echo "key$i,value"
143+ echo "fjij.key$i,value"
121144 done
122145
123146 exit 0
@@ -130,7 +153,16 @@ in
130153
131154 for i in {1..10}
132155 do
133− echo "key$i"
156+ echo "foo.vat.fkijf.fjij.key$i"
157+ done
158+ for i in {1..10}
159+ do
160+ echo "foo.vat.fjij.key$i"
161+ done
162+
163+ for i in {1..10}
164+ do
165+ echo "fjij.key$i"
134166 done
135167
136168 exit 0
@@ -162,8 +194,8 @@ in
162194 export CHECKING_COMMAND=${echoScript}
163195 export CLEANUP_COMMAND=${echoScript}
164196 export ARCHIVE_CHECKING_COMMAND=${echoScript}
165− export CONFIG_CHECKING_COMMAND=${echoScript}
166− export INFO_COMMAND=${infoScript}
197+ export CONFIG_CHECKING_COMMAND=${failScript}
198+ export INFO_COMMAND=${smallInfoScript}
167199 export NOTIFICATION_COMMAND=${writeScript}
168200 export DEPLOYMENT_CONFIG_COMMAND=${infoScript}
169201 export DEPLOYMENT_KEYS_COMMAND=${keysScript}
helm-control-scripts/src/config_check.rs+6−2
@@ -35,7 +35,10 @@ fn main() {
3535 Ok(status) => {
3636 let (deployments, statefulsets, _ingresses, _old_ingresses, _postgresqls, _kafkas) = match parse_to_k8s(status) {
3737 Ok((deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas)) => (deployments, statefulsets, ingresses, old_ingresses, postgresqls, kafkas),
38− Err(err) => panic!("{}", err)
38+ Err(err) => {
39+ println!("Something went wrong: {}", err);
40+ panic!("{}", err);
41+ }
3942 };
4043 match deployments_statefulsets_to_images(deployments, statefulsets) {
4144 Some(images) => {
@@ -53,7 +56,8 @@ fn main() {
5356 }
5457 }
5558 }
56− Err(status) => {
59+ Err((status, err)) => {
60+ println!("{:#}", err);
5761 error!("Error during helm execution");
5862 panic!("{:?}", status);
5963 }
helm-control-scripts/src/lib.rs+8−4
@@ -254,7 +254,7 @@ pub mod lib {
254254 Err(helm.status)
255255 }
256256 }
257− pub fn run_stdout(&self) -> Result<String, ExitStatus> {
257+ pub fn run_stdout(&self) -> Result<String, (ExitStatus, String)> {
258258 let helm = Command::new(&self.name)
259259 .args(&self.args())
260260 .output()
@@ -262,9 +262,11 @@ pub mod lib {
262262 if helm.status.success() {
263263 Ok(String::from_utf8(helm.stdout).unwrap())
264264 } else {
265− info!("helm stdout:\n {}", String::from_utf8(helm.stdout).unwrap());
266− info!("helm stderr:\n {}", String::from_utf8(helm.stderr).unwrap());
267− Err(helm.status)
265+ let stdout = String::from_utf8(helm.stdout).unwrap();
266+ let stderr = String::from_utf8(helm.stderr).unwrap();
267+ info!("helm stdout:\n {}", stdout);
268+ info!("helm stderr:\n {}", stderr);
269+ Err((helm.status, stderr))
268270 }
269271 }
270272 }
@@ -731,6 +733,7 @@ pub mod lib {
731733 match helm_repo_add.run() {
732734 Ok(_status) => info!("Repo add success!"),
733735 Err(status) => {
736+ println!("Couldn't add repo.");
734737 error!("Error during helm execution");
735738 panic!("{:?}", status);
736739 }
@@ -738,6 +741,7 @@ pub mod lib {
738741 match helm_repo_update.run() {
739742 Ok(_status) => info!("Repo update success!"),
740743 Err(status) => {
744+ println!("Couldn't update repo.");
741745 error!("Error during helm execution");
742746 panic!("{:?}", status);
743747 }
octo-cli/octo-cli.cabal−1
@@ -59,7 +59,6 @@ executable octo
5959 transformers,
6060 table-layout ^>= 0.9.0.0,
6161 servant-auth,
62− ordered-containers,
6362 http-types,
6463 hs-source-dirs: src
6564 default-language: Haskell2010
octo-cli/src/Octopod/CLI.hs+8−21
@@ -13,9 +13,10 @@ import Data.Aeson (decode)
1313import qualified Data.ByteString.Char8 as BSC
1414import qualified Data.ByteString.Lazy.Char8 as LBSC
1515import Data.Coerce
16+import qualified Data.ConfigTree as CT
17+import Data.Foldable
1618import Data.Generics.Labels ()
1719import Data.Generics.Product
18−import qualified Data.Map.Ordered.Strict as OM
1920import Data.Maybe
2021import Data.Proxy
2122import Data.Text (Text)
@@ -123,24 +124,10 @@ handleUpdate ::
123124handleUpdate auth dName dNewAppOvs removedAppOvs dNewDepOvs removedDepOvs = do
124125 clientEnv <- ask
125126 dep <- runClientM' (_fullInfoH auth dName) clientEnv
126− let removeAll :: Ord k => [k] -> OM.OMap k v -> Either k (OM.OMap k v)
127− removeAll [] m = Right m
128− removeAll (k : kk) m =
129− if k `OM.member` m
130− then removeAll kk $ OM.delete k m
131− else Left k
132− removeAllM :: MonadIO m => [Text] -> Overrides l -> m (Overrides l)
133− removeAllM ks (Overrides m) =
134− either
135− (\k -> dieT $ "Override " <> k <> " not present in deployment.")
136− (pure . Overrides)
137− $ removeAll ks m
138− appOverrides' <-
139− fmap (<> dNewAppOvs) $
140− removeAllM removedAppOvs $ dep ^. #deployment . #appOverrides
141− deploymentOverrides' <-
142− fmap (<> dNewDepOvs) $
143− removeAllM removedDepOvs $ dep ^. #deployment . #deploymentOverrides
127+ let removeAll :: [Text] -> Overrides l -> (Overrides l)
128+ removeAll ks (Overrides (CT.toFlatList -> m)) = Overrides $ CT.fromFlatList $ filter (flip elem ks . fst) m
129+ appOverrides' = dNewAppOvs <> (removeAll removedAppOvs $ dep ^. #deployment . #appOverrides)
130+ deploymentOverrides' = dNewDepOvs <> (removeAll removedDepOvs $ dep ^. #deployment . #deploymentOverrides)
144131 liftIO $ do
145132 let dUpdate =
146133 DeploymentUpdate
@@ -295,7 +282,7 @@ printInfo (DeploymentInfo (Deployment _ dAppOvs dStOvs) (DeploymentMetadata dMet
295282 putStrLn $ unlines $ formatOverrides False dStOvs
296283 T.putStrLn ""
297284 T.putStrLn $ "Metadata: "
298− forM_ dMeta $ \m ->
285+ for_ dMeta $ \m ->
299286 T.putStrLn $
300287 " " <> m ^. #name <> ": " <> m ^. #link
301288 T.putStrLn ""
@@ -352,7 +339,7 @@ formatOverrides splitlines (Overrides m) =
352339 ]
353340 unicodeS
354341 def
355− $ showOverride <$> (reverse . OM.assocs) m
342+ $ showOverride <$> (reverse . CT.toFlatList) m
356343 where
357344 showOverride (k, v) =
358345 colsAllG top $ [if splitlines then T.chunksOf 15 k else [k], showValue v]
octo-cli/src/Octopod/CLI/Args.hs+2−1
@@ -68,7 +68,8 @@ readLogOutput _ = Nothing
6868
6969-- | Parses octo CLI subcommands and arguments.
7070parseArgs :: IO Args
71−parseArgs = execParser $ info (commandArgs <**> helper) fullDesc
71+parseArgs =
72+ customExecParser (prefs showHelpOnError) $ info (commandArgs <**> helper) fullDesc
7273
7374-- | Parses octo CLI subcommands.
7475commandArgs :: Parser Args
octopod-api/src/Octopod/API/WebSocket.hs+1
@@ -1,3 +1,4 @@
1+{- ORMOLU_DISABLE -}
12{-# LANGUAGE CPP #-}
23
34module Octopod.API.WebSocket
octopod-backend/octopod-backend.cabal−1
@@ -113,7 +113,6 @@ library
113113 , time
114114 , hasql
115115 , hasql-transaction
116− , ordered-containers
117116 , unordered-containers
118117 , vector
119118 , process
octopod-backend/src/Control/CacheMap.hs+2−1
@@ -12,6 +12,7 @@ import Control.Monad.Base
1212import Control.Monad.Except
1313import Control.Monad.Trans.Control
1414import Data.Fixed
15+import Data.Foldable
1516import Data.IORef.Lifted
1617import Data.Map.Strict (Map)
1718import qualified Data.Map.Strict as M
@@ -81,7 +82,7 @@ lookupBlocking (CacheMap ref createValue update) k = do
8182 res@(Right _) -> do
8283 putMVar var res
8384 pure $ (var, if recompute then Just blank else Nothing)
84− forM_ mwVar $ \wVar -> do
85+ for_ mwVar $ \wVar -> do
8586 fork $ do
8687 v <- (Right <$> createValue k) `catchError` (pure . Left)
8788 putMVar wVar v
octopod-backend/src/Octopod/Server.hs+7−7
@@ -23,6 +23,7 @@ import qualified Data.ByteString.Lazy as BSL
2323import qualified Data.ByteString.Lazy.Char8 as BSLC
2424import Data.Coerce
2525import Data.Conduit (ConduitT, yield)
26+import qualified Data.ConfigTree as CT
2627import qualified Data.Csv as C
2728import Data.Fixed
2829import Data.Foldable
@@ -32,7 +33,6 @@ import Data.Generics.Labels ()
3233import Data.Generics.Product
3334import Data.IORef.Lifted
3435import Data.Int
35−import qualified Data.Map.Ordered.Strict as OM
3636import Data.Maybe
3737import Data.Pool
3838import Data.Text (pack)
@@ -192,7 +192,7 @@ runOctopodServer sha = do
192192 decodeCSVDefaultConfig :: BSL.ByteString -> Either String (DefaultConfig l)
193193 decodeCSVDefaultConfig bs = do
194194 x <- C.decode C.NoHeader bs
195− pure $ DefaultConfig . OM.fromList . V.toList $ x
195+ pure $ DefaultConfig . CT.fromFlatList . V.toList $ x
196196 either500S :: (KatipContext m, MonadError ServerError m) => Either String x -> m x
197197 either500S (Right x) = pure x
198198 either500S (Left err) = do
@@ -478,7 +478,7 @@ getSingleFullInfo dName = do
478478 d <- each deploymentSchema
479479 where_ $ d ^. #name ==. litExpr dName
480480 pure d
481− deployments <- forM deploymentsSchema extractDeploymentFullInfo
481+ deployments <- for deploymentsSchema extractDeploymentFullInfo
482482 logLocM DebugS $ "get deployments: " <> show' deployments
483483 return $ listToMaybe deployments
484484
@@ -489,7 +489,7 @@ getFullInfo = do
489489 deploymentsSchema <-
490490 runStatement . select . orderBy (view #updatedAt >$< desc) $
491491 each deploymentSchema
492− deployments <- forM deploymentsSchema extractDeploymentFullInfo
492+ deployments <- for deploymentsSchema extractDeploymentFullInfo
493493 logLocM DebugS $ "get deployments: " <> show' deployments
494494 return deployments
495495
@@ -739,7 +739,7 @@ transitionToStatus dName s = do
739739 (deps, oldS) <- either throwError pure res
740740 dep <- lift $ ensureOne deps
741741 lift $
742− forM_ (processOutput s) $ \(output, act) ->
742+ for_ (processOutput s) $ \(output, act) ->
743743 createDeploymentLog
744744 dep
745745 act
@@ -748,7 +748,7 @@ transitionToStatus dName s = do
748748 (output ^. #stdout)
749749 (output ^. #stderr)
750750 notificationCmd <- asks notificationCommand
751− forM_ notificationCmd $ \nCmd ->
751+ for_ notificationCmd $ \nCmd ->
752752 runBgWorker . void $
753753 runCommandArgs' nCmd
754754 =<< notificationCommandArgs dName oldS newS
@@ -1307,7 +1307,7 @@ runDeploymentBgWorker newS dName pre post = do
13071307 dName
13081308 (respondSync $ Left err409 {errBody = "The deployment is currently being processed."})
13091309 ( do
1310− let preWithAssert = pre <* forM newS (assertDeploymentTransitionPossibleS dName)
1310+ let preWithAssert = pre <* for newS (assertDeploymentTransitionPossibleS dName)
13111311 result <- catchError (Right <$> preWithAssert) (pure . Left)
13121312 respondSync result
13131313 liftEither result >>= \res -> do
octopod-backend/src/Octopod/Server/ControlScriptUtils.hs+2−2
@@ -37,9 +37,9 @@ import Control.Monad.Base
3737import Control.Monad.Reader
3838import qualified Data.ByteString.Lazy as TL
3939import Data.Coerce
40+import qualified Data.ConfigTree as CT
4041import Data.Fixed
4142import Data.Generics.Product.Typed
42−import qualified Data.Map.Ordered.Strict as MO
4343import qualified Data.Text as T
4444import qualified Data.Text.Encoding as T
4545import Data.Time
@@ -257,7 +257,7 @@ overridesArgs :: forall l. KnownOverrideLevel l => Config l -> ControlScriptArgs
257257overridesArgs (Config cc) =
258258 ControlScriptArgs
259259 . concatMap (\(T.unpack -> k, T.unpack -> v) -> [argumentName, k <> "=" <> v])
260− . MO.assocs
260+ . CT.toFlatList
261261 $ cc
262262 where
263263 argumentName = case knownOverrideLevel @l of
octopod-backend/src/Octopod/Server/Posix.hs+2−2
@@ -1,8 +1,8 @@
11module Octopod.Server.Posix (installShutdownHandler) where
22
3−import Control.Monad
43import Control.Monad.Trans.Control
54import Data.Aeson
5+import Data.Traversable
66import Octopod.Server.Logging
77import System.Posix.Signals
88
@@ -21,7 +21,7 @@ installShutdownHandler ::
2121 m () ->
2222 m [Handler]
2323installShutdownHandler signals action =
24− forM signals $ \signal -> liftBaseWith $ \run ->
24+ for signals $ \signal -> liftBaseWith $ \run ->
2525 installHandler signal (Catch $ run (handler signal)) Nothing
2626 where
2727 handler signal = katipAddNamespace "signal handler" $
octopod-common/octopod-common.cabal+3−1
@@ -18,7 +18,7 @@ library
1818 exposed-modules: Common.Types
1919 , Common.Utils
2020 , Common.Validation
21− , Data.Map.Ordered.Strict.Extra
21+ , Data.ConfigTree
2222 , Control.Searchable
2323 , Common.Orphans
2424 -- other-modules:
@@ -56,6 +56,7 @@ library
5656 , UndecidableInstances
5757 , FunctionalDependencies
5858 , QuantifiedConstraints
59+ , PatternSynonyms
5960 build-depends: base
6061 , aeson
6162 , cassava
@@ -69,6 +70,7 @@ library
6970 , time
7071 , ordered-containers
7172 , deepseq
73+ , these
7274 hs-source-dirs: src
7375 default-language: Haskell2010
7476 ghc-options:
octopod-common/src/Common/Orphans.hs+14−4
@@ -3,8 +3,18 @@
33module Common.Orphans () where
44
55import Control.DeepSeq
6−import Data.Map.Ordered
6+import Data.Aeson
7+import Data.Map.Internal
8+import Data.Map.Ordered.Internal
9+import qualified Data.Map.Ordered.Strict as OM
10+import GHC.Generics (Generic)
711
8−-- better than nothing
9−instance (NFData a, NFData b) => NFData (OMap a b) where
10− rnf = rnf . toMap
12+deriving stock instance Generic (Map k v)
13+
14+deriving stock instance Generic (OMap k v)
15+deriving anyclass instance (NFData k, NFData v) => NFData (OMap k v)
16+instance (ToJSON k, ToJSON v) => ToJSON (OMap k v) where
17+ toJSON = toJSON . OM.assocs
18+
19+instance (FromJSON k, FromJSON v, Ord k) => FromJSON (OMap k v) where
20+ parseJSON = fmap OM.fromList . parseJSON
octopod-common/src/Common/Types.hs+43−44
@@ -12,12 +12,12 @@ import Control.DeepSeq (NFData)
1212import Control.Lens
1313import Control.Searchable
1414import Data.Aeson hiding (Result)
15+import Data.ConfigTree (ConfigTree)
16+import qualified Data.ConfigTree as CT
1517import Data.Csv
1618import Data.Generics.Labels ()
1719import Data.Int
18−import Data.Map.Ordered.Strict (OMap, (<>|))
19−import qualified Data.Map.Ordered.Strict as OM
20−import Data.Map.Ordered.Strict.Extra ()
20+import Data.List.NonEmpty (NonEmpty)
2121import Data.Maybe
2222import Data.Text (Text)
2323import qualified Data.Text as T
@@ -54,21 +54,21 @@ type OverrideValue = OverrideValue' Text
5454
5555type DefaultConfig = DefaultConfig' Text
5656
57−newtype DefaultConfig' te (l :: OverrideLevel) = DefaultConfig (OMap te te)
58− deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
57+newtype DefaultConfig' te (l :: OverrideLevel) = DefaultConfig (ConfigTree te te)
58+ deriving newtype (Eq, Ord, Show)
59+
60+deriving newtype instance FromJSON (DefaultConfig' Text l)
61+deriving newtype instance ToJSON (DefaultConfig' Text l)
5962
6063instance Searchable t x => Searchable t (DefaultConfig' x l) where
6164 type SearchableConstraint t (DefaultConfig' x l) res = (Ord (Searched x res), SearchableConstraint t x res)
6265 type Searched (DefaultConfig' x l) res = DefaultConfig' (Searched x res) l
63− searchWith f (DefaultConfig oMap) = do
64− oMap' <- searchWith f . OM.assocs $ oMap
65− pure $ DefaultConfig $ OM.fromList oMap'
66+ searchWith f (DefaultConfig ct) = DefaultConfig <$> searchWith f ct
6667 {-# INLINE searchWith #-}
6768
68−lookupDefaultConfig :: DefaultConfig l -> Text -> Maybe Text
69−lookupDefaultConfig (DefaultConfig m) k = OM.lookup k m
69+type ConfigKey = [Text]
7070
71−newtype Config (l :: OverrideLevel) = Config {unConfig :: OMap Text Text}
71+newtype Config (l :: OverrideLevel) = Config {unConfig :: ConfigTree Text Text}
7272 deriving newtype (Eq, Ord, Show, ToJSON, FromJSON)
7373
7474data FullDefaultConfig = FullDefaultConfig
@@ -87,63 +87,58 @@ data FullConfig = FullConfig
8787
8888applyOverrides :: Overrides l -> DefaultConfig l -> Config l
8989applyOverrides (Overrides oo) (DefaultConfig dd) =
90− Config . extract $ oo <>| (ValueAdded <$> dd)
90+ Config . extract $ oo <> (ValueAdded <$> dd)
9191 where
92− extract :: OMap Text OverrideValue -> OMap Text Text
92+ extract :: ConfigTree Text OverrideValue -> ConfigTree Text Text
9393 extract =
9494 fmap
9595 ( \case
9696 ValueAdded v -> v
9797 ValueDeleted -> error "invariant"
9898 )
99− . OM.filter
100− ( \_ -> \case
99+ . CT.filter
100+ ( \case
101101 ValueAdded _ -> True
102102 ValueDeleted -> False
103103 )
104104
105+newtype Overrides' t (l :: OverrideLevel) = Overrides {unOverrides :: ConfigTree t (OverrideValue' t)}
106+ deriving newtype (Eq, Ord, Show, NFData, Semigroup, Monoid)
107+
108+deriving newtype instance FromJSON (Overrides' Text l)
109+deriving newtype instance ToJSON (Overrides' Text l)
110+
105111instance Searchable t x => Searchable t (Overrides' x l) where
106112 type SearchableConstraint t (Overrides' x l) res = (Ord (Searched x res), SearchableConstraint t x res)
107113 type Searched (Overrides' x l) res = Overrides' (Searched x res) l
108− searchWith f (Overrides oMap) = do
109− oMap' <- searchWith f . OM.assocs $ oMap
110− pure $ Overrides $ OM.fromList oMap'
114+ searchWith f (Overrides ct) = Overrides <$> searchWith f ct
111115 {-# INLINE searchWith #-}
112116
113−newtype Overrides' t (l :: OverrideLevel) = Overrides {unOverrides :: OMap t (OverrideValue' t)}
114− deriving newtype (Eq, Ord, Show, ToJSON, FromJSON, NFData)
115−
116117type Overrides = Overrides' Text
117118
118119extractOverrides :: DefaultConfig l -> Config l -> Overrides l
119120extractOverrides (DefaultConfig dCfg) (Config cfg) =
120− Overrides . OM.fromList $ removed <> present
121+ Overrides . CT.fromList $ removed <> present
121122 where
122− present :: [(Text, OverrideValue)]
123− present = mapMaybe processPresent . OM.assocs $ cfg
123+ present :: [(NonEmpty Text, OverrideValue)]
124+ present = mapMaybe processPresent . CT.toList $ cfg
124125
125− processPresent :: (Text, Text) -> Maybe (Text, OverrideValue)
126− processPresent (k, v) = case OM.lookup k dCfg of
126+ processPresent :: (NonEmpty Text, Text) -> Maybe (NonEmpty Text, OverrideValue)
127+ processPresent (k, v) = case CT.lookup k dCfg of
127128 Just v' | v == v' -> Nothing
128129 _ -> Just (k, ValueAdded v)
129130
130− processRemoved :: (Text, Text) -> Maybe (Text, OverrideValue)
131+ processRemoved :: (NonEmpty Text, Text) -> Maybe (NonEmpty Text, OverrideValue)
131132 processRemoved (k, _) =
132− if OM.member k cfg
133+ if CT.member k cfg
133134 then Nothing
134135 else Just (k, ValueDeleted)
135136
136− removed :: [(Text, OverrideValue)]
137− removed = mapMaybe processRemoved . OM.assocs $ dCfg
137+ removed :: [(NonEmpty Text, OverrideValue)]
138+ removed = mapMaybe processRemoved . CT.toList $ dCfg
138139
139−ov :: Text -> OverrideValue -> Overrides l
140−ov k v = Overrides $ OM.singleton (k, v)
141−
142−instance Semigroup (Overrides l) where
143− (Overrides lhs) <> (Overrides rhs) = Overrides $ rhs <>| lhs
144−
145−instance Monoid (Overrides l) where
146− mempty = Overrides OM.empty
140+ov :: NonEmpty Text -> OverrideValue -> Overrides l
141+ov k v = Overrides $ CT.singleton k v
147142
148143newtype DeploymentId = DeploymentId {unDeploymentId :: Int64}
149144 deriving stock (Show)
@@ -243,9 +238,11 @@ data Deployment' t = Deployment
243238 , deploymentOverrides :: Overrides' t 'DeploymentLevel
244239 }
245240 deriving stock (Generic, Show, Eq)
246− deriving (FromJSON, ToJSON) via Snake (Deployment' t)
247241 deriving anyclass (NFData)
248242
243+deriving via Snake (Deployment' Text) instance FromJSON (Deployment' Text)
244+deriving via Snake (Deployment' Text) instance ToJSON (Deployment' Text)
245+
249246instance (Searchable needle t) => Searchable needle (Deployment' t) where
250247 type
251248 SearchableConstraint needle (Deployment' t) res =
@@ -317,9 +314,11 @@ data DeploymentFullInfo' t = DeploymentFullInfo
317314 , updatedAt :: UTCTime
318315 }
319316 deriving stock (Generic, Show, Eq)
320− deriving (FromJSON, ToJSON) via Snake (DeploymentFullInfo' t)
321317 deriving anyclass (NFData)
322318
319+deriving via Snake (DeploymentFullInfo' Text) instance FromJSON (DeploymentFullInfo' Text)
320+deriving via Snake (DeploymentFullInfo' Text) instance ToJSON (DeploymentFullInfo' Text)
321+
323322type DeploymentFullInfo = DeploymentFullInfo' Text
324323
325324isDeploymentArchived :: DeploymentFullInfo' t -> Bool
@@ -386,14 +385,14 @@ parseSetOverrides texts = do
386385 Just x -> Right x
387386 Nothing ->
388387 Left $ "Malformed override key-value pair " <> text <> ", should be similar to FOO=bar"
389− return . Overrides $ OM.fromList pairs'
388+ return . Overrides $ CT.fromList pairs'
390389 where
391− parseSingleOverride :: Text -> Maybe (Text, OverrideValue)
390+ parseSingleOverride :: Text -> Maybe (NonEmpty Text, OverrideValue)
392391 parseSingleOverride t
393392 | Just i <- T.findIndex (== '=') t =
394− let (key, value) = bimap T.strip (T.tail . T.strip) $ T.splitAt i t
393+ let (CT.deconstructConfigKey -> key, value) = bimap T.strip (T.tail . T.strip) $ T.splitAt i t
395394 in Just (key, ValueAdded value)
396395 parseSingleOverride _ = Nothing
397396
398397parseUnsetOverrides :: [Text] -> Overrides l
399−parseUnsetOverrides = Overrides . OM.fromList . fmap (,ValueDeleted)
398+parseUnsetOverrides = Overrides . CT.fromList . fmap ((,ValueDeleted) . CT.deconstructConfigKey)
octopod-common/src/Control/Searchable.hs+20−1
@@ -2,11 +2,14 @@
22
33module Control.Searchable
44 ( Searchable (..),
5+ traverseOMap,
56 )
67where
78
89import Control.Lens
910import Data.Kind
11+import Data.Map.Ordered (OMap)
12+import qualified Data.Map.Ordered.Strict as OM
1013import Data.Text (Text)
1114
1215-- | This says that you can search 'needle's in a 'haystack'. What precisely
@@ -38,10 +41,26 @@ instance Searchable needle x => Searchable needle [x] where
3841 {-# INLINE searchWith #-}
3942
4043instance (Searchable needle a, Searchable needle b) => Searchable needle (a, b) where
41− type SearchableConstraint needle (a, b) res = (SearchableConstraint needle a res, SearchableConstraint needle b res)
44+ type
45+ SearchableConstraint needle (a, b) res =
46+ (SearchableConstraint needle a res, SearchableConstraint needle b res)
4247 type Searched (a, b) res = (Searched a res, Searched b res)
4348 searchWith f (a, b) = do
4449 a' <- searchWith f a
4550 b' <- searchWith f b
4651 pure (a', b')
4752 {-# INLINE searchWith #-}
53+
54+instance Searchable needle haystack => Searchable needle (Maybe haystack) where
55+ type SearchableConstraint needle (Maybe haystack) res = SearchableConstraint needle haystack res
56+ type Searched (Maybe haystack) res = Maybe (Searched haystack res)
57+ searchWith _ Nothing = pure Nothing
58+ searchWith f (Just h) = Just <$> searchWith f h
59+ {-# INLINE searchWith #-}
60+
61+traverseOMap ::
62+ forall h k x y.
63+ Ord k =>
64+ (forall f. Applicative f => (h -> f k) -> (x -> f y) -> OMap h x -> f (OMap k y))
65+traverseOMap kf vf (OM.assocs -> l) =
66+ fmap OM.fromList $ (\(h, x) -> (,) <$> kf h <*> vf x) `traverse` l
octopod-common/src/Data/ConfigTree.hsnew file+165
@@ -0,0 +1,165 @@
1+module Data.ConfigTree
2+ ( ConfigTree (..),
3+ null,
4+ filter,
5+ toList,
6+ singleton,
7+ fromList,
8+ lookup,
9+ member,
10+ deconstructConfigKey,
11+ reconstructConfigKey,
12+ toFlatList,
13+ fromFlatList,
14+ Data.ConfigTree.zip,
15+ catMaybes,
16+ insert,
17+ markValues,
18+ )
19+where
20+
21+import Common.Orphans ()
22+import Control.Applicative
23+import Control.DeepSeq (NFData)
24+import Control.Searchable
25+import Data.Aeson
26+import qualified Data.Bifunctor as Bi
27+import Data.Foldable (fold)
28+import qualified Data.List as L
29+import Data.List.NonEmpty (NonEmpty)
30+import qualified Data.List.NonEmpty as NE
31+import Data.Map.Ordered.Strict (OMap)
32+import qualified Data.Map.Ordered.Strict as OM
33+import Data.Maybe (fromJust, fromMaybe, isJust)
34+import Data.String
35+import Data.Text (Text)
36+import qualified Data.Text as T
37+import Data.These
38+import GHC.Generics (Generic)
39+import Prelude hiding (filter, lookup, null)
40+
41+zip :: Ord k => ConfigTree k v -> ConfigTree k h -> ConfigTree k (These v h)
42+zip (ConfigTree m) (ConfigTree w) =
43+ let intersection =
44+ OM.intersectionWith
45+ ( \_ (mv, vMap) (mh, hMap) ->
46+ ( case (mv, mh) of
47+ (Nothing, Nothing) -> Nothing
48+ (Just v, Nothing) -> Just $ This v
49+ (Nothing, Just h) -> Just $ That h
50+ (Just v, Just h) -> Just $ These v h
51+ , Data.ConfigTree.zip vMap hMap
52+ )
53+ )
54+ m
55+ w
56+ vCT = ConfigTree $ m OM.\\ intersection
57+ hCT = ConfigTree $ w OM.\\ intersection
58+ in fmap This vCT <> ConfigTree intersection <> fmap That hCT
59+
60+newtype ConfigTree k v = ConfigTree (OMap k (Maybe v, ConfigTree k v))
61+ deriving newtype (Eq, Ord, Show, NFData)
62+ deriving stock (Functor, Generic)
63+
64+instance ToJSON v => ToJSON (ConfigTree Text v) where
65+ toJSON = toJSON . toFlatList
66+
67+instance FromJSON v => FromJSON (ConfigTree Text v) where
68+ parseJSON = fmap fromFlatList . parseJSON
69+
70+markValues :: (IsString t, Monoid t, Ord t) => ConfigTree t c -> ConfigTree t (t, c)
71+markValues = fromList . fmap (\(k, v) -> (k, (reconstructConfigKey k, v))) . toList
72+{-# INLINE markValues #-}
73+
74+toFlatList :: (IsString t, Monoid t) => ConfigTree t c -> [(t, c)]
75+toFlatList = fmap (Bi.first reconstructConfigKey) . toList
76+{-# INLINE toFlatList #-}
77+
78+fromFlatList :: [(Text, v)] -> ConfigTree Text v
79+fromFlatList = fromList . fmap (Bi.first deconstructConfigKey)
80+{-# INLINE fromFlatList #-}
81+
82+toList :: ConfigTree k v -> [(NonEmpty k, v)]
83+toList (ConfigTree m) =
84+ OM.assocs m >>= \(k, (mv, s)) ->
85+ maybe id (\v -> (:) (pure k, v)) mv $ Bi.first (NE.cons k) <$> toList s
86+{-# INLINE toList #-}
87+
88+fromList :: Ord k => [(NonEmpty k, v)] -> ConfigTree k v
89+fromList = foldMap $ uncurry singleton
90+{-# INLINE fromList #-}
91+
92+singleton :: Ord k => NonEmpty k -> v -> ConfigTree k v
93+singleton (NE k) v = ConfigTree $ OM.singleton (k, (Just v, mempty))
94+singleton (NECons k ks) v = ConfigTree $ OM.singleton (k, (Nothing, singleton ks v))
95+{-# INLINE singleton #-}
96+
97+lookup :: Ord k => NonEmpty k -> ConfigTree k v -> Maybe v
98+lookup (NE k) (ConfigTree m) = OM.lookup k m >>= fst
99+lookup (NECons k ks) (ConfigTree m) = OM.lookup k m >>= lookup ks . snd
100+{-# INLINE lookup #-}
101+
102+member :: Ord k => NonEmpty k -> ConfigTree k v -> Bool
103+member (NE k) (ConfigTree m) = OM.member k m
104+member (NECons k ks) (ConfigTree m) = maybe False (member ks . snd) $ OM.lookup k m
105+{-# INLINE member #-}
106+
107+null :: ConfigTree l v -> Bool
108+null (ConfigTree m) = OM.null m
109+{-# INLINE null #-}
110+
111+filter :: Ord k => (v -> Bool) -> ConfigTree k v -> ConfigTree k v
112+filter f (ConfigTree m) =
113+ ConfigTree $
114+ OM.filter (\_ (mv, ConfigTree m') -> isJust mv || not (OM.null m'))
115+ . fmap (\(mv, ct) -> (mv >>= (\v -> if f v then Just v else Nothing), filter f ct))
116+ $ m
117+{-# INLINE filter #-}
118+
119+instance Ord k => Semigroup (ConfigTree k v) where
120+ (ConfigTree lMap) <> (ConfigTree rMap) =
121+ ConfigTree $ OM.unionWithR (\_ (lvm, lm) (rvm, rm) -> (lvm <|> rvm, lm <> rm)) lMap rMap
122+ {-# INLINE (<>) #-}
123+
124+instance Ord k => Monoid (ConfigTree k v) where
125+ mempty = ConfigTree OM.empty
126+
127+instance (Searchable needle k, Searchable needle v) => Searchable needle (ConfigTree k v) where
128+ type
129+ SearchableConstraint needle (ConfigTree k v) res =
130+ (SearchableConstraint needle k res, SearchableConstraint needle v res, Ord (Searched k res))
131+ type Searched (ConfigTree k v) res = ConfigTree (Searched k res) (Searched v res)
132+ searchWith f (ConfigTree oMap) = do
133+ oMap' <- traverseOMap (searchWith f) (searchWith f) oMap
134+ pure $ ConfigTree oMap'
135+ {-# INLINE searchWith #-}
136+
137+deconstructConfigKey :: Text -> NonEmpty Text
138+deconstructConfigKey =
139+ fromMaybe (pure "") . NE.nonEmpty . L.filter (not . T.null) . fmap T.strip . T.splitOn "."
140+{-# INLINE deconstructConfigKey #-}
141+
142+reconstructConfigKey :: (IsString t, Monoid t) => NonEmpty t -> t
143+reconstructConfigKey = fold . NE.intersperse "."
144+{-# INLINE reconstructConfigKey #-}
145+
146+pattern NE :: a -> NonEmpty a
147+pattern NE x = x NE.:| []
148+pattern NECons :: a -> NonEmpty a -> NonEmpty a
149+pattern NECons x y <- (NE.uncons -> (x, Just y))
150+{-# COMPLETE NE, NECons #-}
151+
152+catMaybes :: Ord k => ConfigTree k (Maybe v) -> ConfigTree k v
153+catMaybes = fmap fromJust . filter isJust
154+{-# INLINE catMaybes #-}
155+
156+insert :: Ord k => NonEmpty k -> v -> ConfigTree k v -> ConfigTree k v
157+insert (NE k) v (ConfigTree m) = ConfigTree $ (k, (Just v, subCfg)) OM.<| m
158+ where
159+ subCfg = case OM.lookup k m of
160+ Nothing -> mempty
161+ Just (_, m') -> m'
162+insert (NECons k ks) v (ConfigTree m) = ConfigTree $ case OM.lookup k m of
163+ Nothing -> (k, (Nothing, singleton ks v)) OM.<| m
164+ Just (v', m') -> (k, (v', insert ks v m')) OM.<| m
165+{-# INLINE insert #-}
octopod-common/src/Data/Map/Ordered/Strict/Extra.hsdeleted−15
@@ -1,15 +0,0 @@
1−{-# OPTIONS_GHC -Wno-orphans #-}
2−
3−module Data.Map.Ordered.Strict.Extra
4− (
5− )
6−where
7−
8−import Data.Aeson
9−import Data.Map.Ordered.Strict
10−
11−instance (ToJSON k, ToJSON v) => ToJSON (OMap k v) where
12− toJSON = toJSON . assocs
13−
14−instance (FromJSON k, FromJSON v, Ord k) => FromJSON (OMap k v) where
15− parseJSON = fmap fromList . parseJSON
octopod-css/development/layouts/staging.html+6
@@ -126,6 +126,12 @@
126126 <div class="row">
127127 <span class="key-default-pristine">SECRET_CODE: </span><span class="value-edited">asdfaisdjri235868ear7%^&IUAK$^%&asdfasdfij</span>
128128 </div>
129+ <div class="row">
130+ <span class="key-deleted">sso.vaultEnv.hotels-sso.mapping[0].APP_URL </span><span class="value-deleted">http://my-api.my-domain.com</span>
131+ </div>
132+ <div class="row">
133+ <span class="key-deleted">air.vaultEnv.air-sso:</span> <span class="value-unknown">unknown</span>
134+ </div>
129135 <div class="collapse collapse--project">
130136 <button class="collapse__head" type="button">
131137 air
octopod-css/development/markups/_keys-and-values.html+100−1
@@ -66,6 +66,28 @@
6666 <span class="value-edited">value-edited</span>
6767 </td>
6868 </tr>
69+ <tr>
70+ <td>
71+ Ключ и значение удалены:
72+ </td>
73+ <td>
74+ <span class="key-deleted">key-deleted</span>
75+ </td>
76+ <td>
77+ <span class="value-deleted">value-deleted</span>
78+ </td>
79+ </tr>
80+ <tr>
81+ <td>
82+ Ключ удалён, значение неизвестно:
83+ </td>
84+ <td>
85+ <span class="key-deleted">key-deleted</span>
86+ </td>
87+ <td>
88+ <span class="value-unknown">unknown</span> (value-unknown)
89+ </td>
90+ </tr>
6991 </tbody>
7092 </table>
7193 </div>
@@ -111,6 +133,28 @@
111133 <span class="value-edited">http://my-api.my-domain.com</span>
112134 </td>
113135 </tr>
136+ <tr>
137+ <td>
138+ Ключ и значение удалены:
139+ </td>
140+ <td>
141+ <span class="key-deleted">sso.vaultEnv.hotels-sso.mapping[0].APP_URL</span>
142+ </td>
143+ <td>
144+ <span class="value-deleted">http://my-api.my-domain.com</span>
145+ </td>
146+ </tr>
147+ <tr>
148+ <td>
149+ Ключ удалён, значение неизвестно:
150+ </td>
151+ <td>
152+ <span class="key-deleted">air.vaultEnv.air-sso:</span>
153+ </td>
154+ <td>
155+ <span class="value-unknown">unknown</span>
156+ </td>
157+ </tr>
114158 </tbody>
115159 </table>
116160 </div>
@@ -119,7 +163,8 @@
119163 <br>
120164 <p>
121165 Для экономии узлов в DOM-дереве можно не создавать отдельную обёртку &lt;span&gt;...&lt;/span&gt;
122− а вешать эти классы прям на ячейки таблицы, или на любого другого родителя:
166+ а вешать эти классы прям на ячейки таблицы, или на любого другого родителя.
167+ Работает со всеми классами кроме <b>.value-unknown</b> -- это отдельный компонент.
123168 </p>
124169
125170 <div class="table">
@@ -158,6 +203,28 @@
158203 http://my-api.my-domain.com
159204 </td>
160205 </tr>
206+ <tr>
207+ <td>
208+ Ключ и значение удалены:
209+ </td>
210+ <td class="key-deleted">
211+ sso.vaultEnv.hotels-sso.mapping[0].APP_URL
212+ </td>
213+ <td class="value-deleted">
214+ http://my-api.my-domain.com
215+ </td>
216+ </tr>
217+ <tr>
218+ <td>
219+ Ключ удалён, значение неизвестно:
220+ </td>
221+ <td class="key-deleted">
222+ air.vaultEnv.air-sso:
223+ </td>
224+ <td>
225+ <span class="value-unknown">unknown</span>
226+ </td>
227+ </tr>
161228 </tbody>
162229 </table>
163230 </div>
@@ -199,6 +266,22 @@
199266 <span class="key-custom-edited">app_api_url:</span> <span class="value-edited">http://my-api.my-domain.com</span>
200267 </td>
201268 </tr>
269+ <tr>
270+ <td>
271+ Ключ и значение удалены:
272+ </td>
273+ <td>
274+ <span class="key-deleted">sso.vaultEnv.hotels-sso.mapping[0].APP_URL</span> <span class="value-deleted">http://my-api.my-domain.com</span>
275+ </td>
276+ </tr>
277+ <tr>
278+ <td>
279+ Ключ удалён, значение неизвестно:
280+ </td>
281+ <td>
282+ <span class="key-deleted">air.vaultEnv.air-sso:</span> <span class="value-unknown">unknown</span>
283+ </td>
284+ </tr>
202285 </tbody>
203286 </table>
204287 </div>
@@ -236,6 +319,22 @@
236319 <span class="key-custom-edited">app_api_url: </span><span class="value-edited">http://my-api.my-domain.com</span>
237320 </td>
238321 </tr>
322+ <tr>
323+ <td>
324+ Ключ и значение удалены:
325+ </td>
326+ <td>
327+ <span class="key-deleted">sso.vaultEnv.hotels-sso.mapping[0].APP_URL </span><span class="value-deleted">http://my-api.my-domain.com</span>
328+ </td>
329+ </tr>
330+ <tr>
331+ <td>
332+ Ключ удалён, значение неизвестно:
333+ </td>
334+ <td>
335+ <span class="key-deleted">air.vaultEnv.air-sso: </span><span class="value-unknown">unknown</span>
336+ </td>
337+ </tr>
239338 </tbody>
240339 </table>
241340 </div>
octopod-css/development/styles/_collapse.css+6−4
@@ -9,6 +9,12 @@
99 font-family: inherit;
1010 text-align: left;
1111 cursor: pointer;
12+ -webkit-touch-callout: none;
13+ -webkit-user-select: none;
14+ -khtml-user-select: none;
15+ -moz-user-select: none;
16+ -ms-user-select: none;
17+ user-select: none;
1218 }
1319
1420 .collapse__body {
@@ -87,7 +93,3 @@
8793 padding-left: 32px;
8894 border-left: 1px solid #C4C4C4;
8995 }
90−
91−
92−
93−
octopod-css/development/styles/_key-deleted.cssnew file+6
@@ -0,0 +1,6 @@
1+.key-deleted {
2+ font-weight: 300 !important;
3+ font-style: italic !important;
4+ text-decoration: line-through;
5+ color: #D3D3D3;
6+}
octopod-css/development/styles/_table.css+1−1
@@ -61,7 +61,7 @@
6161
6262}
6363 .table--clickable tbody tr {
64− cursor: default;
64+ cursor: pointer;
6565 }
6666
6767 .table--clickable tbody tr:hover {
octopod-css/development/styles/_value-deleted.cssnew file+5
@@ -0,0 +1,5 @@
1+.value-deleted {
2+ font-weight: 300 !important;
3+ text-decoration: line-through;
4+ color: #D3D3D3;
5+}
octopod-css/development/styles/_value-unknown.cssnew file+12
@@ -0,0 +1,12 @@
1+.value-unknown {
2+ display: inline-block;
3+ vertical-align: baseline;
4+ width: 92px;
5+ height: 0.5715em;
6+ background-color: #F3F3F3;
7+ border-radius: 2px;
8+ margin-right: 4px;
9+ text-indent: 200%;
10+ white-space: nowrap;
11+ overflow: hidden;
12+}
octopod-css/development/styles/style.css+5
@@ -43,6 +43,10 @@
4343@import url("_key-default-pristine.css");
4444@import url("_key-default-edited.css");
4545@import url("_key-custom-edited.css");
46+@import url("_key-deleted.css");
47+@import url("_value-pristine.css");
48+@import url("_value-edited.css");
49+@import url("_value-deleted.css");
4650@import url("_value-pristine.css");
4751@import url("_value-edited.css");
4852@import url("_collapse.css");
@@ -50,3 +54,4 @@
5054@import url("_padded.css");
5155@import url("_has-changes.css");
5256@import url("_editable-row.css");
57+@import url("_value-unknown.css");
octopod-frontend/octopod-frontend.cabal+4
@@ -52,6 +52,7 @@ executable frontend
5252 , Frontend.UIKit.Button.Static
5353 , Frontend.UIKit.Button.Action
5454 , Frontend.UIKit.Button.Sort
55+ , Frontend.UIKit.Button.Tree
5556 , Reflex.Dom.Renderable
5657 , Reflex.Dom.AsyncEvent
5758 ghc-options:
@@ -103,6 +104,7 @@ executable frontend
103104 , ApplicativeDo
104105 , PartialTypeSignatures
105106 , DeriveAnyClass
107+ , MultiWayIf
106108 build-depends: aeson
107109 , base
108110 , bytestring
@@ -135,5 +137,7 @@ executable frontend
135137 , deepseq
136138 , patch
137139 , reflex-dom-core
140+ , these
141+ , ref-tf
138142 hs-source-dirs: src
139143 default-language: Haskell2010
octopod-frontend/src/Data/Text/Search.hs+14
@@ -21,6 +21,7 @@ import Data.Maybe (listToMaybe)
2121import Data.Ord
2222import Data.Semigroup
2323import Data.Sequence (Seq (..))
24+import Data.String
2425import Data.Text (Text)
2526import qualified Data.Text as T
2627import GHC.Generics (Generic)
@@ -92,6 +93,19 @@ data SearchResult = SearchResult
9293 deriving stock (Eq, Ord, Generic)
9394 deriving anyclass (NFData)
9495
96+instance IsString SearchResult where
97+ fromString s = SearchResult (T.pack s) Nothing
98+
99+instance Semigroup SearchResult where
100+ SearchResult a aRes <> SearchResult b bRes = SearchResult (a <> b) $ case (aRes, bRes) of
101+ (Nothing, Nothing) -> Nothing
102+ (Just a', Nothing) -> Just $ a' :|> NotMatched b
103+ (Nothing, Just b') -> Just $ NotMatched a :<| b'
104+ (Just a', Just b') -> Just $ a' <> b'
105+
106+instance Monoid SearchResult where
107+ mempty = SearchResult mempty Nothing
108+
95109instance Searchable SearchResult SearchResult where
96110 type Searched SearchResult res = res
97111 searchWith f t = f t
octopod-frontend/src/Data/WorkingOverrides.hs+104−76
@@ -1,32 +1,104 @@
11module Data.WorkingOverrides
22 ( WorkingOverrides,
3− WorkingOverride,
4− WorkingOverride',
53 WorkingOverrideKey' (..),
4+ getConfigValueText,
65 WorkingOverrideKey,
76 WorkingOverrideKeyType (..),
8− WorkingOverrideValue' (..),
9− WorkingOverrideValue,
107 destructWorkingOverrides,
118 constructWorkingOverrides,
12− newWorkingOverride,
9+ ConfigValue (..),
10+ CustomKey (..),
11+ CustomConfigValue (..),
12+ WorkingConfigTree,
13+ WorkingOverride,
14+ WorkingOverride',
15+ configTreeHasLeaf,
16+ destructWorkingOverridesDyn,
1317 )
1418where
1519
20+import Common.Orphans ()
1621import Common.Types
17−import qualified Data.List as L
18−import qualified Data.Map.Ordered.Strict as OM
19−import Data.Maybe
22+import Control.Monad.Reader
23+import Control.Monad.Ref
24+import Data.ConfigTree (ConfigTree)
25+import qualified Data.ConfigTree as CT
26+import Data.Foldable
27+import Data.Functor
28+import Data.Map (Map)
29+import qualified Data.Map as M
30+import Data.Maybe (catMaybes)
31+import qualified Data.Maybe as M
2032import Data.Text (Text)
21−import Data.UniqMap
33+import Data.These
34+import GHC.Generics (Generic)
35+import Reflex
36+
37+type WorkingConfigTree k v = ConfigTree k (ConfigValue v)
38+
39+data ConfigValue te
40+ = DefaultConfigValue !te
41+ | CustomConfigValue !(Either (CustomKey te) (CustomConfigValue te))
42+ deriving stock (Show, Generic, Eq, Ord)
43+
44+newtype CustomKey v = CustomKey v
45+ deriving stock (Show, Generic, Eq, Ord)
46+
47+data CustomConfigValue te
48+ = CustomValue !te
49+ | DeletedValue !(Maybe te)
50+ deriving stock (Show, Generic, Eq, Ord)
2251
52+getConfigValueText :: ConfigValue te -> Maybe te
53+getConfigValueText (DefaultConfigValue t) = Just t
54+getConfigValueText (CustomConfigValue (Left (CustomKey t))) = Just t
55+getConfigValueText (CustomConfigValue (Right (CustomValue t))) = Just t
56+getConfigValueText (CustomConfigValue (Right (DeletedValue t))) = t
57+
58+-- Super inefficient but i dont care
59+configTreeHasLeaf ::
60+ forall m kv te.
61+ ( MonadReader (Ref m (Map (Text, ConfigTree kv te) Bool)) m
62+ , MonadRef m
63+ , Ord kv
64+ , Ord te
65+ ) =>
66+ -- | The id of the predicate used for caching
67+ Text ->
68+ (te -> Bool) ->
69+ ConfigTree kv te ->
70+ m Bool
71+configTreeHasLeaf name f = memo (\(_, CT.ConfigTree x) -> go $ toList x) . (name,)
72+ where
73+ go :: [(Maybe te, ConfigTree kv te)] -> m Bool
74+ go [] = pure False
75+ go ((Just y, _) : _) | f y = pure True
76+ go ((_, ct) : rest) =
77+ configTreeHasLeaf name f ct >>= \case
78+ True -> pure True
79+ False -> go rest
80+
81+memo ::
82+ (MonadReader (Ref m (Map x y)) m, Ord x, MonadRef m) =>
83+ (x -> m y) ->
84+ x ->
85+ m y
86+memo f x = do
87+ memoRef <- ask
88+ m <- readRef memoRef
89+ case M.lookup x m of
90+ Just y -> pure y
91+ Nothing -> do
92+ y <- f x
93+ modifyRef memoRef $ M.insert x y
94+ pure y
2395type WorkingOverrides = WorkingOverrides' Text
2496
25−type WorkingOverrides' te = UniqKeyMap (WorkingOverride' te)
97+type WorkingOverrides' te = ConfigTree te (ConfigValue te)
2698
2799type WorkingOverride = WorkingOverride' Text
28100
29−type WorkingOverride' te = (WorkingOverrideKey' te, WorkingOverrideValue' te)
101+type WorkingOverride' te = (te, ConfigValue te)
30102
31103type WorkingOverrideKey = WorkingOverrideKey' Text
32104
@@ -36,75 +108,31 @@ data WorkingOverrideKey' te = WorkingOverrideKey !WorkingOverrideKeyType !te
36108data WorkingOverrideKeyType = CustomWorkingOverrideKey | DefaultWorkingOverrideKey
37109 deriving stock (Show, Eq)
38110
39−type WorkingOverrideValue = WorkingOverrideValue' Text
111+destructWorkingOverridesDyn :: Reflex t => [Dynamic t WorkingOverride] -> Dynamic t (Overrides l)
112+destructWorkingOverridesDyn =
113+ fmap (Overrides . CT.fromFlatList . catMaybes) . sequenceA . (fmap . fmap) (\(k, v) -> (k,) <$> unConfigValue v)
40114
41−data WorkingOverrideValue' te
42− = WorkingCustomValue !te
43− | WorkingDefaultValue !te
44− | WorkingDeletedValue !(Maybe te)
45− deriving stock (Show)
46−
47−destructWorkingOverrides :: WorkingOverrides -> Overrides l
115+destructWorkingOverrides :: [WorkingOverride] -> Overrides l
48116destructWorkingOverrides =
49− Overrides
50− . foldr
51− ( \pair@(k, v) m -> case OM.lookup k m of
52− Just _ | v == ValueDeleted -> m
53− _ -> m OM.|> pair
54− )
55− OM.empty
56− . mapMaybe
57− ( \case
58− (WorkingOverrideKey CustomWorkingOverrideKey k, getWorkingOverrideValue -> v) -> Just (k, v)
59− (WorkingOverrideKey _ k, WorkingCustomValue v) -> Just (k, ValueAdded v)
60− (WorkingOverrideKey _ k, WorkingDeletedValue _) -> Just (k, ValueDeleted)
61− (WorkingOverrideKey DefaultWorkingOverrideKey _, WorkingDefaultValue _) -> Nothing
62− )
63− . elemsUniq
64− where
65− getWorkingOverrideValue :: WorkingOverrideValue -> OverrideValue
66− getWorkingOverrideValue (WorkingCustomValue x) = ValueAdded x
67− getWorkingOverrideValue (WorkingDefaultValue x) = ValueAdded x
68− getWorkingOverrideValue (WorkingDeletedValue _) = ValueDeleted
117+ Overrides . CT.fromFlatList . M.catMaybes . fmap (\(k, v) -> (k,) <$> unConfigValue v)
118+
119+unConfigValue :: ConfigValue te -> Maybe (OverrideValue' te)
120+unConfigValue (DefaultConfigValue _) = Nothing
121+unConfigValue (CustomConfigValue (Left (CustomKey te))) = Just $ ValueAdded te
122+unConfigValue (CustomConfigValue (Right (CustomValue te))) = Just $ ValueAdded te
123+unConfigValue (CustomConfigValue (Right (DeletedValue _))) = Just ValueDeleted
69124
70125constructWorkingOverrides ::
71126 Ord te =>
72− Maybe (DefaultConfig' te l) ->
127+ DefaultConfig' te l ->
73128 Overrides' te l ->
74129 WorkingOverrides' te
75−constructWorkingOverrides (Just (DefaultConfig dCfg)) (Overrides ovsM) =
76− let custom =
77− uniqMapFromList
78− . mapMaybe
79− ( \(k, v) ->
80− let k' = WorkingOverrideKey (if OM.member k dCfg then DefaultWorkingOverrideKey else CustomWorkingOverrideKey) k
81− in case v of
82− ValueAdded x -> Just (k', WorkingCustomValue x)
83− ValueDeleted -> (k',) . WorkingDeletedValue . Just <$> OM.lookup k dCfg
84− )
85− . OM.assocs
86− $ ovsM
87− in L.foldl'
88− ( \m (k, v) ->
89− if OM.member k ovsM
90− then m
91− else
92− fst $
93− insertUniqEnd (WorkingOverrideKey DefaultWorkingOverrideKey k, WorkingDefaultValue v) m
94− )
95− custom
96− (OM.assocs dCfg)
97−constructWorkingOverrides Nothing (Overrides ovsM) =
98− uniqMapFromList
99− . fmap
100− ( \(k, v) ->
101− let k' = WorkingOverrideKey CustomWorkingOverrideKey k
102− in case v of
103− ValueAdded x -> (k', WorkingCustomValue x)
104− ValueDeleted -> (k', WorkingDeletedValue Nothing)
105− )
106− . OM.assocs
107− $ ovsM
108−
109−newWorkingOverride :: WorkingOverride
110−newWorkingOverride = (WorkingOverrideKey CustomWorkingOverrideKey "", WorkingCustomValue "")
130+constructWorkingOverrides (DefaultConfig defCT) (Overrides newCT) =
131+ CT.catMaybes $
132+ CT.zip newCT defCT <&> \case
133+ That x -> Just $ DefaultConfigValue x
134+ This x -> case x of
135+ ValueDeleted -> Nothing
136+ ValueAdded v -> Just $ CustomConfigValue $ Left $ CustomKey v
137+ These ValueDeleted v -> Just $ CustomConfigValue $ Right $ DeletedValue $ Just v
138+ These (ValueAdded v) _ -> Just $ CustomConfigValue $ Right $ CustomValue v
octopod-frontend/src/Frontend/UIKit.hs+281−115
@@ -7,53 +7,63 @@ module Frontend.UIKit
77 octopodTextInputDyn,
88 loadingOverride,
99 loadingOverrides,
10− overrideField,
11− OverrideField (..),
10+ configField,
1211 popupOverlay,
1312 Default (..),
1413 module X,
1514 (.~~),
1615 (?~~),
17− OverrideFieldType (..),
1816 deletedOverride,
19− showNonEditableWorkingOverride,
20− NonEditableWorkingOverrideStyle (..),
2117 untilReadyEv',
2218 untilReady',
2319 runWithReplace',
2420 joinEvM,
21+ showNonEditableWorkingOverrideTree,
22+ showFlatConfig,
23+ ConfigFieldButtonAction (..),
24+ showOverrideTree,
25+ nonEditableLoading,
2526 )
2627where
2728
29+import Common.Types
2830import Control.Lens
29− ( ASetter',
30− (%~),
31− (<>~),
32− (?~),
33− (^.),
34− )
31+import Control.Lens.Extras (is)
3532import Control.Monad
33+import Control.Monad.Reader
34+import Control.Monad.Ref
3635import Data.Align
36+import Data.ConfigTree (ConfigTree (ConfigTree))
37+import qualified Data.ConfigTree as CT
3738import Data.Default
39+import Data.Either
40+import Data.Foldable
3841import Data.Functor
3942import Data.Generics.Labels ()
43+import Data.Generics.Sum
4044import Data.List.NonEmpty
4145import qualified Data.List.NonEmpty as NE
46+import Data.Map (Map)
4247import qualified Data.Map as M
43−import Data.Maybe (isNothing, maybeToList)
48+import qualified Data.Map.Ordered.Strict as OM
49+import Data.Maybe (fromMaybe, isNothing, maybeToList)
50+import Data.Monoid (Endo (..))
4451import Data.Text (Text)
4552import qualified Data.Text as T
4653import Data.Text.Search
4754import Data.These
55+import Data.Traversable
4856import Data.Witherable
4957import Data.WorkingOverrides
5058import Frontend.Classes as X
5159import Frontend.UIKit.Button.Action as X
60+import Frontend.UIKit.Button.Common
5261import Frontend.UIKit.Button.Dash as X
5362import Frontend.UIKit.Button.Expander as X
5463import Frontend.UIKit.Button.Large as X
5564import Frontend.UIKit.Button.Sort as X
5665import Frontend.UIKit.Button.Static as X
66+import Frontend.UIKit.Button.Tree
5767import GHC.Generics (Generic)
5868import Reflex.Dom
5969import Reflex.Dom.AsyncEvent
@@ -85,64 +95,84 @@ errorCommonWidget =
8595 elClass "b" "null__heading" $ text "Cannot retrieve the data"
8696 divClass "null__message" $ text "Try to reload the page"
8797
88−data OverrideField t = OverrideField
98+data ConfigField t = ConfigField
8999 { fieldValue :: Dynamic t Text
90100 , fieldError :: Dynamic t (Maybe (NonEmpty Text))
91− , fieldDisabled :: Dynamic t Bool
92− , fieldType :: Dynamic t OverrideFieldType
101+ , fieldType :: Dynamic t ConfigFieldType
93102 }
94103 deriving stock (Generic)
95104
96−data OverrideFieldType
97− = DefaultOverrideFieldType
98− | EditedOverrideFieldType
105+data ConfigFieldType
106+ = DefaultConfigFieldType
107+ | CustomConfigFieldType
99108 deriving stock (Show)
100109
101−overrideFieldTypeClasses :: OverrideFieldType -> Classes
102−overrideFieldTypeClasses DefaultOverrideFieldType = "input--default"
103−overrideFieldTypeClasses EditedOverrideFieldType = mempty
104−
105−overrideField :: MonadWidget t m => Dynamic t [Text] -> OverrideField t -> OverrideField t -> m (Dynamic t Text, Dynamic t Text, Event t ())
106−overrideField overrideKeyValues keyDyn valueDyn = do
107− elDiv "overrides__item" $ do
108− keyInp <-
110+configField ::
111+ MonadWidget t m =>
112+ Dynamic t [Text] ->
113+ Dynamic t WorkingOverride ->
114+ m (Dynamic t Text, Dynamic t Text, Event t ConfigFieldButtonAction)
115+configField overrideKeyValues (splitDynPure -> (k, v')) = do
116+ let (splitDynPure -> (keyClass, valueClass)) =
117+ v' <&> \case
118+ DefaultConfigValue _ -> ("key-default-pristine", "value-pristine")
119+ CustomConfigValue (Right (CustomValue _)) -> ("key-default-edited", "value-edited")
120+ CustomConfigValue (Right (DeletedValue Just {})) -> ("key-deleted", "value-deleted")
121+ CustomConfigValue (Right (DeletedValue Nothing)) -> ("key-deleted", "value-unknown")
122+ CustomConfigValue (Left _) -> ("key-custom-edited", "value-edited")
123+ valDyn =
124+ v' <&> \case
125+ DefaultConfigValue v -> v
126+ CustomConfigValue (Right (CustomValue v)) -> v
127+ CustomConfigValue (Right (DeletedValue v)) -> fromMaybe "UNKNOWN" v
128+ CustomConfigValue (Left (CustomKey v)) -> v
129+ deletedDyn' = (is (_Ctor' @"CustomConfigValue" . _Right . _Ctor' @"DeletedValue")) <$> v'
130+ deletedDyn <- holdUniqDyn deletedDyn'
131+ let extraDivClasses =
132+ deletedDyn <&> \case
133+ True -> "input--deleted"
134+ False -> mempty
135+ elDiv "editable-row" $ mdo
136+ (value -> keyTextDyn) <-
109137 octopodTextInputDyn
110138 overrideKeyValues
111− (keyDyn ^. #fieldDisabled)
112− ( do
113− t <- keyDyn ^. #fieldType
114− pure $ "overrides__key" <> overrideFieldTypeClasses t
115− )
139+ deletedDyn
140+ (mappend "editable-row__key" <$> extraDivClasses)
141+ keyClass
116142 "key"
117− (keyDyn ^. #fieldValue)
118− (keyDyn ^. #fieldError)
119− let keyTextDyn = value keyInp
143+ k
144+ (pure Nothing)
120145 (value -> valTextDyn) <-
121146 octopodTextInputDyn
122147 (pure [])
123− (valueDyn ^. #fieldDisabled)
124− ( do
125− t <- valueDyn ^. #fieldType
126− pure $ "overrides__value" <> overrideFieldTypeClasses t
127− )
148+ deletedDyn
149+ (mappend "editable-row__value" <$> extraDivClasses)
150+ valueClass
128151 "value"
129− (valueDyn ^. #fieldValue)
130− (valueDyn ^. #fieldError)
131− closeEv <- deleteOverrideButton
152+ valDyn
153+ (valTextDyn <&> \t -> if (T.null . T.strip) t then Just (pure "Value can not be empty") else Nothing)
154+ closeEv <-
155+ networkView >=> switchHold never $
156+ deletedDyn <&> \case
157+ True -> fmap (const ConfigFieldButtonRecover) <$> undoOverrideButton
158+ False -> fmap (const ConfigFieldButtonClear) <$> deleteOverrideButton
132159 pure (keyTextDyn, valTextDyn, closeEv)
133160
161+data ConfigFieldButtonAction = ConfigFieldButtonClear | ConfigFieldButtonRecover
162+ deriving stock (Show)
163+
134164deletedOverride :: MonadWidget t m => Text -> Maybe Text -> m (Event t ())
135165deletedOverride k vM = do
136− elDiv "overrides__item" $ do
137− elDiv "overrides__key input input--deleted" $ field k
166+ elDiv "editable-row" $ do
167+ elDiv "editable-row__key input input--deleted" $ field k
138168 case vM of
139169 Nothing -> do
140− elDiv "overrides__placeholder overrides__value" blank
170+ elDiv "editable-row__placeholder editable-row__value" blank
141171 loadingOverrideSpinner
142172 pure never
143173 Just v -> do
144− elDiv "overrides__value input input--deleted" $ field $ v
145− undoOverrideButton
174+ elDiv "editable-row__value input input--deleted" $ field $ v
175+ (($> ()) <$> undoOverrideButton)
146176 where
147177 field t =
148178 void $
@@ -152,20 +182,20 @@ deletedOverride k vM = do
152182 & initialAttributes .~ ("type" =: "text" <> "class" =: "input__widget")
153183
154184loadingOverrideField :: MonadWidget t m => m ()
155−loadingOverrideField = elDiv "overrides__placeholder" blank
185+loadingOverrideField = elDiv "editable-row__placeholder" blank
156186
157187loadingOverrideSpinner :: MonadWidget t m => m ()
158188loadingOverrideSpinner = elDiv "overrides__delete spot spot--loader" blank
159189
160190loadingOverride :: MonadWidget t m => m ()
161191loadingOverride = do
162− elDiv "overrides__item loader" $ do
192+ elDiv "editable-row loader" $ do
163193 loadingOverrideField
164194 loadingOverrideField
165195 loadingOverrideSpinner
166196
167197loadingOverrides :: MonadWidget t m => m ()
168−loadingOverrides = do
198+loadingOverrides = divClass "padded" $ do
169199 loadingOverride
170200 loadingOverride
171201 loadingOverride
@@ -181,6 +211,8 @@ octopodTextInput' ::
181211 (Dynamic t [Text]) ->
182212 -- | Disabled?
183213 Dynamic t Bool ->
214+ -- | Div classes.
215+ Dynamic t Classes ->
184216 -- | Input field classes.
185217 Dynamic t Classes ->
186218 -- | Placeholder for input field.
@@ -190,14 +222,14 @@ octopodTextInput' ::
190222 -- | Event carrying the error message.
191223 Event t Text ->
192224 m (InputElement EventResult GhcjsDomSpace t, Dynamic t Bool)
193−octopodTextInput' valuesDyn disabledDyn clssDyn placeholder inValDyn' errEv = mdo
225+octopodTextInput' valuesDyn disabledDyn inpClassesDyn clssDyn placeholder inValDyn' errEv = mdo
194226 errDyn <-
195227 holdDyn Nothing $
196228 leftmost
197229 [ Just . pure <$> errEv
198230 , Nothing <$ updated (value inp)
199231 ]
200− inp <- octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn
232+ inp <- octopodTextInputDyn valuesDyn disabledDyn inpClassesDyn clssDyn placeholder inValDyn' errDyn
201233 pure (inp, isNothing <$> errDyn)
202234
203235-- | The only text input field that is used in project forms. This input
@@ -208,6 +240,8 @@ octopodTextInputDyn ::
208240 Dynamic t [Text] ->
209241 -- | Disabled?
210242 Dynamic t Bool ->
243+ -- | Div classes.
244+ Dynamic t Classes ->
211245 -- | Input field classes.
212246 Dynamic t Classes ->
213247 -- | Placeholder for input field.
@@ -217,7 +251,7 @@ octopodTextInputDyn ::
217251 -- | Event carrying the error message.
218252 Dynamic t (Maybe (NonEmpty Text)) ->
219253 m (InputElement EventResult GhcjsDomSpace t)
220−octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn = mdo
254+octopodTextInputDyn valuesDyn disabledDyn divClasses inpClassesDyn placeholder inValDyn' errDyn = mdo
221255 inValDyn <- holdUniqDyn inValDyn'
222256 let valDyn = value inp
223257 inValEv =
@@ -237,18 +271,22 @@ octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn =
237271
238272 let classDyn = do
239273 errClasses <- errorClassesDyn
240− additionalClasses <- clssDyn
274+ additionalClasses <- divClasses
241275 pure . destructClasses $ "input" <> errClasses <> additionalClasses
242276
243277 inVal <- sample . current $ inValDyn
244278 disabled <- sample . current $ disabledDyn
279+ let actualInputClassesDyn = do
280+ inpClass <- inpClassesDyn
281+ pure $ destructClasses $ "input__widget" <> inpClass
282+ clss <- sample . current $ actualInputClassesDyn
245283 (inp, selectedValue) <- elDynClass "div" classDyn $ do
246284 inp' <-
247285 inputElement $
248286 def
249287 & initialAttributes
250288 .~ ( "type" =: "text"
251− <> "class" =: "input__widget"
289+ <> "class" =: clss
252290 <> "placeholder" =: placeholder
253291 <> "spellcheck" =: "false"
254292 )
@@ -260,9 +298,11 @@ octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn =
260298 <>~ updated
261299 ( do
262300 disabled' <- disabledDyn
301+ classes <- actualInputClassesDyn
263302 pure $
264− M.singleton "disabled" $
265− if disabled' then Just "disabled" else Nothing
303+ ( "disabled" =: (if disabled' then Just "disabled" else Nothing)
304+ <> "class" =: Just classes
305+ )
266306 )
267307 void $
268308 simpleList ((maybeToList >=> NE.toList) <$> errDyn) $ \err ->
@@ -288,14 +328,14 @@ octopodTextInputDyn valuesDyn disabledDyn clssDyn placeholder inValDyn' errDyn =
288328 searchResultEv <&> \case
289329 [] -> pure never
290330 searchResult -> do
291− elClass "ul" "overrides__search" $ do
331+ elClass "ul" "input__dropdown" $ do
292332 fmap leftmost $
293− forM searchResult $ \(res, initialText) -> do
294− (resEl, ()) <- elClass' "li" "overrides__search-item" $
295− forM_ res $ \case
333+ for searchResult $ \(res, initialText) -> do
334+ (resEl, ()) <- elClass' "li" "input__suggest" $
335+ for_ res $ \case
296336 Matched t -> elAttr "span" ("style" =: "font-weight: bold;") $ text t
297337 NotMatched t -> text t
298− -- Because 'Click' would fire after the text field loses
338+ -- Because 'Click' would fire after the text field looses
299339 -- focus and the popup disappears.
300340 pure $ domEvent Mousedown resEl $> initialText
301341
@@ -307,60 +347,186 @@ popupOverlay :: DomBuilder t m => m ()
307347popupOverlay =
308348 elAttr "div" ("class" =: "popup__overlay" <> "aria-hidden" =: "true") blank
309349
310−data NonEditableWorkingOverrideStyle
311− = RegularNonEditableWorkingOverrideStyle
312− | LargeNonEditableWorkingOverrideStyle
313−
314−nonEditableWorkingOverrideStyleClasses :: NonEditableWorkingOverrideStyle -> Classes
315−nonEditableWorkingOverrideStyleClasses RegularNonEditableWorkingOverrideStyle = mempty
316−nonEditableWorkingOverrideStyleClasses LargeNonEditableWorkingOverrideStyle = "listing--larger"
317−
318−-- | Widget that shows overrides list. It does not depend on their type.
319−showNonEditableWorkingOverride ::
320− (MonadWidget t m, Renderable te) =>
321− -- | Loading?
322− Bool ->
323− -- | Is it fully loaded?
324− Bool ->
325− NonEditableWorkingOverrideStyle ->
326− -- | Overrides list.
327− [WorkingOverride' te] ->
350+showFlatConfig ::
351+ (MonadWidget t m, Renderable tk, Renderable tv) =>
352+ [(tk, OverrideValue' tv)] ->
353+ m ()
354+showFlatConfig l =
355+ for_ l $ \(k, v) ->
356+ let keyClasses = if is (_Ctor' @"ValueDeleted") v then "key-deleted" else "key-default-pristine"
357+ in divClass "row" $ do
358+ elClass "span" (destructClasses keyClasses) $ do
359+ rndr k
360+ text ": "
361+ pure ()
362+ case v of
363+ ValueAdded t -> elClass "span" "value-edited" $ rndr t
364+ ValueDeleted -> elClass "div" "listing__placeholder listing__placeholder__value" $ pure ()
365+ pure ()
366+
367+showNonEditableWorkingOverrideTree ::
368+ (MonadWidget t m, Renderable tk, Renderable tv, Ord tk, Ord (ConfigValue tv)) =>
369+ Dynamic t (WorkingConfigTree tk tv) ->
370+ m ()
371+showNonEditableWorkingOverrideTree treeDyn = do
372+ memoRef <- newRef mempty
373+ flip runReaderT memoRef $
374+ untilReady (lift nonEditableLoading) $
375+ networkView $
376+ treeDyn <&> \tree ->
377+ untilReady (lift nonEditableLoading) $
378+ divClass "padded" $
379+ showWorkingOverrideTree'
380+ (const False)
381+ (is (_Ctor' @"CustomConfigValue"))
382+ (\_ _ -> ())
383+ (\_ k v -> renderRow k v $> pure ())
384+ tree
385+ (pure True)
386+ never
387+ pure ()
388+
389+nonEditableLoading :: DomBuilder t m => m ()
390+nonEditableLoading = for_ [1 :: Int .. 3] $ \_ ->
391+ divClass "padded" $
392+ divClass "row" $ do
393+ elClass "div" "listing__placeholder" $ pure ()
394+ elClass "div" "listing__spinner" $ pure ()
395+
396+showOverrideTree ::
397+ forall m t tv tk x.
398+ ( MonadWidget t m
399+ , Renderable tk
400+ , Show x
401+ , Ord tk
402+ , Ord tv
403+ ) =>
404+ -- | Value should be shown by default
405+ (tv -> Bool) ->
406+ -- | Value is modified
407+ (tv -> Bool) ->
408+ -- | Prepare the fixpoint data
409+ (tk -> tv -> x) ->
410+ -- | Render row
411+ (x -> tk -> tv -> m (Dynamic t x)) ->
412+ ConfigTree tk tv ->
413+ -- | Should we render anything
414+ m [Dynamic t x]
415+showOverrideTree isShown isModified prepareData renderValue ct = do
416+ memoRef <- newRef mempty
417+ flip runReaderT memoRef $
418+ showWorkingOverrideTree' isShown isModified prepareData ((fmap . fmap . fmap) lift renderValue) ct (pure True) never
419+
420+showWorkingOverrideTree' ::
421+ ( MonadWidget t m
422+ , Renderable tk
423+ , Show x
424+ , MonadReader (Ref m (Map (Text, ConfigTree tk tv) Bool)) m
425+ , Ord tv
426+ , Ord tk
427+ ) =>
428+ -- | Value should be shown by default
429+ (tv -> Bool) ->
430+ -- | Value is modified
431+ (tv -> Bool) ->
432+ -- | Prepare the fixpoint data
433+ (tk -> tv -> x) ->
434+ -- | Render row
435+ (x -> tk -> tv -> m (Dynamic t x)) ->
436+ ConfigTree tk tv ->
437+ -- | Parent is open
438+ Dynamic t Bool ->
439+ -- | Force open/close all subtrees
440+ Event t Bool ->
441+ m [Dynamic t x]
442+showWorkingOverrideTree' isShown isModified prepareData renderValue (ConfigTree m) shouldRenderDyn' forceSubtreesEv = do
443+ shouldRenderDyn <- holdUniqDyn shouldRenderDyn'
444+ (appEndo . fold -> f) <- for (OM.assocs m) $ \(k, (mv, _)) -> do
445+ xMaybe <- for mv $ \v -> mdo
446+ let initialData = prepareData k v
447+ x <-
448+ networkView $
449+ shouldRenderDyn <&> \render -> do
450+ lastX <- sample $ current x'
451+ if render
452+ then elClass "div" "row" $ renderValue lastX k v
453+ else pure (pure lastX)
454+ x' <- (join <$> (holdDyn (pure initialData) x))
455+ pure $ x'
456+ pure $ Endo $ maybe id (:) xMaybe
457+ fmap (f . concat) $
458+ for (OM.assocs m) $ \case
459+ (_, (_, subtree)) | CT.null subtree -> pure []
460+ (k, (_, subtree)) -> mdo
461+ let wrapperClass = do
462+ open <- openDyn
463+ pure $
464+ destructClasses $
465+ "collapse--project" <> "collapse"
466+ <> if open
467+ then "collapse--expanded"
468+ else mempty
469+ (ovs, openDyn) <- elDynClass "div" wrapperClass $ do
470+ modified <- configTreeHasLeaf "isModified" isModified subtree
471+ shown <- configTreeHasLeaf "isShown" isShown subtree
472+ (traceEvent "clickEv" -> clickEv) <-
473+ treeButton
474+ TreeButtonConfig
475+ { buttonText = TextBuilder $ rndr k
476+ , subtreeHasChanges = modified
477+ , visible = shouldRenderDyn
478+ , forceState = forceSubtreesEv
479+ }
480+ buttonIsOpen <- holdDyn shown $ leftmost [forceSubtreesEv, fst <$> clickEv]
481+ let selfForceSubtrees = fst <$> ffilter (isRight . snd) clickEv
482+ ovs' <-
483+ divClass "collapse__inner" $
484+ showWorkingOverrideTree'
485+ isShown
486+ isModified
487+ prepareData
488+ renderValue
489+ subtree
490+ ((&&) <$> buttonIsOpen <*> shouldRenderDyn)
491+ (leftmost [selfForceSubtrees, forceSubtreesEv])
492+ pure (ovs', buttonIsOpen)
493+ pure ovs
494+
495+renderRow ::
496+ (DomBuilder t m, Renderable te, Renderable k) =>
497+ k ->
498+ ConfigValue te ->
328499 m ()
329−showNonEditableWorkingOverride loading loaded style cfg =
330− divClass
331− ( destructClasses $
332− "listing" <> "listing--for-text" <> nonEditableWorkingOverrideStyleClasses style
333− )
334− $ do
335− case cfg of
336− [] ->
337− divClass "listing__item" $
338− elClass "span" "listing--info-text" $
339− text $
340− if loaded then "no configuration" else "no custom configuration"
341− _ -> forM_ cfg $ \(WorkingOverrideKey keyType key, val) -> do
342− let wrapper = case val of
343− WorkingDeletedValue _ -> divClass "listing__item deleted"
344− _ -> divClass "listing__item"
345− wrapper $ do
346− let keyWrapper = case keyType of
347− CustomWorkingOverrideKey -> elClass "span" "listing__key"
348− DefaultWorkingOverrideKey -> elClass "span" "listing__key default"
349− keyWrapper $ do
350− rndr key
351− text ": "
352−
353− case val of
354− WorkingCustomValue txt -> elClass "span" "listing__value" $ rndr txt
355− WorkingDefaultValue txt -> elClass "span" "listing__value default" $ rndr txt
356− WorkingDeletedValue (Just txt) -> elClass "span" "listing__value default" $ rndr txt
357− WorkingDeletedValue Nothing -> do
358− elClass "div" "listing__placeholder listing__placeholder__value" $ pure ()
359− when loading $ elClass "div" "listing__spinner" $ pure ()
360− when loading $
361− divClass "listing__item" $ do
362− elClass "div" "listing__placeholder" $ pure ()
363− elClass "div" "listing__spinner" $ pure ()
500+renderRow k v = do
501+ case v of
502+ DefaultConfigValue v' -> do
503+ elClass "span" "key-default-pristine" $ do
504+ rndr k
505+ text ": "
506+ pure ()
507+ elClass "span" "value-pristine" $ rndr v'
508+ pure ()
509+ CustomConfigValue (Left (CustomKey v')) -> do
510+ elClass "span" "key-custom-edited" $ do
511+ rndr k
512+ text ": "
513+ pure ()
514+ elClass "span" "value-edited" $ rndr v'
515+ pure ()
516+ CustomConfigValue (Right (CustomValue v')) -> do
517+ elClass "span" "key-default-edited" $ do
518+ rndr k
519+ text ": "
520+ pure ()
521+ elClass "span" "value-edited" $ rndr v'
522+ pure ()
523+ CustomConfigValue (Right (DeletedValue mv)) -> do
524+ elClass "span" "key-deleted" $ do
525+ rndr k
526+ text ": "
527+ pure ()
528+ elClass "span" "value-deleted" $ rndr `traverse_` mv
529+ pure ()
364530
365531untilReadyEv' ::
366532 (Adjustable t m, PostBuild t m, MonadHold t m) =>
octopod-frontend/src/Frontend/UIKit/Button/Action.hs+3−3
@@ -18,7 +18,7 @@ data ActionButtonConfig t = ActionButtonConfig
1818 { buttonText :: Text
1919 , buttonEnabled :: Dynamic t Bool
2020 , buttonType :: Maybe ActionButtonType
21− , buttonBaseTag :: BaseButtonTag
21+ , buttonBaseTag :: BaseButtonTag t
2222 }
2323 deriving stock (Generic)
2424
@@ -47,7 +47,7 @@ buttonTypeClasses = \case
4747actionButton ::
4848 (DomBuilder t m, PostBuild t m) =>
4949 ActionButtonConfig t ->
50− m (Event t ())
50+ m (Event t (Either () ()))
5151actionButton cfg =
5252 buttonEl
5353 CommonButtonConfig
@@ -58,6 +58,6 @@ actionButton cfg =
5858 , enabledClasses = mempty
5959 , disabledClasses = "action--disabled"
6060 , buttonEnabled = cfg ^. #buttonEnabled
61− , buttonText = pure $ cfg ^. #buttonText
61+ , buttonText = textBuilder $ cfg ^. #buttonText
6262 , buttonBaseTag = cfg ^. #buttonBaseTag
6363 }
octopod-frontend/src/Frontend/UIKit/Button/Common.hs+64−12
@@ -2,48 +2,100 @@ module Frontend.UIKit.Button.Common
22 ( CommonButtonConfig (..),
33 BaseButtonTag (..),
44 buttonEl,
5+ TextBuilder (..),
6+ textBuilder,
57 )
68where
79
8−import Control.Lens
10+import Control.Lens hiding (element)
911import Data.Generics.Labels ()
1012import Data.Map (Map)
13+import Data.Proxy
14+import Data.String
1115import Data.Text (Text)
16+import qualified Data.Text as T
1217import Frontend.Classes
1318import GHC.Generics (Generic)
1419import Reflex.Dom
1520
21+textBuilder :: Text -> TextBuilder t
22+textBuilder t = TextBuilder $ text t
23+
24+newtype TextBuilder t = TextBuilder
25+ { unTextBuilder ::
26+ forall m.
27+ ( DomBuilder t m
28+ , PostBuild t m
29+ ) =>
30+ m ()
31+ }
32+
33+instance IsString (TextBuilder t) where
34+ fromString t = TextBuilder (text . T.pack $ t)
35+
1636data CommonButtonConfig t = CommonButtonConfig
1737 { constantClasses :: Dynamic t Classes
1838 , enabledClasses :: Classes
1939 , disabledClasses :: Classes
2040 , buttonEnabled :: Dynamic t Bool
21− , buttonText :: Dynamic t Text
22− , buttonBaseTag :: BaseButtonTag
41+ , buttonText :: TextBuilder t
42+ , buttonBaseTag :: BaseButtonTag t
2343 }
2444 deriving stock (Generic)
2545
26−data BaseButtonTag = ButtonTag | ATag Text
46+data BaseButtonTag t = ButtonTag | ATag (Dynamic t Text)
2747
28−baseTag :: BaseButtonTag -> (Text, Map Text Text)
29−baseTag ButtonTag = ("button", "type" =: "button")
30−baseTag (ATag url) = ("a", "href" =: url <> "target" =: "_blank")
48+baseTag :: Reflex t => BaseButtonTag t -> (Text, Dynamic t (Map Text Text))
49+baseTag ButtonTag = ("button", pure $ "type" =: "button")
50+baseTag (ATag urlDyn) =
51+ ( "a"
52+ , urlDyn <&> \url -> "href" =: url <> "target" =: "_blank"
53+ )
3154
3255buttonEl ::
56+ forall m t.
3357 (DomBuilder t m, PostBuild t m) =>
3458 CommonButtonConfig t ->
35− m (Event t ())
59+ m (Event t (Either () ()))
3660buttonEl cfg = do
37− let (t, staticAttrs) = baseTag (cfg ^. #buttonBaseTag)
61+ let (t, tagAttrsDyn) = baseTag (cfg ^. #buttonBaseTag)
3862 attrsDyn = do
3963 enabled <- cfg ^. #buttonEnabled
4064 let (enabledClasses, enabledAttrs) = case enabled of
4165 True -> (cfg ^. #enabledClasses, mempty)
4266 False -> (cfg ^. #disabledClasses, "disabled" =: "")
4367 cs <- cfg ^. #constantClasses
68+ tagAttrs <- tagAttrsDyn
4469 pure $
45− staticAttrs
70+ tagAttrs
4671 <> "class" =: destructClasses (enabledClasses <> cs)
4772 <> enabledAttrs
48− (bEl, _) <- elDynAttr' t attrsDyn $ dynText $ cfg ^. #buttonText
49− pure $ domEvent Click bEl
73+ modAttrs <- dynamicAttributesToModifyAttributes attrsDyn
74+ let elCfg =
75+ (def @(ElementConfig EventResult t (DomBuilderSpace m)))
76+ & elementConfig_modifyAttributes .~ fmapCheap mapKeysToAttributeName modAttrs
77+ & elementConfig_eventSpec
78+ %~~ addEventSpecFlags
79+ (Proxy @(DomBuilderSpace m))
80+ Contextmenu
81+ ( const
82+ EventFlags
83+ { _eventFlags_propagation = Propagation_StopImmediate
84+ , _eventFlags_preventDefault = False
85+ }
86+ )
87+ & elementConfig_eventSpec
88+ %~~ addEventSpecFlags
89+ (Proxy @(DomBuilderSpace m))
90+ Click
91+ ( const
92+ EventFlags
93+ { _eventFlags_propagation = Propagation_StopImmediate
94+ , _eventFlags_preventDefault = False
95+ }
96+ )
97+ (bEl, _) <- element t elCfg (unTextBuilder (cfg ^. #buttonText))
98+ pure $ leftmost [Left <$> domEvent Click bEl, Right <$> domEvent Contextmenu bEl]
99+
100+(%~~) :: ASetter' s a -> (a -> a) -> s -> s
101+(%~~) = (%~)
octopod-frontend/src/Frontend/UIKit/Button/Dash.hs+2−2
@@ -54,7 +54,7 @@ buttonTypeClasses = \case
5454dashButton ::
5555 (DomBuilder t m, PostBuild t m) =>
5656 DashButtonConfig t ->
57− m (Event t ())
57+ m (Event t (Either () ()))
5858dashButton cfg =
5959 buttonEl
6060 CommonButtonConfig
@@ -66,6 +66,6 @@ dashButton cfg =
6666 , enabledClasses = mempty
6767 , disabledClasses = "dash--disabled"
6868 , buttonEnabled = cfg ^. #buttonEnabled
69− , buttonText = pure $ cfg ^. #buttonText
69+ , buttonText = textBuilder $ cfg ^. #buttonText
7070 , buttonBaseTag = ButtonTag
7171 }
octopod-frontend/src/Frontend/UIKit/Button/Expander.hs+3−2
@@ -10,6 +10,7 @@ where
1010import Control.Lens
1111import Control.Monad.Fix
1212import Data.Default
13+import Data.Functor
1314import Data.Generics.Labels ()
1415import Data.Text (Text)
1516import Frontend.Classes
@@ -65,7 +66,7 @@ expanderButton ::
6566 ExpanderButtonConfig t ->
6667 m (Dynamic t ExpanderState)
6768expanderButton cfg = mdo
68− stateDyn <- foldDyn (\() -> toggleState) (cfg ^. #buttonInitialState) toggleEv
69+ stateDyn <- foldDyn (\() -> toggleState) (cfg ^. #buttonInitialState) (toggleEv $> ())
6970 let constantClasses =
7071 "expander"
7172 <> maybe mempty buttonTypeClasses (cfg ^. #buttonType)
@@ -79,7 +80,7 @@ expanderButton cfg = mdo
7980 , enabledClasses = mempty
8081 , disabledClasses = mempty
8182 , buttonEnabled = pure True
82− , buttonText = cfg ^. #buttonText
83+ , buttonText = TextBuilder $ dynText $ cfg ^. #buttonText
8384 , buttonBaseTag = ButtonTag
8485 }
8586 pure stateDyn
octopod-frontend/src/Frontend/UIKit/Button/Large.hs+3−3
@@ -23,7 +23,7 @@ data LargeButtonConfig t = LargeButtonConfig
2323 , buttonType :: Dynamic t (Maybe LargeButtonType)
2424 , buttonPriority :: LargeButtonPriority
2525 , buttonStyle :: LargeButtonStyle
26− , buttonBaseTag :: BaseButtonTag
26+ , buttonBaseTag :: BaseButtonTag t
2727 }
2828 deriving stock (Generic)
2929
@@ -77,7 +77,7 @@ buttonTypeClasses LoadingLargeButtonType = "button--save-loading"
7777largeButton ::
7878 (DomBuilder t m, PostBuild t m) =>
7979 LargeButtonConfig t ->
80− m (Event t ())
80+ m (Event t (Either () ()))
8181largeButton cfg =
8282 buttonEl
8383 CommonButtonConfig
@@ -91,6 +91,6 @@ largeButton cfg =
9191 , enabledClasses = mempty
9292 , disabledClasses = "button--disabled"
9393 , buttonEnabled = cfg ^. #buttonEnabled
94− , buttonText = pure $ cfg ^. #buttonText
94+ , buttonText = textBuilder $ cfg ^. #buttonText
9595 , buttonBaseTag = cfg ^. #buttonBaseTag
9696 }
octopod-frontend/src/Frontend/UIKit/Button/Sort.hs+2−2
@@ -41,7 +41,7 @@ buttonSortStateClasses = \case
4141sortButton ::
4242 (DomBuilder t m, PostBuild t m) =>
4343 SortButtonConfig t ->
44− m (Event t ())
44+ m (Event t (Either () ()))
4545sortButton cfg =
4646 buttonEl
4747 CommonButtonConfig
@@ -53,6 +53,6 @@ sortButton cfg =
5353 , enabledClasses = mempty
5454 , disabledClasses = mempty
5555 , buttonEnabled = cfg ^. #buttonEnabled
56− , buttonText = pure $ cfg ^. #buttonText
56+ , buttonText = textBuilder $ cfg ^. #buttonText
5757 , buttonBaseTag = ButtonTag
5858 }
octopod-frontend/src/Frontend/UIKit/Button/Static.hs+25−12
@@ -4,13 +4,14 @@ module Frontend.UIKit.Button.Static
44 closePopupButton,
55 deleteOverrideButton,
66 undoOverrideButton,
7+ dropButton,
78 )
89where
910
1011import Frontend.UIKit.Button.Common
1112import Reflex.Dom
1213
13−closeNotificationButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
14+closeNotificationButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
1415closeNotificationButton =
1516 buttonEl
1617 CommonButtonConfig
@@ -18,11 +19,11 @@ closeNotificationButton =
1819 , enabledClasses = mempty
1920 , disabledClasses = mempty
2021 , buttonEnabled = pure True
21− , buttonText = pure ""
22+ , buttonText = ""
2223 , buttonBaseTag = ButtonTag
2324 }
2425
25−closeClassicPopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
26+closeClassicPopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
2627closeClassicPopupButton =
2728 buttonEl
2829 CommonButtonConfig
@@ -30,11 +31,11 @@ closeClassicPopupButton =
3031 , enabledClasses = mempty
3132 , disabledClasses = mempty
3233 , buttonEnabled = pure True
33− , buttonText = pure ""
34+ , buttonText = ""
3435 , buttonBaseTag = ButtonTag
3536 }
3637
37−closePopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
38+closePopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
3839closePopupButton =
3940 buttonEl
4041 CommonButtonConfig
@@ -42,30 +43,42 @@ closePopupButton =
4243 , enabledClasses = mempty
4344 , disabledClasses = mempty
4445 , buttonEnabled = pure True
45− , buttonText = pure ""
46+ , buttonText = ""
4647 , buttonBaseTag = ButtonTag
4748 }
4849
49−deleteOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
50+deleteOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
5051deleteOverrideButton =
5152 buttonEl
5253 CommonButtonConfig
53− { constantClasses = pure "overrides__delete spot spot--cancel"
54+ { constantClasses = pure "editable-row__delete spot spot--cancel"
5455 , enabledClasses = mempty
5556 , disabledClasses = mempty
5657 , buttonEnabled = pure True
57− , buttonText = pure ""
58+ , buttonText = ""
5859 , buttonBaseTag = ButtonTag
5960 }
6061
61−undoOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
62+undoOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
6263undoOverrideButton =
6364 buttonEl
6465 CommonButtonConfig
65− { constantClasses = pure "overrides__delete spot spot--undo"
66+ { constantClasses = pure "editable-row__delete spot spot--undo"
6667 , enabledClasses = mempty
6768 , disabledClasses = mempty
6869 , buttonEnabled = pure True
69− , buttonText = pure ""
70+ , buttonText = ""
71+ , buttonBaseTag = ButtonTag
72+ }
73+
74+dropButton :: (DomBuilder t m, PostBuild t m) => m (Event t (Either () ()))
75+dropButton =
76+ buttonEl
77+ CommonButtonConfig
78+ { constantClasses = pure "drop__handler"
79+ , enabledClasses = mempty
80+ , disabledClasses = mempty
81+ , buttonEnabled = pure True
82+ , buttonText = "Actions"
7083 , buttonBaseTag = ButtonTag
7184 }
octopod-frontend/src/Frontend/UIKit/Button/Tree.hsnew file+84
@@ -0,0 +1,84 @@
1+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
2+
3+{-# HLINT ignore "Use newtype instead of data" #-}
4+module Frontend.UIKit.Button.Tree
5+ ( treeButton,
6+ TreeButtonConfig (..),
7+ TextBuilder (..),
8+ TreeButtonStatus (..),
9+ TreeButtonApply (..),
10+ )
11+where
12+
13+import Control.Lens
14+import Control.Monad
15+import Control.Monad.Fix
16+import Data.Default
17+import Data.Functor
18+import Data.Generics.Labels ()
19+import Frontend.UIKit.Button.Common
20+import GHC.Generics (Generic)
21+import Reflex.Dom
22+import Reflex.Network (networkView)
23+
24+data TreeButtonConfig t = TreeButtonConfig
25+ { buttonText :: TextBuilder t
26+ , subtreeHasChanges :: Bool
27+ , visible :: Dynamic t Bool
28+ , -- | From outside
29+ forceState :: Event t Bool
30+ }
31+ deriving stock (Generic)
32+
33+instance Reflex t => Default (TreeButtonConfig t) where
34+ def =
35+ TreeButtonConfig
36+ { buttonText = ""
37+ , subtreeHasChanges = False
38+ , visible = pure True
39+ , forceState = never
40+ }
41+
42+treeButton ::
43+ (DomBuilder t m, PostBuild t m, MonadHold t m, MonadFix m) =>
44+ TreeButtonConfig t ->
45+ -- | (<Open?>, <Click>)
46+ m (Event t (Bool, Either () ()))
47+treeButton cfg = mdo
48+ (traceEvent "treeButton" -> clickedEv) <-
49+ networkView >=> switchHold never $
50+ (cfg ^. #visible) <&> \visible ->
51+ if visible
52+ then
53+ buttonEl
54+ CommonButtonConfig
55+ { constantClasses = do
56+ open <- openDyn
57+ pure $
58+ "collapse__head"
59+ <> (if cfg ^. #subtreeHasChanges then "collapse__head--has-changes" else mempty)
60+ <> (if open then mempty else "collapse--expanded")
61+ , buttonText = cfg ^. #buttonText
62+ , buttonBaseTag = ButtonTag
63+ , buttonEnabled = pure True
64+ , disabledClasses = mempty
65+ , enabledClasses = mempty
66+ }
67+ else pure never
68+ openDyn <-
69+ foldDyn
70+ ( \case
71+ Left () -> not
72+ Right x -> const x
73+ )
74+ False
75+ $ (leftmost [Right <$> cfg ^. #forceState, clickedEv $> Left ()])
76+ pure $ coincidence $ (\click -> (,click) <$> updated openDyn) <$> clickedEv
77+
78+data TreeButtonStatus
79+ = TreeButtonClosed
80+ | TreeButtonOpen
81+
82+data TreeButtonApply
83+ = TreeButtonApplyToSelf
84+ | TreeButtonApplyToSubtree
octopod-frontend/src/Frontend/Utils.hs+158−392
@@ -20,60 +20,45 @@ module Frontend.Utils
2020 DeploymentPageNotification (..),
2121 formatPosixToDateTime,
2222 dropdownWidget,
23− dropdownWidget',
2423 deploymentConfigProgressiveComponents,
25− deploymentConfigProgressive,
24+ -- deploymentConfigProgressive,
2625 holdClearingWith,
2726 unitEv,
2827 (<&&>),
29− ProgressiveFullConfig (..),
3028 RequestErrorHandler,
31− deploymentOverridesWidget,
32− deploymentOverridesWidgetSearched,
33− applicationOverridesWidget,
34− applicationOverridesWidgetSearched,
3529 debounceDyn,
3630 catchReturns,
31+ holdDynMaybe,
32+ constructWorkingOverridesEv,
3733 )
3834where
3935
4036import Common.Types as CT
37+import Control.Arrow
4138import Control.Lens
4239import Control.Monad
4340import Control.Monad.Reader
44−import Control.Monad.State.Strict
45−import qualified Data.Bifunctor as Bi
46−import qualified Data.Foldable as F
41+import qualified Data.ConfigTree as CT
4742import Data.Functor
48−import Data.Functor.Misc
4943import Data.Generics.Labels ()
50−import Data.List.NonEmpty (NonEmpty)
51−import Data.Map (Map)
44+import Data.Generics.Sum
5245import qualified Data.Map as M
53−import Data.Map.Monoidal.Strict (MonoidalMap)
54−import qualified Data.Map.Monoidal.Strict as MM
55−import Data.Maybe (fromMaybe, isJust, isNothing)
56−import Data.Monoid
57−import Data.Set (Set)
58−import qualified Data.Set as S
46+import Data.Maybe (fromMaybe)
47+import Data.String
5948import Data.Text as T (Text, null, pack, strip)
60−import Data.Text.Search
61−import Data.These
6249import Data.Time
6350import Data.UniqMap
6451import Data.Unique
6552import Data.Witherable
6653import Data.WorkingOverrides
54+import Debug.Trace
6755import Frontend.API
6856import Frontend.GHCJS
6957import Frontend.UIKit
70−import GHC.Generics (Generic)
7158import GHCJS.DOM
7259import GHCJS.DOM.Element as DOM
7360import GHCJS.DOM.EventM (on, target)
7461import GHCJS.DOM.GlobalEventHandlers as Events (click)
75−import GHCJS.DOM.Node as DOM
76−import Generic.Data (Generically (..))
7762import Reflex.Dom as R
7863import Reflex.Dom.Renderable
7964import Reflex.Network
@@ -90,49 +75,30 @@ elementClick = do
9075 doc <- currentDocumentUnchecked
9176 wrapDomEvent doc (`on` Events.click) $ ClickedElement <$> target
9277
93−-- | Dropdown widget which binds its own @document click@ event.
94−dropdownWidget ::
95− MonadWidget t m =>
96− -- | Button widget which opens the dropdown widget.
97− m () ->
98− -- | Widget with the dropdown list.
99− -- Returns an event carrying the user's selection.
100− m (Event t a) ->
101− m (Event t a)
102−dropdownWidget btn body = mdo
103− clickedEl <- elementClick
104− dropdownWidget' clickedEl btn body
105−
10678-- | Similar to 'dropdownWidget' but uses @document click@ event that may be
10779-- shared between other widgets.
108−dropdownWidget' ::
80+dropdownWidget ::
10981 MonadWidget t m =>
110− -- | Document click event that may be shared between other widgets.
111− Event t ClickedElement ->
112− -- | Button widget which opens the dropdown widget.
113− m () ->
11482 -- | Widget with the dropdown list.
11583 -- Returns an event carrying the user's selection.
11684 m (Event t a) ->
11785 m (Event t a)
118−dropdownWidget' clickedEl btn body = mdo
119− clickInsideEv <- performEvent $
120− ffor clickedEl $ \(ClickedElement clicked) ->
121− DOM.contains (_element_raw btnEl) clicked
122− openedDyn <- foldDyn switchState False $ clickInsideEv
123− let switchState ev cur = ev && not cur
124− wrapperClassDyn = ffor openedDyn $ \case
86+dropdownWidget body = mdo
87+ openedDyn <- toggle False $ btnEl
88+ let wrapperClassDyn = ffor openedDyn $ \case
12589 True -> "class" =: "drop drop--actions drop--expanded"
12690 False -> "class" =: "drop drop--actions"
127− (btnEl, (_, wEv)) <- elDynAttr'
91+ (btnEl, wEv) <- elDynAttr
12892 "div"
12993 wrapperClassDyn
13094 $ do
131− btn
132− elDynAttr'
133− "div"
134− (constDyn $ "class" =: "drop__dropdown")
135− body
95+ clickedEv <- dropButton
96+ aEv <-
97+ elDynAttr
98+ "div"
99+ (constDyn $ "class" =: "drop__dropdown")
100+ body
101+ pure (clickedEv, aEv)
136102 pure wEv
137103
138104showT :: Show a => a -> Text
@@ -214,90 +180,46 @@ octopodTextInput clss lbl placeholder val errEv =
214180 elClass "section" "deployment__section" $ do
215181 elClass "h3" "deployment__sub-heading" $ text lbl
216182 elClass "div" "deployment__widget" $ do
217− (value -> valDyn, validDyn) <- octopodTextInput' (pure []) (pure False) (pure clss) placeholder (pure . fromMaybe "" $ val) errEv
183+ (value -> valDyn, validDyn) <- octopodTextInput' (pure []) (pure False) (pure mempty) (pure clss) placeholder (pure . fromMaybe "" $ val) errEv
218184 pure (valDyn, validDyn)
219185
220186-- | Widget that can show and hide overrides if there are more than 3. This
221187-- widget is used in the deployments table and the deployment action table.
222188overridesWidget ::
223− (MonadWidget t m, Renderable te, Ord te) =>
189+ (MonadWidget t m, Renderable te, IsString te, Monoid te) =>
224190 -- | List of overrides.
225191 Overrides' te l ->
226− (Event t () -> m (Event t (DefaultConfig' te l))) ->
227192 m ()
228−overridesWidget ovs getDef = divClass "listing listing--for-text" $ mdo
229− defMDyn <- getDef firstExpand >>= holdDynMaybe
230− dyn_ $
231− expandState <&> \case
232− ExpandedState ->
233− void $
234− networkView $
235− do
236− defM <- defMDyn
237− pure $
238− showNonEditableWorkingOverride (isNothing defM) (isJust defM) RegularNonEditableWorkingOverrideStyle $
239− elemsUniq $ constructWorkingOverrides defM ovs
240− ContractedState ->
241− let ovsList = elemsUniq $ constructWorkingOverrides Nothing ovs
242− in showNonEditableWorkingOverride False False RegularNonEditableWorkingOverrideStyle $
243− take 3 ovsList
244−
245− expandState <-
246− expanderButton
247− ExpanderButtonConfig
248− { buttonText = do
249− s <- expandState
250− pure $ case s of
251− ExpandedState -> "Hide default configuration"
252− ContractedState -> "Show full configuration"
253− , buttonInitialState = ContractedState
254− , buttonType = Just ListingExpanderButton
255− , buttonStyle = RegularExpanderButtonStyle
256− }
257− firstExpand <- headE $ ($> ()) $ ffilter (== ExpandedState) $ updated expandState
193+overridesWidget (Overrides ovs) = mdo
194+ let l = CT.toFlatList ovs
195+ expandable = length l > 3
196+
197+ if expandable
198+ then mdo
199+ dyn_ $
200+ expandState <&> \s -> do
201+ let l' = case s of
202+ ExpandedState -> l
203+ ContractedState -> take 3 l
204+ showFlatConfig l'
205+ pure ()
206+
207+ expandState <-
208+ expanderButton
209+ ExpanderButtonConfig
210+ { buttonText = do
211+ s <- expandState
212+ pure $ case s of
213+ ExpandedState -> "Hide default configuration"
214+ ContractedState -> "Show full configuration"
215+ , buttonInitialState = ContractedState
216+ , buttonType = Just ListingExpanderButton
217+ , buttonStyle = RegularExpanderButtonStyle
218+ }
219+ pure ()
220+ else showFlatConfig l
258221 pure ()
259222
260−deploymentOverridesWidget ::
261− (MonadWidget t m) =>
262− RequestErrorHandler t m ->
263− Overrides 'DeploymentLevel ->
264− m ()
265−deploymentOverridesWidget hReq depOvs =
266− deploymentOverridesWidgetSearched hReq (wrapResult depOvs)
267−
268−deploymentOverridesWidgetSearched ::
269− (MonadWidget t m) =>
270− RequestErrorHandler t m ->
271− Overrides' SearchResult 'DeploymentLevel ->
272− m ()
273−deploymentOverridesWidgetSearched hReq depOvs =
274− overridesWidget depOvs $ (fmap . fmap . fmap) wrapResult $ defaultDeploymentOverrides >=> hReq
275−
276−applicationOverridesWidget ::
277− MonadWidget t m =>
278− RequestErrorHandler t m ->
279− Overrides 'DeploymentLevel ->
280− Overrides 'ApplicationLevel ->
281− m ()
282−applicationOverridesWidget hReq depOvs appOvs =
283− applicationOverridesWidgetSearched hReq (wrapResult depOvs) (wrapResult appOvs)
284−
285−applicationOverridesWidgetSearched ::
286− MonadWidget t m =>
287− RequestErrorHandler t m ->
288− Overrides' SearchResult 'DeploymentLevel ->
289− Overrides' SearchResult 'ApplicationLevel ->
290− m ()
291−applicationOverridesWidgetSearched hReq depOvs appOvs =
292− overridesWidget appOvs $ \fire -> do
293− depDefEv <- defaultDeploymentOverrides fire >>= hReq
294− fmap switchDyn $
295− (fmap . fmap . fmap) wrapResult $
296− networkHold (pure never) $
297− depDefEv <&> \depDef -> do
298− pb <- getPostBuild
299− defaultApplicationOverrides (pure $ Right $ applyOverrides (deSearch depOvs) depDef) pb >>= hReq
300−
301223-- | Type of notification at the top of pages.
302224data DeploymentPageNotification
303225 = -- | Message text for a \"success\" events.
@@ -351,9 +273,11 @@ deploymentPopupBody ::
351273 m (Dynamic t (Maybe DeploymentUpdate))
352274deploymentPopupBody hReq defAppOv defDepOv errEv = mdo
353275 pb <- getPostBuild
354− (defDepEv, defApp, depCfgEv) <- deploymentConfigProgressiveComponents hReq deploymentOvsDynDebounced
355− defAppM <- holdClearingWith defApp (unitEv deploymentOvsDyn)
356− defDep <- holdDynMaybe defDepEv
276+ (defDepEv, defAppEv, depCfgEv) <-
277+ deploymentConfigProgressiveComponents hReq deploymentOvsDynDebounced
278+
279+ defAppEvM <- holdClearingWith defAppEv (unitEv deploymentOvsDyn)
280+
357281 depKeys <- deploymentOverrideKeys pb >>= hReq
358282
359283 let commandResponseEv = fmapMaybe commandResponse errEv
@@ -361,46 +285,47 @@ deploymentPopupBody hReq defAppOv defDepOv errEv = mdo
361285
362286 let holdDCfg ::
363287 Dynamic t [Text] ->
364− Dynamic t (Maybe (DefaultConfig l)) ->
288+ (DefaultConfig l) ->
365289 Overrides l ->
366− m (Dynamic t (Overrides l), Dynamic t (Overrides l), Dynamic t Bool)
367− holdDCfg values dCfgDyn ovs = mdo
368− ovsDyn <- holdDyn ovs ovsEv
369− ovsDynDebounced <- holdDyn ovs ovsEvDebounced
370− x <- attachDyn (current ovsDyn) dCfgDyn
371− (splitE -> (ovsEvEv, isValidDynEv)) <-
372− (>>= joinEvM) $
373− runWithReplace' loadingOverrides $
374− updated x <&> \(ovs', dCfg) -> untilReady' loadingOverrides $ envVarsInput values dCfg ovs'
375− ovsEv <- joinEvM ovsEvEv
376− ovsEvDebounced <- debounce 2 ovsEv
377− isValid <- join <$> holdDyn (pure False) isValidDynEv
378− pure (ovsDyn, ovsDynDebounced, isValid)
290+ m (Dynamic t (Overrides l), Dynamic t (Overrides l))
291+ holdDCfg (traceDyn "values" -> values) cfgDyn@(DefaultConfig ct) ovs = mdo
292+ let ovs' = constructWorkingOverrides cfgDyn ovs
293+ lookupOverride k = CT.lookup (CT.deconstructConfigKey k) ct
294+ (resCfgDyn :: Dynamic t [Dynamic t WorkingOverride]) <- envVarsInput values lookupOverride ovs'
295+ (resEvDebounced :: Event t [Dynamic t WorkingOverride]) <- debounce 2 $ updated resCfgDyn
296+ let resDyn' = join . fmap destructWorkingOverridesDyn $ resCfgDyn
297+ debouncedResDyn <- fmap join . holdDyn (pure ovs) . fmap destructWorkingOverridesDyn $ resEvDebounced
298+ pure (resDyn', debouncedResDyn)
379299
380300 depKeysDyn <- holdDyn [] depKeys
381− (deploymentOvsDyn, deploymentOvsDynDebounced, depValidDyn) <-
382− deploymentSection "Deployment configuration" $ holdDCfg depKeysDyn defDep defDepOv
301+
302+ (join . fmap fst &&& join . fmap snd -> (deploymentOvsDyn, deploymentOvsDynDebounced)) <-
303+ deploymentSection "Deployment configuration" $
304+ networkHold (loadingOverrides $> (pure defDepOv, pure defDepOv)) $
305+ defDepEv <&> \defDep -> holdDCfg depKeysDyn defDep defDepOv
383306
384307 appKeys <- waitForValuePromptly depCfgEv $ \deploymentCfg -> do
385308 pb' <- getPostBuild
386309 applicationOverrideKeys (pure $ Right deploymentCfg) pb' >>= hReq >>= immediateNothing
310+
387311 appKeysDyn <- holdDyn [] $ catMaybes appKeys
388− (applicationOvsDyn, _, appValidDyn) <- deploymentSection "App configuration" $ holdDCfg appKeysDyn defAppM defAppOv
312+ (join -> applicationOvsMDyn) <-
313+ deploymentSection "App configuration" >=> holdDyn (pure Nothing) . fmap sequenceA $
314+ networkView $
315+ defAppEvM <&> \case
316+ Nothing -> loadingOverrides $> Nothing
317+ Just defApp -> Just . fst <$> holdDCfg appKeysDyn defApp defAppOv
389318
390319 pure $ do
391− depValid <- depValidDyn
392− appValid <- appValidDyn
393− if depValid && appValid
394− then do
395− depCfg <- deploymentOvsDyn
396− appOvs <- applicationOvsDyn
397− pure $
398− Just $
399− DeploymentUpdate
400− { appOverrides = appOvs
401− , deploymentOverrides = depCfg
402− }
403− else pure Nothing
320+ depCfg <- deploymentOvsDyn
321+ appOvsM <- applicationOvsMDyn
322+ pure $ do
323+ appOvs <- appOvsM
324+ Just $
325+ DeploymentUpdate
326+ { appOverrides = appOvs
327+ , deploymentOverrides = depCfg
328+ }
404329
405330deploymentConfigProgressiveComponents ::
406331 MonadWidget t m =>
@@ -427,36 +352,17 @@ deploymentConfigProgressiveComponents hReq depOvsDyn = do
427352 defaultApplicationOverrides (pure $ Right depCfg) pb' >>= hReq
428353 pure (defDepEv, defAppEv, depCfgEv)
429354
430−deploymentConfigProgressive ::
431− MonadWidget t m =>
432− RequestErrorHandler t m ->
433− Dynamic t (Overrides 'DeploymentLevel) ->
434− Dynamic t (Overrides 'ApplicationLevel) ->
435− m (Dynamic t ProgressiveFullConfig)
436−deploymentConfigProgressive hReq depOvsDyn appOvsDyn = do
437− (depCfgEv, defAppEv, _) <- deploymentConfigProgressiveComponents hReq depOvsDyn
438− defAppMDyn <- holdDynMaybe defAppEv
439− defCfgMDyn <- holdDynMaybe depCfgEv
440− pure $ do
441− defAppM <- defAppMDyn
442− appOvs <- appOvsDyn
443− defDepM <- defCfgMDyn
444− depOvs <- depOvsDyn
445− pure
446− ProgressiveFullConfig
447− { appConfig = constructWorkingOverrides defAppM appOvs
448− , appConfigLoading = isNothing defAppM
449− , depConfig = constructWorkingOverrides defDepM depOvs
450− , depConfigLoading = isNothing defDepM
451− }
452−
453−data ProgressiveFullConfig = ProgressiveFullConfig
454− { appConfig :: WorkingOverrides
455− , appConfigLoading :: Bool
456− , depConfig :: WorkingOverrides
457− , depConfigLoading :: Bool
458− }
459− deriving stock (Generic)
355+constructWorkingOverridesEv ::
356+ MonadWidget t m => Event t (DefaultConfig l) -> Dynamic t (Overrides l) -> m (Event t WorkingOverrides)
357+constructWorkingOverridesEv defCfgEv ovsDyn = do
358+ defCfgMDyn <- holdDynMaybe defCfgEv
359+ pure $
360+ catMaybes . updated $ do
361+ defCfgM <- defCfgMDyn
362+ ovs <- ovsDyn
363+ pure $ do
364+ defCfg <- defCfgM
365+ pure $ constructWorkingOverrides defCfg ovs
460366
461367waitForValuePromptly :: (MonadHold t m, Adjustable t m) => Event t x -> (x -> m (Event t y)) -> m (Event t y)
462368waitForValuePromptly ev f = fmap switchPromptlyDyn $ networkHold (pure never) $ f <$> ev
@@ -492,32 +398,23 @@ errorHeader ::
492398 -- | Message text.
493399 Dynamic t Text ->
494400 m ()
495−errorHeader appErr = do
496− divClass "deployment__output notification notification--danger" $ do
401+errorHeader appErr =
402+ elClass "pre" "deployment__output notification notification--danger" $
497403 dynText appErr
498404
499405-- | Widget with override fields. This widget supports adding and
500406-- removing key-value pairs.
501407envVarsInput ::
502− forall l t m.
408+ forall t m.
503409 MonadWidget t m =>
504410 Dynamic t [Text] ->
505− Maybe (DefaultConfig l) ->
411+ (Text -> Maybe Text) ->
506412 -- | Initial deployment overrides.
507− Overrides l ->
508− -- | Updated deployment overrides.
509− m (Event t (Overrides l), Dynamic t Bool)
510−envVarsInput values dCfg ovs = mdo
511− let initialOverrides = constructWorkingOverrides dCfg ovs
512− (envsDyn, addedKeys) <-
513− mapAccumDyn
514− ( \prevOvs -> \case
515− Left () ->
516− Bi.second S.singleton $ insertUniqStart newWorkingOverride prevOvs
517− Right (Endo f) -> (f prevOvs, S.empty)
518− )
519− initialOverrides
520− $ leftmost [Left <$> clickEv, Right <$> updEv]
413+ WorkingOverrides ->
414+ -- | Updated deployment overrides. [(<full deconstructed key>, <value>)]
415+ m (Dynamic t [Dynamic t WorkingOverride])
416+envVarsInput (traceDyn "values" -> values) lookupOverride ovs = divClass "padded" $ mdo
417+ let initialConfig = CT.markValues ovs
521418 clickEv <-
522419 dashButton
523420 DashButtonConfig
@@ -526,187 +423,65 @@ envVarsInput values dCfg ovs = mdo
526423 , buttonType = Just AddDashButtonType
527424 , buttonStyle = OverridesDashButtonStyle
528425 }
529− let workingOverridesWithErrors = validateWorkingOverrides . uniqMap <$> envsDyn
530− erroredOverride = M.filter ((/= mempty) . snd) <$> workingOverridesWithErrors
531− isValid = M.null <$> erroredOverride
532− (_, changedValues) <-
533− mapAccumDyn
534− ( \prev new ->
535− ( fmap (\(v, _) -> (v, mempty)) new
536− , new <> prev
537− )
538− )
539− mempty
540− (updated erroredOverride)
541− let updatedValues = restrictMapEv updatedKeys (updated workingOverridesWithErrors)
542− valChangedEvSelector = fanMap (clearedOvs <> updatedValues <> changedValues)
543− (splitE -> (clearedOvs, deletedOvs)) =
544− alignEventWithMaybe
545− ( \case
546− These keys m ->
547− Just $
548− let cleared = M.restrictKeys m keys
549− in (cleared, M.fromSet (const Nothing) (keys `S.difference` M.keysSet cleared))
550− _ -> Nothing
551− )
552− removedKeys
553− (updated workingOverridesWithErrors)
554− listChange =
555− deletedOvs <> (fmap . fmap) Just (restrictMapEv addedKeys (updated workingOverridesWithErrors))
556−
557− (splitE3 -> (updEv, removedKeys, updatedKeys)) <-
558− switchDyn . fmap F.fold
559− <$> listHoldWithKey
560− (validateWorkingOverrides . uniqMap $ initialOverrides)
561− listChange
562− ( \i val -> do
563− valDyn <- holdDyn val $ select valChangedEvSelector $ Const2 i
564− userActionEv <- envVarInput values valDyn
565− pure $ performUserOverrideAction (lookupDefaultConfig <$> dCfg) i <$> userActionEv
566− )
567− let addingIsEnabled = all (\(WorkingOverrideKey _ x, _) -> not . T.null $ x) . elemsUniq <$> envsDyn
568− case dCfg of
569− Just _ -> pure ()
570− Nothing -> loadingOverrides
571− pure (updated $ destructWorkingOverrides <$> envsDyn, isValid)
572−
573−restrictMapEv :: (Reflex t, Ord k) => Event t (Set k) -> Event t (Map k v) -> Event t (Map k v)
574−restrictMapEv =
575− alignEventWithMaybe
576− ( \case
577− These keys _ | S.null keys -> Nothing
578− These keys m -> Just $ M.restrictKeys m keys
579− _ -> Nothing
580− )
581−
582−validateWorkingOverrides ::
583− forall f.
584− Traversable f =>
585− f WorkingOverride ->
586− f (WorkingOverride, OverrideErrors)
587−validateWorkingOverrides overrides =
588− let (result, keyOccurrences :: MonoidalMap Text (Sum Int)) =
589− flip runState mempty $ forM overrides \override@(WorkingOverrideKey _ key, value') -> do
590− case value' of
591− WorkingDeletedValue _ -> pure ()
592− _ -> modify (<> MM.singleton key (Sum 1))
593− pure . (override,) . mconcat $
594− [ case MM.lookup key keyOccurrences of
595− Just (Sum n)
596− | n > 1 ->
597− overrideKeyErrors "You can not use the same key multiple times."
598− _ -> mempty
599− , if T.null key
600− then overrideKeyErrors "Keys can not be empty."
601− else mempty
602− , case value' of
603− WorkingCustomValue "" -> overrideValueErrors "Values can not be empty."
604− WorkingCustomValue _ -> mempty
605− WorkingDefaultValue "" -> overrideValueErrors "Values can not be empty."
606− WorkingDefaultValue _ -> mempty
607− WorkingDeletedValue _ -> mempty
608− ]
609− in result
610−
611−data OverrideErrors = OverrideErrors
612− { keyErrors :: Maybe (NonEmpty Text)
613− , valueErrors :: Maybe (NonEmpty Text)
614− }
615− deriving stock (Generic, Eq, Show)
616− deriving (Semigroup, Monoid) via Generically OverrideErrors
617−
618−overrideKeyErrors :: Text -> OverrideErrors
619−overrideKeyErrors x = mempty {keyErrors = Just $ pure x}
620−
621−overrideValueErrors :: Text -> OverrideErrors
622−overrideValueErrors x = mempty {valueErrors = Just $ pure x}
426+ let emptyItem = ("", CustomConfigValue $ Left $ CustomKey "")
427+ addedItemsCount <- foldDyn (const (fst . insertUniqStart ())) emptyUniqKeyMap clickEv
428+ addedOvsListDyn <- (fmap . fmap) (fmap snd . M.toList)
429+ <$> list (uniqMap <$> addedItemsCount)
430+ $ \_ ->
431+ envVarInput lookupOverride values emptyItem
432+ treeResCfg <-
433+ showOverrideTree
434+ ((== Just True) . fmap (T.null . T.strip) . getConfigValueText . snd)
435+ (isn't (_Ctor' @"DefaultConfigValue") . snd)
436+ (\_ v -> v)
437+ (\a _ _ -> envVarInput lookupOverride values (trace "a" a))
438+ initialConfig
439+ let resCfg :: Dynamic t [Dynamic t (Text, ConfigValue Text)]
440+ resCfg = (<> treeResCfg) <$> addedOvsListDyn
441+ addingIsEnabled = join $ fmap and . sequenceA . (fmap . fmap) (not . T.null . fst) <$> resCfg
442+ pure resCfg
623443
624444-- | Widget for entering a key-value pair.
625445envVarInput ::
626446 (MonadWidget t m) =>
447+ (Text -> Maybe Text) ->
627448 -- | The key values to suggest to the user
628449 Dynamic t [Text] ->
629450 -- | Current variable key and value.
630− Dynamic t (WorkingOverride, OverrideErrors) ->
631− m (Event t UserOverrideAction)
632−envVarInput values val = untilReadyEv' loadingOverride $ do
633− let kDyn = val <&> fst <&> \(WorkingOverrideKey _ x, _) -> x
634− -- Either <override present> <override deleted>
635− d <-
636− eitherDyn $
637− val <&> snd . fst <&> \case
638− WorkingCustomValue v -> Right (v, EditedOverrideFieldType)
639− WorkingDefaultValue v -> Right (v, DefaultOverrideFieldType)
640− WorkingDeletedValue v -> Left v
641− networkView >=> switchHold never $
642− d <&> \case
643− Right vtDyn -> do
644− let (vDyn, vTypeDyn) = splitDynPure vtDyn
645− (keyTextDyn, valTextDyn, closeEv) <-
646− overrideField
647− values
648− OverrideField
649− { fieldValue = kDyn
650− , fieldError = keyErrors . snd <$> val
651− , fieldDisabled =
652− val <&> fst <&> \(WorkingOverrideKey t _, _) -> t == DefaultWorkingOverrideKey
653− , fieldType =
654− val <&> fst <&> \(WorkingOverrideKey t _, _) -> case t of
655− CustomWorkingOverrideKey -> EditedOverrideFieldType
656− DefaultWorkingOverrideKey -> DefaultOverrideFieldType
657− }
658− OverrideField
659− { fieldValue = vDyn
660− , fieldError = valueErrors . snd <$> val
661− , fieldDisabled = pure False
662− , fieldType = vTypeDyn
663− }
664− pure $
665− leftmost
666− [ UpdateKey <$> updated keyTextDyn
667− , UpdateValue <$> updated valTextDyn
668− , closeEv $> DeleteOverride
669− ]
670− Left vDyn -> do
671− restoreEv <-
672− networkView >=> switchHold never $ do
673− v <- vDyn
674− k <- kDyn
675− pure $ deletedOverride k v
451+ WorkingOverride ->
452+ m (Dynamic t WorkingOverride)
453+envVarInput f keys val = mdo
454+ value' <- holdDyn val (leftmost [buttonValue, updated inputValue])
455+ (keyTextDyn, valTextDyn, buttonPressEv) <- configField keys value'
456+ let inputValue = do
457+ v <- valTextDyn
458+ k <- keyTextDyn
676459 pure $
677− flip push restoreEv $ \() -> do
678− v <- sample . current $ vDyn
679− pure $ UpdateValue <$> v
680−
681−data UserOverrideAction = UpdateKey !Text | UpdateValue !Text | DeleteOverride
682−
683−performUserOverrideAction ::
684− Maybe (Text -> Maybe Text) ->
685− Int ->
686− UserOverrideAction ->
687− -- | (endo, removed/cleared keys, updatedKeys)
688− (Endo WorkingOverrides, Set Int, Set Int)
689−performUserOverrideAction f i (UpdateValue v) = (,S.empty,S.singleton i) $
690− Endo $
691− updateUniq i $ \(k@(WorkingOverrideKey _ kt), _) ->
692− ( k
693− , case f >>= ($ kt) of
694− Just v' | v == v' -> WorkingDefaultValue v
695− _ -> WorkingCustomValue v
696− )
697−performUserOverrideAction _ i (UpdateKey k) = (,S.empty,S.singleton i) $
698− Endo $
699− updateUniq i $
700− \(_, v) -> (WorkingOverrideKey CustomWorkingOverrideKey k, v)
701−performUserOverrideAction f i DeleteOverride = (,S.singleton i,S.empty) $
702− Endo $ \m ->
703− case f of
704− Nothing -> updateUniq i (\(k, _) -> (k, WorkingDeletedValue Nothing)) m
705− Just _ -> case lookupUniq i m of
706− Nothing -> m
707− Just (WorkingOverrideKey DefaultWorkingOverrideKey k, _) ->
708− updateUniq i (const (WorkingOverrideKey DefaultWorkingOverrideKey k, WorkingDeletedValue (f >>= ($ k)))) m
709− Just (WorkingOverrideKey CustomWorkingOverrideKey _, _) -> deleteUniq i m
460+ ( k
461+ , case f k of
462+ Nothing -> CustomConfigValue $ Left $ CustomKey v
463+ Just x | x == v -> DefaultConfigValue v
464+ Just _ -> CustomConfigValue $ Right $ CustomValue v
465+ )
466+ buttonValue = flip push buttonPressEv $ \x ->
467+ Just <$> do
468+ k <- sample . current $ keyTextDyn
469+ (k,) <$> case x of
470+ ConfigFieldButtonClear -> do
471+ v <- case f k of
472+ Nothing -> sample . current $ valTextDyn
473+ Just v -> pure v
474+ pure $ CustomConfigValue $ Right $ DeletedValue $ Just v
475+ ConfigFieldButtonRecover -> do
476+ case f k of
477+ Nothing -> do
478+ -- the value is not in the default config
479+ v <- sample . current $ valTextDyn
480+ pure $ CustomConfigValue $ Left $ CustomKey v
481+ Just v -> do
482+ -- the value is in the default config
483+ pure $ DefaultConfigValue v
484+ pure $ value'
710485
711486holdDynMaybe :: (Reflex t, MonadHold t m) => Event t a -> m (Dynamic t (Maybe a))
712487holdDynMaybe ev = holdDyn Nothing $ fmapCheap Just ev
@@ -716,12 +491,6 @@ immediateNothing ev = do
716491 pb <- getPostBuild
717492 pure $ leftmost [pb $> Nothing, fmapCheap Just ev]
718493
719−attachDyn :: (Reflex t, MonadHold t m) => Behavior t a -> Dynamic t b -> m (Dynamic t (a, b))
720−attachDyn b d = do
721− currD <- sample . current $ d
722− currB <- sample b
723− holdDyn (currB, currD) (attach b $ updated d)
724−
725494holdClearingWith :: (Reflex t, MonadHold t m) => Event t a -> Event t () -> m (Dynamic t (Maybe a))
726495holdClearingWith aEv clear =
727496 holdDyn Nothing $ leftmost [fmapCheap Just aEv, fmapCheap (const Nothing) clear]
@@ -751,6 +520,3 @@ catchReturns :: (DomBuilder t m, MonadFix m) => (Event t () -> m a) -> m a
751520catchReturns f = mdo
752521 (divEl, a) <- el' "div" $ f $ ($> ()) . ffilter (== 13) $ domEvent Keypress divEl
753522 pure a
754−
755−splitE3 :: Reflex t => Event t (a, b, c) -> (Event t a, Event t b, Event t c)
756−splitE3 e = ((\(a, _, _) -> a) <$> e, (\(_, b, _) -> b) <$> e, (\(_, _, c) -> c) <$> e)
octopod-frontend/src/Page/ClassicPopup.hs+3−2
@@ -12,6 +12,7 @@ where
1212
1313import Reflex.Dom
1414
15+import Data.Functor
1516import Data.Generics.Labels ()
1617import Frontend.UIKit
1718
@@ -53,7 +54,7 @@ popupWidget m =
5354 divClass "classic-popup__slot" $ do
5455 (okEv, cancelEv) <- m
5556 closeEv <- closeClassicPopupButton
56− pure $ (okEv, leftmost [cancelEv, closeEv])
57+ pure $ (okEv, leftmost [cancelEv, closeEv $> ()])
5758
5859-- | Popup that requires confirmation of deployment deletion.
5960confirmArchivePopup ::
@@ -77,5 +78,5 @@ confirmArchivePopup showEv txt = do
7778 & #buttonStyle .~~ DialogActionLargeButtonStyle
7879 & #buttonPriority .~~ SecondaryLargeButton
7980 & #buttonText .~~ "Cancel"
80− pure (okEv, cancelEv)
81+ pure (okEv $> (), cancelEv $> ())
8182 classicPopup showEv body
octopod-frontend/src/Page/Deployment.hs+39−34
@@ -5,31 +5,30 @@
55--This module contains the definition of a deployment page.
66module Page.Deployment (deploymentPage) where
77
8−import Control.Lens
9−import Control.Monad
10−import Data.Coerce
11−import Data.Generics.Product (field)
12−import Data.Text as T (Text, pack)
13−import Obelisk.Route.Frontend
14−import Reflex.Dom as R
15−import Servant.Reflex
16−
178import Common.Types as CT
189import Common.Utils
10+import Control.Lens
11+import Control.Monad
1912import Control.Monad.Reader
2013import Data.Align
14+import Data.Coerce
15+import Data.Functor
2116import Data.Generics.Labels ()
17+import Data.Generics.Product (field)
18+import Data.Text as T (Text, pack)
2219import Data.Time
23−import Data.UniqMap
2420import Frontend.API
2521import Frontend.GHCJS
2622import Frontend.Route
2723import Frontend.UIKit
2824import Frontend.Utils
25+import Obelisk.Route.Frontend
2926import Page.ClassicPopup
3027import Page.Elements.Links
3128import Page.Popup.EditDeployment
29+import Reflex.Dom as R
3230import Reflex.Network
31+import Servant.Reflex
3332import Servant.Reflex.Extra
3433
3534-- | The root widget of a deployment page. It requests the deployment data.
@@ -125,7 +124,7 @@ deploymentHead dfiDyn sentEv =
125124 & #buttonStyle .~~ PageActionLargeButtonStyle
126125 & #buttonText .~~ "Recover from archive"
127126 & #buttonEnabled .~~ btnEnabledDyn
128− void $ restoreEndpoint (Right . coerce <$> dname) btnEv
127+ void $ restoreEndpoint (Right . coerce <$> dname) $ btnEv $> ()
129128 pure (never, never)
130129 else mdo
131130 let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status"
@@ -159,10 +158,10 @@ deploymentHead dfiDyn sentEv =
159158 , buttonType = pure $ Just LogsLargeButtonType
160159 , buttonPriority = SecondaryLargeButton
161160 , buttonStyle = PageActionLargeButtonStyle
162− , buttonBaseTag = ATag url
161+ , buttonBaseTag = ATag $ pure url
163162 }
164163 )
165− delEv <- confirmArchivePopup archEv $ do
164+ delEv <- confirmArchivePopup (archEv $> ()) $ do
166165 text "Are you sure you want to archive the"
167166 el "br" blank
168167 dynText dname
@@ -192,7 +191,7 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $
192191 wrapRequestErrors $ \hReq -> do
193192 let nameDyn = dfiDyn <^.> dfiName
194193 depDyn = dfiDyn <^.> #deployment
195− cfgDyn <- deploymentConfigProgressive hReq (depDyn <^.> #deploymentOverrides) (depDyn <^.> #appOverrides)
194+ (defDepCfgEv, defAppCfgEv, _) <- deploymentConfigProgressiveComponents hReq (depDyn <^.> #deploymentOverrides)
196195 divClass "deployment__summary" $ do
197196 divClass "deployment__stat" $ do
198197 elClass "b" "deployment__param" $ text "Status"
@@ -213,18 +212,26 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $
213212 divClass "deployment__widget" $
214213 divClass "listing" $
215214 void $ simpleList urlsDyn renderMetadataLink
216− void $
217− networkView $
218− cfgDyn <&> \cfg -> do
219− let showVars bL l =
220− divClass "deployment__widget" $
221− showNonEditableWorkingOverride (cfg ^. bL) (not $ cfg ^. bL) LargeNonEditableWorkingOverrideStyle $
222− elemsUniq (cfg ^. l)
223− deploymentSection "Deployment configuration" $ showVars #depConfigLoading #depConfig
224− deploymentSection "App configuration" $ showVars #appConfigLoading #appConfig
215+ deploymentSection "Deployment configuration" $ showVars defDepCfgEv $ depDyn <^.> #deploymentOverrides
216+ deploymentSection "App configuration" $ showVars defAppCfgEv $ depDyn <^.> #appOverrides
225217 deploymentSection "Actions" $
226218 divClass "table table--actions" $
227− actionsTable hReq updEv nameDyn
219+ actionsTable updEv nameDyn
220+
221+showVars ::
222+ MonadWidget t m =>
223+ Event t (DefaultConfig l) ->
224+ Dynamic t (Overrides l) ->
225+ m ()
226+showVars defCfgEv ovsDyn = do
227+ workingOvsEv <- constructWorkingOverridesEv defCfgEv ovsDyn
228+ _ <-
229+ divClass "deployment__widget" $
230+ networkHold nonEditableLoading $
231+ workingOvsEv <&> \x -> do
232+ showNonEditableWorkingOverrideTree (pure x)
233+ pure ()
234+ pure ()
228235
229236-- | Widget with a table of actions that can be performed on a deployment.
230237-- It requests deployment data.
@@ -232,12 +239,11 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $
232239-- otherwise it calls 'actionsTableData', passing the received data.
233240actionsTable ::
234241 MonadWidget t m =>
235− RequestErrorHandler t m ->
236242 -- | Event notifying about the need to update data.
237243 Event t () ->
238244 Dynamic t DeploymentName ->
239245 m ()
240−actionsTable hReq updEv nameDyn = do
246+actionsTable updEv nameDyn = do
241247 pb <- getPostBuild
242248 respEv <- infoEndpoint (Right <$> nameDyn) pb
243249 let okEv = join . fmap logs <$> fmapMaybe reqSuccess respEv
@@ -247,7 +253,7 @@ actionsTable hReq updEv nameDyn = do
247253 widgetHold_ actionsTableLoading $
248254 leftmost
249255 [ actionsTableError <$ errEv
250− , actionsTableData hReq updEv nameDyn <$> okEv
256+ , actionsTableData updEv nameDyn <$> okEv
251257 ]
252258
253259-- | Header of the actions table.
@@ -285,25 +291,24 @@ actionsTableError = do
285291-- It updates data every time when the supplied event fires.
286292actionsTableData ::
287293 MonadWidget t m =>
288− RequestErrorHandler t m ->
289294 -- | Event notifying about the need to update data.
290295 Event t () ->
291296 Dynamic t DeploymentName ->
292297 -- | Initial logs.
293298 [DeploymentLog] ->
294299 m ()
295−actionsTableData hReq updEv nameDyn initLogs = do
300+actionsTableData updEv nameDyn initLogs = do
296301 respEv <- infoEndpoint (Right <$> nameDyn) updEv
297302 let okEv = (>>= logs) <$> fmapMaybe reqSuccess respEv
298303 logsDyn <- holdDyn initLogs okEv
299304 el "tbody" $
300305 void $
301306 simpleList logsDyn $ \logDyn -> do
302− dyn_ $ actinRow hReq <$> logDyn
307+ dyn_ $ actionRow <$> logDyn
303308
304309-- | Data row of the actions table.
305−actinRow :: RequestErrorHandler t m -> MonadWidget t m => DeploymentLog -> m ()
306−actinRow hReq DeploymentLog {..} = do
310+actionRow :: MonadWidget t m => DeploymentLog -> m ()
311+actionRow DeploymentLog {..} = do
307312 el "tr" $ do
308313 el "td" $ do
309314 text $ actionToText action
@@ -311,8 +316,8 @@ actinRow hReq DeploymentLog {..} = do
311316 "status "
312317 <> if exitCode == 0 then "status--success" else "status--failure"
313318 divClass statusClass blank
314− el "td" $ deploymentOverridesWidget hReq deploymentDepOverrides
315− el "td" $ applicationOverridesWidget hReq deploymentDepOverrides deploymentAppOverrides
319+ el "td" $ overridesWidget deploymentDepOverrides
320+ el "td" $ overridesWidget deploymentAppOverrides
316321 el "td" $ text $ showT $ exitCode
317322 el "td" $ text $ formatPosixToDateTime createdAt
318323 el "td" $ text $ formatDuration duration
octopod-frontend/src/Page/Deployments.hs+36−72
@@ -24,6 +24,7 @@ import Common.Utils
2424import Control.Applicative
2525import Control.Monad.Reader
2626import Data.Align
27+import Data.Foldable
2728import Data.Functor
2829import qualified Data.Semigroup as S
2930import qualified Data.Text as T
@@ -79,20 +80,19 @@ deploymentsWidget ::
7980 [DeploymentFullInfo] ->
8081 m ()
8182deploymentsWidget updAllEv dfis = do
82− (showNewDeploymentEv, editEv) <- deploymentsWidgetWrapper $
83− wrapRequestErrors $ \hReq -> mdo
84− pageNotification $
85− leftmost
86− [ DPMError
87− "Deployment list update failed, deployment list\
88− \ may be slightly outdated."
89− <$ errUpdEv
90− , DPMClear <$ okUpdEv
91− ]
92− (showNewDeploymentEv', termDyn') <- deploymentsHeadWidget True okUpdEv
93− termDyn <- debounceDyn 0.3 termDyn'
94− (okUpdEv, errUpdEv, editEv) <- deploymentsListWidget hReq updAllEv termDyn dfis
95− pure (showNewDeploymentEv', deSearch <$> editEv)
83+ (showNewDeploymentEv, editEv) <- deploymentsWidgetWrapper $ mdo
84+ pageNotification $
85+ leftmost
86+ [ DPMError
87+ "Deployment list update failed, deployment list\
88+ \ may be slightly outdated."
89+ <$ errUpdEv
90+ , DPMClear <$ okUpdEv
91+ ]
92+ (showNewDeploymentEv', termDyn') <- deploymentsHeadWidget True okUpdEv
93+ termDyn <- debounceDyn 0.3 termDyn'
94+ (okUpdEv, errUpdEv, editEv) <- deploymentsListWidget updAllEv termDyn dfis
95+ pure (showNewDeploymentEv', deSearch <$> editEv)
9696 newDepEv <- newDeploymentPopup showNewDeploymentEv never
9797 setRoute $ newDepEv <&> \newDep -> DashboardRoute :/ Just (newDep ^. #name)
9898
@@ -178,13 +178,12 @@ deploymentsListWidget ::
178178 , SetRoute t (R Routes) m
179179 , MonadReader ProjectConfig m
180180 ) =>
181− RequestErrorHandler t m ->
182181 Event t () ->
183182 Dynamic t Text ->
184183 -- | Initial deployment data
185184 [DeploymentFullInfo] ->
186185 m (Event t (), Event t (), Event t SearchedDeploymentInfo)
187−deploymentsListWidget hReq updAllEv termDyn ds = dataWidgetWrapper $ mdo
186+deploymentsListWidget updAllEv termDyn ds = dataWidgetWrapper $ mdo
188187 retryEv <- delay 10 errUpdEv
189188 updRespEv <- listEndpoint $ leftmost [updAllEv, () <$ retryEv]
190189 let okUpdEv = fmapMaybe reqSuccess updRespEv
@@ -197,9 +196,8 @@ deploymentsListWidget hReq updAllEv termDyn ds = dataWidgetWrapper $ mdo
197196 let (archivedDsDyn, activeDsDyn) =
198197 splitDynPure $ L.partition isDeploymentArchived <$> searchedDyn
199198 searchSorting = termDyn $> Nothing
200− clickedEv <- elementClick
201− editEv <- activeDeploymentsWidget hReq searchSorting clickedEv activeDsDyn
202− archivedDeploymentsWidget hReq searchSorting clickedEv archivedDsDyn
199+ editEv <- activeDeploymentsWidget searchSorting activeDsDyn
200+ archivedDeploymentsWidget searchSorting archivedDsDyn
203201 pure (() <$ okUpdEv, () <$ errUpdEv, editEv)
204202
205203type SearchedDeploymentInfo = DeploymentFullInfo' SearchResult
@@ -210,16 +208,12 @@ activeDeploymentsWidget ::
210208 , SetRoute t (R Routes) m
211209 , MonadReader ProjectConfig m
212210 ) =>
213− RequestErrorHandler t m ->
214211 Dynamic t (Maybe (SortDir DeploymentFullInfo)) ->
215− -- | Event that carries the clicked DOM element. This event is required by
216− -- `dropdownWidget'`.
217− Event t ClickedElement ->
218212 Dynamic t [SearchedDeploymentInfo] ->
219213 -- | Returns an event carrying editable deployment
220214 -- to \"edit deployment\" sidebar.
221215 m (Event t SearchedDeploymentInfo)
222−activeDeploymentsWidget hReq searchSorting clickedEv dsDyn =
216+activeDeploymentsWidget searchSorting dsDyn =
223217 divClass "data__primary" $
224218 tableWrapper (updated searchSorting $> SortingChanged) $ \sortDyn -> do
225219 sorting <- holdDyn Nothing (mergeWith (<|>) [updated sortDyn, updated searchSorting])
@@ -230,7 +224,7 @@ activeDeploymentsWidget hReq searchSorting clickedEv dsDyn =
230224 dyn $
231225 emptyDyn <&> \case
232226 False -> do
233− editEvs <- simpleList dsSortedDyn (activeDeploymentWidget hReq clickedEv)
227+ editEvs <- simpleList dsSortedDyn activeDeploymentWidget
234228 pure $ switchDyn $ leftmost <$> editEvs
235229 True -> do
236230 emptyTableBody noDeploymentsWidget
@@ -250,15 +244,11 @@ activeDeploymentWidget ::
250244 , SetRoute t (R Routes) m
251245 , MonadReader ProjectConfig m
252246 ) =>
253− RequestErrorHandler t m ->
254− -- | Event that carries the clicked DOM element. This event is required by
255− -- `dropdownWidget'`.
256− Event t ClickedElement ->
257247 Dynamic t SearchedDeploymentInfo ->
258248 -- | Returns event carrying editable deployment
259249 -- that is required by \"edit deployment\" sidebar.
260250 m (Event t SearchedDeploymentInfo)
261−activeDeploymentWidget hReq clickedEv dDyn' = do
251+activeDeploymentWidget dDyn' = do
262252 dDyn <- holdUniqDyn dDyn'
263253 editEvEv <- dyn $
264254 ffor dDyn $ \d@DeploymentFullInfo {..} -> do
@@ -270,27 +260,17 @@ activeDeploymentWidget hReq clickedEv dDyn' = do
270260 statusWidget $ constDyn status
271261 el "td" $
272262 divClass "listing" $
273− forM_ (unDeploymentMetadata metadata) (renderMetadataLink . pure)
263+ for_ (unDeploymentMetadata metadata) (renderMetadataLink . pure)
274264 el "td" $
275− deploymentOverridesWidgetSearched hReq (deployment ^. field @"deploymentOverrides" . coerced)
265+ overridesWidget (deployment ^. field @"deploymentOverrides" . coerced)
276266 el "td" $
277− applicationOverridesWidgetSearched
278− hReq
279− (deployment ^. field @"deploymentOverrides" . coerced)
280− (deployment ^. field @"appOverrides" . coerced)
267+ overridesWidget (deployment ^. field @"appOverrides" . coerced)
281268 el "td" $
282269 text $ formatPosixToDate createdAt
283270 el "td" $
284271 text $ formatPosixToDate updatedAt
285272 el "td" $ do
286273 let enabled = not . isPending . recordedStatus $ status
287− btn =
288− elAttr
289− "button"
290− ( "class" =: "drop__handler"
291− <> "type" =: "button"
292− )
293− $ text "Actions"
294274 body = do
295275 btnEditEv <-
296276 actionButton $
@@ -315,13 +295,13 @@ activeDeploymentWidget hReq clickedEv dDyn' = do
315295 def
316296 { buttonText = "Details"
317297 , buttonType = Just LogsActionButtonType
318− , buttonBaseTag = ATag url
298+ , buttonBaseTag = ATag $ pure url
319299 }
320300 )
321301 pure $
322302 leftmost
323303 [ArchiveDeployment <$ btnArcEv, EditDeployment <$ btnEditEv]
324− dropdownWidget' clickedEv btn body
304+ dropdownWidget body
325305 let archEv = () <$ ffilter (is (_Ctor @"ArchiveDeployment")) dropdownEv
326306 editEv = d <$ ffilter (is (_Ctor @"EditDeployment")) dropdownEv
327307 delEv <- confirmArchivePopup archEv $ do
@@ -330,7 +310,7 @@ activeDeploymentWidget hReq clickedEv dDyn' = do
330310 text $ unDeploymentName dName <> " deployment?"
331311 void $ archiveEndpoint (constDyn . Right $ dName) delEv
332312 let route = DashboardRoute :/ Just dName
333− setRoute $ route <$ domEvent Dblclick linkEl
313+ setRoute $ route <$ domEvent Click linkEl
334314 pure editEv
335315 switchHold never editEvEv
336316
@@ -340,14 +320,10 @@ archivedDeploymentsWidget ::
340320 ( MonadWidget t m
341321 , SetRoute t (R Routes) m
342322 ) =>
343− RequestErrorHandler t m ->
344323 Dynamic t (Maybe (SortDir DeploymentFullInfo)) ->
345− -- | Event that carries the clicked DOM element. This event is required by
346− -- `dropdownWidget'`.
347− Event t ClickedElement ->
348324 Dynamic t [SearchedDeploymentInfo] ->
349325 m ()
350−archivedDeploymentsWidget hReq searchSorting clickedEv dsDyn = do
326+archivedDeploymentsWidget searchSorting dsDyn = do
351327 showDyn <- toggleButton
352328 let classDyn = ffor showDyn $ \case
353329 True -> "data__archive data__archive--open"
@@ -364,7 +340,7 @@ archivedDeploymentsWidget hReq searchSorting clickedEv dsDyn = do
364340 void $
365341 simpleList
366342 dsSortedDyn
367− (archivedDeploymentWidget hReq clickedEv)
343+ archivedDeploymentWidget
368344 True -> emptyTableBody noDeploymentsWidget
369345
370346-- | Row with archived deployment.
@@ -372,11 +348,9 @@ archivedDeploymentWidget ::
372348 ( MonadWidget t m
373349 , SetRoute t (R Routes) m
374350 ) =>
375− RequestErrorHandler t m ->
376− Event t ClickedElement ->
377351 Dynamic t SearchedDeploymentInfo ->
378352 m ()
379−archivedDeploymentWidget hReq clickedEv dDyn' = do
353+archivedDeploymentWidget dDyn' = do
380354 dDyn <- holdUniqDyn dDyn'
381355 dyn_ $
382356 ffor dDyn $ \d@DeploymentFullInfo {..} -> do
@@ -388,34 +362,24 @@ archivedDeploymentWidget hReq clickedEv dDyn' = do
388362 statusWidget (pure status)
389363 el "td" $ text "..."
390364 el "td" $
391− deploymentOverridesWidgetSearched hReq (deployment ^. field @"deploymentOverrides" . coerced)
365+ overridesWidget (deployment ^. field @"deploymentOverrides" . coerced)
392366 el "td" $
393− applicationOverridesWidgetSearched
394− hReq
395− (deployment ^. field @"deploymentOverrides" . coerced)
396− (deployment ^. field @"appOverrides" . coerced)
367+ overridesWidget (deployment ^. field @"appOverrides" . coerced)
397368 el "td" $
398369 text $ formatPosixToDate createdAt
399370 el "td" $
400371 text $ formatPosixToDate updatedAt
401372 el "td" $ do
402− let btn =
403− elAttr
404− "button"
405− ( "class" =: "drop__handler"
406− <> "type" =: "button"
407− )
408− $ text "Actions"
409− body =
373+ let body =
410374 actionButton
411375 def
412376 { buttonText = "Restore from archive"
413377 , buttonType = Just ArchiveActionButtonType
414378 }
415− btnEv <- dropdownWidget' clickedEv btn body
416− void $ restoreEndpoint (constDyn $ Right $ dName) btnEv
379+ btnEv <- dropdownWidget body
380+ void $ restoreEndpoint (constDyn $ Right $ dName) (btnEv $> ())
417381 let route = DashboardRoute :/ Just dName
418− setRoute $ route <$ domEvent Dblclick linkEl
382+ setRoute $ route <$ domEvent Click linkEl
419383
420384-- | Sort deployments by the supplied condition.
421385sortDeployments ::
@@ -561,7 +525,7 @@ tableWrapper ::
561525 (Dynamic t (Maybe (SortDir DeploymentFullInfo)) -> m a) ->
562526 m a
563527tableWrapper sChanged ma =
564− divClass "table table--deployments table--clickable table--double-click" $
528+ divClass "table table--deployments table--clickable" $
565529 el "table" $ mdo
566530 ((), sDyn') <- runSortableTableGroup sChanged tableHeader
567531 sDyn <- holdDyn Nothing $ Just <$> sDyn'
octopod-frontend/src/Page/Elements/Links.hs+20−11
@@ -4,8 +4,11 @@ module Page.Elements.Links
44where
55
66import Common.Types
7+import Common.Utils ((<^.>))
78import Control.Lens
9+import Control.Monad
810import qualified Data.Text as T
11+import Frontend.UIKit.Button.Common
912import Reflex.Dom
1013
1114renderMetadataLink ::
@@ -13,14 +16,20 @@ renderMetadataLink ::
1316 Dynamic t DeploymentMetadatum ->
1417 m ()
1518renderMetadataLink metadataD = do
16− let attrDyn =
17− metadataD <&> \metadata ->
18− "class" =: "listing__item external bar bar--larger"
19− <> "href" =: metadata ^. #link
20− <> "target" =: "_blank"
21− elDynAttr "a" attrDyn . dynText $
22− metadataD <&> \case
23− -- If the name is empty, then use the url
24− DeploymentMetadatum {name = name}
25− | (not . T.null . T.strip) name -> name
26− DeploymentMetadatum {link = url} -> url
19+ void $
20+ buttonEl
21+ CommonButtonConfig
22+ { constantClasses = pure $ "listing__item" <> "external" <> "bar" <> "bar--larger"
23+ , enabledClasses = mempty
24+ , disabledClasses = "button--disabled"
25+ , buttonEnabled = pure True
26+ , buttonText =
27+ TextBuilder $
28+ dynText $
29+ metadataD <&> \case
30+ -- If the name is empty, then use the url
31+ DeploymentMetadatum {name = name}
32+ | (not . T.null . T.strip) name -> name
33+ DeploymentMetadatum {link = url} -> url
34+ , buttonBaseTag = ATag $ metadataD <^.> #link
35+ }
octopod-frontend/src/Page/Popup/EditDeployment.hs+1−1
@@ -88,7 +88,7 @@ editDeploymentPopupHeader dname validDyn loadingDyn =
8888 )
8989
9090 divClass "popup__menu drop drop--actions" blank
91− pure (closeEv, saveEv)
91+ pure (closeEv $> (), saveEv $> ())
9292
9393-- | The body of the sidebar containing the edit form. Contains a tag field and
9494-- an override field.
octopod-frontend/src/Page/Popup/NewDeployment.hs+1−1
@@ -83,7 +83,7 @@ newDeploymentPopupHeader enabledDyn loadingDyn =
8383 True -> Just LoadingLargeButtonType
8484 )
8585 divClass "popup__menu drop drop--actions" blank
86− pure (closeEv, saveEv)
86+ pure (closeEv $> (), saveEv $> ())
8787
8888-- | The body of the sidebar contains the creation form. It contains: a name field,
8989-- a tag field and overrides fields. The name field is validated with the regexp:
stack.yaml+3−1
@@ -1,4 +1,4 @@
1−resolver: lts-18.2
1+resolver: lts-18.28
22
33packages:
44 - octo-cli
@@ -10,3 +10,5 @@ extra-deps:
1010 - servant-websockets-2.0.0
1111 - table-layout-0.9.1.0
1212 - data-default-instances-base-0.1.0.1
13+ - git: https://github.com/typeable/ordered-containers.git
14+ commit: 3eb05fb2f44fe482e9092aff447e9c68fbd6a7f6