git.iko.soy/typeable/octopod
on master help sign in
Implemented new override markup (#106)

* Added Frontend.UIKit * Extracted some things into UIKit * Override editing markup * Added deployment page main override markup * Made override size configurable * Did override listing * Deployment overrides are now first everywhere * Really hacky keys search * Button loading state * Patched project home page

commit68aaecc110c0bdb89454fcf5e61e63faeb191520 authoriko <ilyakooo0@gmail.com> dateTue 21 Sep 2021, 14:41 (5 years ago) parent726e4a6
26 files changed+1370−545
.hlint.yaml+55 charts/octopod/Chart.yaml+1−1 charts/octopod/README.md+1−1 octopod-css/development/layouts/staging.html+9 octopod-css/development/markups/_dash.html+1−1 octopod-css/development/styles/_listing.css+44 octopod-css/package.json+1−1 octopod-css/yarn.lock+67−67 octopod-frontend/octopod-frontend.cabal+10 octopod-frontend/src/Data/Text/Search.hs+13 octopod-frontend/src/Frontend/Classes.hs+18 octopod-frontend/src/Frontend/UIKit.hs+318 octopod-frontend/src/Frontend/UIKit/Button/Action.hs+63 octopod-frontend/src/Frontend/UIKit/Button/Common.hs+49 octopod-frontend/src/Frontend/UIKit/Button/Dash.hs+71 octopod-frontend/src/Frontend/UIKit/Button/Expander.hs+85 octopod-frontend/src/Frontend/UIKit/Button/Large.hs+96 octopod-frontend/src/Frontend/UIKit/Button/Sort.hs+58 octopod-frontend/src/Frontend/UIKit/Button/Static.hs+71 octopod-frontend/src/Frontend/Utils.hs+166−353 octopod-frontend/src/Main.hs+1−1 octopod-frontend/src/Page/ClassicPopup.hs+13−4 octopod-frontend/src/Page/Deployment.hs+52−63 octopod-frontend/src/Page/Deployments.hs+73−45 octopod-frontend/src/Page/Popup/EditDeployment.hs+18−4 octopod-frontend/src/Page/Popup/NewDeployment.hs+16−4
.hlint.yamlnew file+55
@@ -0,0 +1,55 @@
1+# HLint configuration file
2+# https://github.com/ndmitchell/hlint
3+##########################
4+
5+# This file contains a template configuration file, which is typically
6+# placed as .hlint.yaml in the root of your project
7+
8+# Specify additional command line arguments
9+#
10+# - arguments: [--color, --cpp-simple, -XQuasiQuotes]
11+
12+# Control which extensions/flags/modules/functions can be used
13+#
14+# - extensions:
15+# - default: false # all extension are banned by default
16+# - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
17+# - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
18+#
19+# - flags:
20+# - {name: -w, within: []} # -w is allowed nowhere
21+#
22+# - modules:
23+# - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
24+# - {name: Control.Arrow, within: []} # Certain modules are banned entirely
25+#
26+# - functions:
27+# - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules
28+
29+# Add custom hints for this project
30+#
31+# Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
32+# - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}
33+
34+# Turn on hints that are off by default
35+#
36+# Ban "module X(module X) where", to require a real export list
37+# - warn: {name: Use explicit module export list}
38+#
39+# Replace a $ b $ c with a . b $ c
40+# - group: {name: dollar, enabled: true}
41+#
42+# Generalise map to fmap, ++ to <>
43+# - group: {name: generalise, enabled: true}
44+
45+# Ignore some builtin hints
46+# - ignore: {name: Use let}
47+# - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
48+
49+- ignore: { name: Replace case with maybe }
50+- ignore: { name: Use if }
51+# Define some custom infix operators
52+# - fixity: infixr 3 ~^#^~
53+
54+# To generate a suitable file for HLint do:
55+# $ hlint --default > .hlint.yaml
charts/octopod/Chart.yaml+1−1
@@ -7,7 +7,7 @@ appVersion: 1.3.1
77keywords:
88 - kubernetes
99 - octopod
10−home: https://octopod.site
10+home: https://github.com/typeable/octopod
1111sources:
1212 - https://github.com/typeable/octopod
1313maintainers:
charts/octopod/README.md+1−1
@@ -1,6 +1,6 @@
11# Octopod
22
3−[Octopod](https://octopod.site/) is a fully open-source self-hosted solution for managing multiple deployments in a Kubernetes cluster with a user-friendly web interface. Managing deployments does not require any technical expertise.
3+[Octopod](https://github.com/typeable/octopod) is a fully open-source self-hosted solution for managing multiple deployments in a Kubernetes cluster with a user-friendly web interface. Managing deployments does not require any technical expertise.
44
55## TL;DR
66```console
octopod-css/development/layouts/staging.html+9
@@ -127,6 +127,15 @@
127127 <span class="listing__key default">SECRET_CODE:</span>
128128 <span class="listing__value">asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen</span>
129129 </div>
130+ <div class="listing__item deleted">
131+ <span class="listing__key default">SECRET_CODE:</span>
132+ <span class="listing__value default">asdfaisdjri235868ear7%lorem-ipsum-dolor-sit-amen</span>
133+ </div>
134+ <div class="listing__item deleted">
135+ <span class="listing__key default">SECRET_CODE:</span>
136+ <div class="listing__placeholder"></div>
137+ <div class="listing__spinner"></div>
138+ </div>
130139 <div class="listing__item">
131140 <span class="listing__key default">INFO:</span>
132141 <span class="listing__value default">New Project Staging</span>
octopod-css/development/markups/_dash.html+1−1
@@ -32,7 +32,7 @@
3232 <br>
3333 <br>
3434 <br>
35− <h2>.dash--add</h2>
35+ <h2>.dash--back</h2>
3636
3737 <button class="dash dash--back" type="button">
3838 All deployments
octopod-css/development/styles/_listing.css+44
@@ -38,6 +38,10 @@
3838 text-overflow: ellipsis;
3939 }
4040
41+ .listing--for-text .deleted > span {
42+ text-decoration-line: line-through;
43+ opacity: 0.2;
44+ }
4145 .listing--for-text .listing__item:first-child {
4246 margin-top: 0;
4347 }
@@ -59,6 +63,26 @@
5963 font-style: italic;
6064 }
6165
66+ .listing__placeholder {
67+ content: "";
68+ display: inline-block;
69+ height: 14px;
70+ width: 140px;
71+ border-radius: 2px;
72+ background: rgba(143, 143, 143, 0.2);
73+ margin-right: 4px;
74+ margin-top: 3px;
75+ }
76+
77+ .listing__placeholder__value {
78+ width: 70px;
79+ }
80+
81+ .listing--info-text {
82+ font-style: italic;
83+ color: rgba(143, 143, 143);
84+ }
85+
6286 .listing--for-text .listing__more {
6387 margin-top: 8px;
6488 }
@@ -68,3 +92,23 @@
6892 font-size: 16px;
6993 line-height: 24px;
7094}
95+
96+.listing__spinner {
97+ display: inline-block;
98+ vertical-align: top;
99+ border: none;
100+ background-color: transparent;
101+ background-repeat: no-repeat;
102+ background-position: center center;
103+ background-size: 20px 20px;
104+ padding: 0;
105+ width: 20px;
106+ height: 20px;
107+ cursor: pointer;
108+ text-indent: 200%;
109+ white-space: nowrap;
110+ overflow: hidden;
111+ opacity: 1;
112+ background-image: url("../vectors/spot-loader.svg");
113+ animation: loading 2.2s linear infinite;
114+}
octopod-css/package.json+1−1
@@ -6,7 +6,7 @@
66 "gulp": "3.9.1",
77 "gulp-base64": "0.1.3",
88 "gulp-change": "1.0.0",
9− "gulp-clean-css": "2.0.6",
9+ "gulp-clean-css": "4.2.0",
1010 "gulp-plumber": "1.1.0",
1111 "gulp-postcss": "6.1.0",
1212 "gulp-size": "2.1.0",
octopod-css/yarn.lock+67−67
@@ -30,6 +30,13 @@ amdefine@>=0.0.4:
3030 resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
3131 integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
3232
33+ansi-colors@^1.0.1:
34+ version "1.1.0"
35+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9"
36+ integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==
37+ dependencies:
38+ ansi-wrap "^0.1.0"
39+
3340ansi-gray@^0.1.1:
3441 version "0.1.1"
3542 resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251"
@@ -66,7 +73,7 @@ ansi-styles@^4.1.0:
6673 dependencies:
6774 color-convert "^2.0.1"
6875
69−ansi-wrap@0.1.0:
76+ansi-wrap@0.1.0, ansi-wrap@^0.1.0:
7077 version "0.1.0"
7178 resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
7279 integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768=
@@ -344,13 +351,6 @@ buffers@~0.1.1:
344351 resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
345352 integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s=
346353
347−bufferstreams@1.0.1:
348− version "1.0.1"
349− resolved "https://registry.yarnpkg.com/bufferstreams/-/bufferstreams-1.0.1.tgz#cfb1ad9568d3ba3cfe935ba9abdd952de88aab2a"
350− integrity sha1-z7GtlWjTujz+k1upq92VLeiKqyo=
351− dependencies:
352− readable-stream "^1.0.33"
353−
354354cache-base@^1.0.1:
355355 version "1.0.1"
356356 resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -471,13 +471,12 @@ class-utils@^0.3.5:
471471 isobject "^3.0.0"
472472 static-extend "^0.1.1"
473473
474−clean-css@^3.4.12:
475− version "3.4.28"
476− resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
477− integrity sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=
474+clean-css@4.2.1:
475+ version "4.2.1"
476+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
477+ integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==
478478 dependencies:
479− commander "2.8.x"
480− source-map "0.4.x"
479+ source-map "~0.6.0"
481480
482481cliui@^3.0.3:
483482 version "3.2.0"
@@ -629,13 +628,6 @@ combined-stream@~0.0.4:
629628 dependencies:
630629 delayed-stream "0.0.5"
631630
632−commander@2.8.x:
633− version "2.8.1"
634− resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
635− integrity sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=
636− dependencies:
637− graceful-readlink ">= 1.0.0"
638−
639631component-emitter@^1.2.1:
640632 version "1.3.0"
641633 resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@@ -1531,11 +1523,6 @@ graceful-fs@~1.2.0:
15311523 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364"
15321524 integrity sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=
15331525
1534−"graceful-readlink@>= 1.0.0":
1535− version "1.0.1"
1536− resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
1537− integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
1538−
15391526gulp-base64@0.1.3:
15401527 version "0.1.3"
15411528 resolved "https://registry.yarnpkg.com/gulp-base64/-/gulp-base64-0.1.3.tgz#164bd9d4f336dc16d669b331cebc139343579145"
@@ -1555,17 +1542,15 @@ gulp-change@1.0.0:
15551542 dependencies:
15561543 event-stream "^3.1.7"
15571544
1558−gulp-clean-css@2.0.6:
1559− version "2.0.6"
1560− resolved "https://registry.yarnpkg.com/gulp-clean-css/-/gulp-clean-css-2.0.6.tgz#c664d5e2f19faad7750d62e543b21d07f6aa3c8d"
1561− integrity sha1-xmTV4vGfqtd1DWLlQ7IdB/aqPI0=
1545+gulp-clean-css@4.2.0:
1546+ version "4.2.0"
1547+ resolved "https://registry.yarnpkg.com/gulp-clean-css/-/gulp-clean-css-4.2.0.tgz#915ec258dc6d3e6a50043f610066d5c2eac4f54e"
1548+ integrity sha512-r4zQsSOAK2UYUL/ipkAVCTRg/2CLZ2A+oPVORopBximRksJ6qy3EX1KGrIWT4ZrHxz3Hlobb1yyJtqiut7DNjA==
15621549 dependencies:
1563− clean-css "^3.4.12"
1564− gulp-util "^3.0.7"
1565− object-assign "^4.0.1"
1566− readable-stream "^2.0.6"
1567− vinyl-bufferstream "^1.0.1"
1568− vinyl-sourcemaps-apply "^0.2.1"
1550+ clean-css "4.2.1"
1551+ plugin-error "1.0.1"
1552+ through2 "3.0.1"
1553+ vinyl-sourcemaps-apply "0.2.1"
15691554
15701555gulp-plumber@1.1.0:
15711556 version "1.1.0"
@@ -3108,6 +3093,16 @@ pleeease-filters@^3.0.0:
31083093 onecolor "~2.4.0"
31093094 postcss "^5.0.4"
31103095
3096+plugin-error@1.0.1:
3097+ version "1.0.1"
3098+ resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c"
3099+ integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==
3100+ dependencies:
3101+ ansi-colors "^1.0.1"
3102+ arr-diff "^4.0.0"
3103+ arr-union "^3.1.0"
3104+ extend-shallow "^3.0.2"
3105+
31113106plur@^2.0.0, plur@^2.1.2:
31123107 version "2.1.2"
31133108 resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a"
@@ -3520,6 +3515,15 @@ read-pkg@^1.0.0:
35203515 normalize-package-data "^2.3.2"
35213516 path-type "^1.0.0"
35223517
3518+"readable-stream@2 || 3", readable-stream@^3.1.1:
3519+ version "3.6.0"
3520+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
3521+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
3522+ dependencies:
3523+ inherits "^2.0.3"
3524+ string_decoder "^1.1.1"
3525+ util-deprecate "^1.0.1"
3526+
35233527"readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17, readable-stream@~1.0.33-1:
35243528 version "1.0.34"
35253529 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
@@ -3540,28 +3544,6 @@ readable-stream@^1.0.33, readable-stream@~1.1.9:
35403544 isarray "0.0.1"
35413545 string_decoder "~0.10.x"
35423546
3543−readable-stream@^2.0.6, readable-stream@~2.3.6:
3544− version "2.3.7"
3545− resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
3546− integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
3547− dependencies:
3548− core-util-is "~1.0.0"
3549− inherits "~2.0.3"
3550− isarray "~1.0.0"
3551− process-nextick-args "~2.0.0"
3552− safe-buffer "~5.1.1"
3553− string_decoder "~1.1.1"
3554− util-deprecate "~1.0.1"
3555−
3556−readable-stream@^3.1.1:
3557− version "3.6.0"
3558− resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
3559− integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
3560− dependencies:
3561− inherits "^2.0.3"
3562− string_decoder "^1.1.1"
3563− util-deprecate "^1.0.1"
3564−
35653547readable-stream@~2.0.0:
35663548 version "2.0.6"
35673549 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
@@ -3574,6 +3556,19 @@ readable-stream@~2.0.0:
35743556 string_decoder "~0.10.x"
35753557 util-deprecate "~1.0.1"
35763558
3559+readable-stream@~2.3.6:
3560+ version "2.3.7"
3561+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
3562+ integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
3563+ dependencies:
3564+ core-util-is "~1.0.0"
3565+ inherits "~2.0.3"
3566+ isarray "~1.0.0"
3567+ process-nextick-args "~2.0.0"
3568+ safe-buffer "~5.1.1"
3569+ string_decoder "~1.1.1"
3570+ util-deprecate "~1.0.1"
3571+
35773572rechoir@^0.6.2:
35783573 version "0.6.2"
35793574 resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
@@ -3900,7 +3895,7 @@ source-map-url@^0.4.0:
39003895 resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
39013896 integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
39023897
3903−source-map@0.4.x, source-map@^0.4.2:
3898+source-map@^0.4.2:
39043899 version "0.4.4"
39053900 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
39063901 integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
@@ -3912,6 +3907,11 @@ source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6:
39123907 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
39133908 integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
39143909
3910+source-map@~0.6.0:
3911+ version "0.6.1"
3912+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
3913+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
3914+
39153915sparkles@^1.0.0:
39163916 version "1.0.1"
39173917 resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c"
@@ -4248,6 +4248,13 @@ text-table@^0.2.0:
42484248 resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
42494249 integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
42504250
4251+through2@3.0.1:
4252+ version "3.0.1"
4253+ resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a"
4254+ integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==
4255+ dependencies:
4256+ readable-stream "2 || 3"
4257+
42514258through2@^0.6.1, through2@^0.6.3, through2@~0.6.1:
42524259 version "0.6.5"
42534260 resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
@@ -4452,13 +4459,6 @@ verror@1.10.0:
44524459 core-util-is "1.0.2"
44534460 extsprintf "^1.2.0"
44544461
4455−vinyl-bufferstream@^1.0.1:
4456− version "1.0.1"
4457− resolved "https://registry.yarnpkg.com/vinyl-bufferstream/-/vinyl-bufferstream-1.0.1.tgz#0537869f580effa4ca45acb47579e4b9fe63081a"
4458− integrity sha1-BTeGn1gO/6TKRay0dXnkuf5jCBo=
4459− dependencies:
4460− bufferstreams "1.0.1"
4461−
44624462vinyl-file@~1.3.0:
44634463 version "1.3.0"
44644464 resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-1.3.0.tgz#aa05634d3a867ba91447bedbb34afcb26f44f6e7"
@@ -4483,7 +4483,7 @@ vinyl-fs@^0.3.0:
44834483 through2 "^0.6.1"
44844484 vinyl "^0.4.0"
44854485
4486−vinyl-sourcemaps-apply@^0.2.0, vinyl-sourcemaps-apply@^0.2.1:
4486+vinyl-sourcemaps-apply@0.2.1, vinyl-sourcemaps-apply@^0.2.0:
44874487 version "0.2.1"
44884488 resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705"
44894489 integrity sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=
octopod-frontend/octopod-frontend.cabal+10
@@ -41,6 +41,15 @@ executable frontend
4141 , Data.Text.Search
4242 , Data.UniqMap
4343 , Data.WorkingOverrides
44+ , Frontend.UIKit
45+ , Frontend.UIKit.Button.Large
46+ , Frontend.UIKit.Button.Dash
47+ , Frontend.Classes
48+ , Frontend.UIKit.Button.Common
49+ , Frontend.UIKit.Button.Expander
50+ , Frontend.UIKit.Button.Static
51+ , Frontend.UIKit.Button.Action
52+ , Frontend.UIKit.Button.Sort
4453 ghc-options:
4554 -Weverything
4655 -Wno-implicit-prelude
@@ -113,5 +122,6 @@ executable frontend
113122 , ordered-containers
114123 , witherable
115124 , reflex
125+ , data-default
116126 hs-source-dirs: src
117127 default-language: Haskell2010
octopod-frontend/src/Data/Text/Search.hs+13
@@ -1,5 +1,6 @@
11module Data.Text.Search
22 ( fuzzySearch,
3+ fuzzySearchMany,
34 FuzzySearchStringChunk (..),
45 )
56where
@@ -8,7 +9,9 @@ import Control.Applicative
89import Data.Bifunctor
910import Data.Char
1011import Data.Function
12+import Data.Functor
1113import qualified Data.List as L
14+import Data.Maybe (mapMaybe)
1215import Data.Text (Text)
1316import qualified Data.Text as T
1417
@@ -44,5 +47,15 @@ fuzzySearch n h = case fuzzySearch' n h 0 0 of
4447 [] -> Nothing
4548 xs@(_ : _) -> Just . (first . fmap . fmap) T.pack $ L.maximumBy (compare `on` snd) xs
4649
50+fuzzySearchMany :: Needle -> [Haystack] -> [(Haystack, [FuzzySearchStringChunk Text])]
51+fuzzySearchMany needle haystacks =
52+ fmap fst . L.sortOn snd $
53+ mapMaybe
54+ ( \haystack ->
55+ fuzzySearch needle haystack
56+ <&> \(res, score) -> ((haystack, res), score)
57+ )
58+ haystacks
59+
4760data FuzzySearchStringChunk a = NotMatched !a | Matched !a
4861 deriving stock (Show, Eq, Functor)
octopod-frontend/src/Frontend/Classes.hsnew file+18
@@ -0,0 +1,18 @@
1+module Frontend.Classes
2+ ( Classes,
3+ destructClasses,
4+ )
5+where
6+
7+import Data.String
8+import Data.Text (Text)
9+import qualified Data.Text as T
10+
11+newtype Classes = Classes [Text]
12+ deriving newtype (Semigroup, Monoid)
13+
14+instance IsString Classes where
15+ fromString s = Classes [fromString s]
16+
17+destructClasses :: Classes -> Text
18+destructClasses (Classes cs) = T.unwords cs
octopod-frontend/src/Frontend/UIKit.hsnew file+318
@@ -0,0 +1,318 @@
1+-- | Most of HTML details should be implemented here with a domain API.
2+module Frontend.UIKit
3+ ( deploymentSection,
4+ loadingCommonWidget,
5+ errorCommonWidget,
6+ octopodTextInput',
7+ loadingOverride,
8+ loadingOverrides,
9+ overrideField,
10+ OverrideField (..),
11+ popupOverlay,
12+ Default (..),
13+ module X,
14+ (.~~),
15+ (?~~),
16+ OverrideFieldType (..),
17+ deletedOverride,
18+ showNonEditableWorkingOverride,
19+ NonEditableWorkingOverrideStyle (..),
20+ )
21+where
22+
23+import Control.Lens
24+import Control.Monad
25+import Control.Monad.IO.Class
26+import Data.Align
27+import Data.Default
28+import Data.Functor
29+import Data.Generics.Labels ()
30+import qualified Data.Map as M
31+import Data.Text (Text)
32+import Data.Text.Search
33+import Data.These
34+import Data.WorkingOverrides
35+import Frontend.Classes as X
36+import Frontend.UIKit.Button.Action as X
37+import Frontend.UIKit.Button.Dash as X
38+import Frontend.UIKit.Button.Expander as X
39+import Frontend.UIKit.Button.Large as X
40+import Frontend.UIKit.Button.Sort as X
41+import Frontend.UIKit.Button.Static as X
42+import GHC.Generics (Generic)
43+import Reflex.Dom
44+import Reflex.Network
45+
46+(.~~) :: ASetter' s a -> a -> s -> s
47+(.~~) = (.~)
48+
49+(?~~) :: ASetter' s (Maybe a) -> a -> s -> s
50+(?~~) = (?~)
51+
52+deploymentSection :: DomBuilder t m => Text -> m a -> m a
53+deploymentSection n m = elClass "section" "deployment__section" $ do
54+ elClass "h3" "deployment__sub-heading" $ text n
55+ elDiv "deployment__widget" m
56+
57+-- | Widget with a loading spinner.
58+loadingCommonWidget :: MonadWidget t m => m ()
59+loadingCommonWidget =
60+ divClass "loading loading--enlarged loading--alternate" $
61+ text "Loading..."
62+
63+-- | Widget with an error message.
64+errorCommonWidget :: MonadWidget t m => m ()
65+errorCommonWidget =
66+ divClass "null null--data" $
67+ divClass "null__content" $ do
68+ elClass "b" "null__heading" $ text "Cannot retrieve the data"
69+ divClass "null__message" $ text "Try to reload the page"
70+
71+data OverrideField t = OverrideField
72+ { fieldValue :: Dynamic t Text
73+ , fieldError :: Event t Text
74+ , fieldDisabled :: Dynamic t Bool
75+ , fieldType :: Dynamic t OverrideFieldType
76+ }
77+ deriving stock (Generic)
78+
79+data OverrideFieldType
80+ = DefaultOverrideFieldType
81+ | EditedOverrideFieldType
82+
83+overrideFieldTypeClasses :: OverrideFieldType -> Classes
84+overrideFieldTypeClasses DefaultOverrideFieldType = "input--default"
85+overrideFieldTypeClasses EditedOverrideFieldType = mempty
86+
87+overrideField :: MonadWidget t m => Dynamic t [Text] -> OverrideField t -> OverrideField t -> m (Dynamic t Text, Dynamic t Text, Event t ())
88+overrideField overrideKeyValues keyDyn valueDyn = do
89+ elDiv "overrides__item" $ do
90+ (keyInp, _) <-
91+ octopodTextInput'
92+ overrideKeyValues
93+ (keyDyn ^. #fieldDisabled)
94+ ( do
95+ t <- keyDyn ^. #fieldType
96+ pure $ "overrides__key" <> overrideFieldTypeClasses t
97+ )
98+ "key"
99+ (keyDyn ^. #fieldValue)
100+ (keyDyn ^. #fieldError)
101+ let keyTextDyn = value keyInp
102+ (value -> valTextDyn, _) <-
103+ octopodTextInput'
104+ (pure [])
105+ (valueDyn ^. #fieldDisabled)
106+ ( do
107+ t <- keyDyn ^. #fieldType
108+ pure $ "overrides__value" <> overrideFieldTypeClasses t
109+ )
110+ "value"
111+ (valueDyn ^. #fieldValue)
112+ (valueDyn ^. #fieldError)
113+ closeEv <- deleteOverrideButton
114+ pure (keyTextDyn, valTextDyn, closeEv)
115+
116+deletedOverride :: MonadWidget t m => Text -> Maybe Text -> m (Event t ())
117+deletedOverride k vM = do
118+ elDiv "overrides__item" $ do
119+ elDiv "overrides__key input input--deleted" $ field k
120+ case vM of
121+ Nothing -> do
122+ elDiv "overrides__placeholder overrides__value" blank
123+ loadingOverrideSpinner
124+ pure never
125+ Just v -> do
126+ elDiv "overrides__value input input--deleted" $ field $ v
127+ undoOverrideButton
128+ where
129+ field t =
130+ void $
131+ inputElement $
132+ def
133+ & inputElementConfig_initialValue .~ t
134+ & initialAttributes .~ ("type" =: "text" <> "class" =: "input__widget")
135+
136+loadingOverrideField :: MonadWidget t m => m ()
137+loadingOverrideField = elDiv "overrides__placeholder" blank
138+
139+loadingOverrideSpinner :: MonadWidget t m => m ()
140+loadingOverrideSpinner = elDiv "overrides__delete spot spot--loader" blank
141+
142+loadingOverride :: MonadWidget t m => m ()
143+loadingOverride = do
144+ elDiv "overrides__item loader" $ do
145+ loadingOverrideField
146+ loadingOverrideField
147+ loadingOverrideSpinner
148+
149+loadingOverrides :: MonadWidget t m => m ()
150+loadingOverrides = do
151+ loadingOverride
152+ loadingOverride
153+ loadingOverride
154+
155+elDiv :: DomBuilder t m => Text -> m a -> m a
156+elDiv = elClass "div"
157+
158+-- | The only text input field that is used in project forms. This input
159+-- provides automatic error message hiding after user starts typing.
160+octopodTextInput' ::
161+ MonadWidget t m =>
162+ -- | Value to suggest
163+ (Dynamic t [Text]) ->
164+ -- | Disabled?
165+ Dynamic t Bool ->
166+ -- | Input field classes.
167+ Dynamic t Classes ->
168+ -- | Placeholder for input field.
169+ Text ->
170+ -- | Init value.
171+ (Dynamic t Text) ->
172+ -- | Event carrying the error message.
173+ Event t Text ->
174+ m (InputElement EventResult GhcjsDomSpace t, Dynamic t Bool)
175+octopodTextInput' valuesDyn disabledDyn clssDyn placeholder inValDyn' errEv = mdo
176+ inValDyn <- holdUniqDyn inValDyn'
177+ let valDyn = value inp
178+ inValEv =
179+ align (leftmost [selectedValue, updated inValDyn]) (updated valDyn)
180+ & fmapMaybe
181+ ( \case
182+ This x -> Just x
183+ These inV currV | inV /= currV -> Just inV
184+ _ -> Nothing
185+ )
186+ isValid <-
187+ holdDyn True $
188+ leftmost
189+ [ False <$ errEv
190+ , True <$ updated valDyn
191+ ]
192+
193+ errorClassesDyn <-
194+ holdDyn mempty $
195+ leftmost
196+ [ "input--error" <$ errEv
197+ , mempty <$ updated valDyn
198+ ]
199+
200+ let classDyn = do
201+ errClasses <- errorClassesDyn
202+ additionalClasses <- clssDyn
203+ pure . destructClasses $ "input" <> errClasses <> additionalClasses
204+
205+ inVal <- sample . current $ inValDyn
206+ disabled <- sample . current $ disabledDyn
207+ (inp, selectedValue) <- elDynClass "div" classDyn $ do
208+ inp' <-
209+ inputElement $
210+ def
211+ & initialAttributes
212+ .~ ( "type" =: "text"
213+ <> "class" =: "input__widget"
214+ <> "placeholder" =: placeholder
215+ )
216+ & inputElementConfig_setValue .~ inValEv
217+ & inputElementConfig_initialValue .~ inVal
218+ & inputElementConfig_elementConfig . elementConfig_initialAttributes
219+ %~ (if disabled then M.insert "disabled" "disabled" else id)
220+ & inputElementConfig_elementConfig . elementConfig_modifyAttributes
221+ <>~ updated
222+ ( do
223+ disabled' <- disabledDyn
224+ pure $
225+ M.singleton "disabled" $
226+ if disabled' then Just "disabled" else Nothing
227+ )
228+ widgetHold_ blank $
229+ leftmost
230+ [ divClass "input__output" . text <$> errEv
231+ , blank <$ updated valDyn
232+ ]
233+ delayedFalseFocus <- delayFalse $ _inputElement_hasFocus inp'
234+ selectedValue' <- networkView >=> switchHoldPromptly never $ do
235+ hasFocus <- delayedFalseFocus
236+ values <- valuesDyn
237+ case hasFocus of
238+ True | (_ : _) <- values -> do
239+ currVal <- valDyn
240+ case fuzzySearchMany currVal values of
241+ [] -> pure $ pure never
242+ ress ->
243+ pure $ do
244+ elClass "ul" "overrides__search" $ do
245+ fmap leftmost $
246+ forM ress $ \(initialText, res) -> do
247+ (resEl, ()) <- elClass' "li" "overrides__search-item" $
248+ forM_ res $ \case
249+ Matched t -> elAttr "span" ("style" =: "font-weight: bold;") $ text t
250+ NotMatched t -> text t
251+ pure $ domEvent Click resEl $> initialText
252+ _ -> pure $ pure never
253+ pure (inp', selectedValue')
254+ pure (inp, isValid)
255+
256+delayFalse :: (MonadHold t m, PerformEvent t m, TriggerEvent t m, MonadIO (Performable m)) => Dynamic t Bool -> m (Dynamic t Bool)
257+delayFalse x = do
258+ initVal <- sample . current $ x
259+ let trueEv = ffilter id $ updated x
260+ falseEv = ffilter not $ updated x
261+ delayedFalseEv <- delay 0.1 falseEv
262+ holdDyn initVal $ leftmost [trueEv, delayedFalseEv]
263+
264+-- | Dark unclickable background for opened sidebar.
265+popupOverlay :: DomBuilder t m => m ()
266+popupOverlay =
267+ elAttr "div" ("class" =: "popup__overlay" <> "aria-hidden" =: "true") blank
268+
269+data NonEditableWorkingOverrideStyle
270+ = RegularNonEditableWorkingOverrideStyle
271+ | LargeNonEditableWorkingOverrideStyle
272+
273+nonEditableWorkingOverrideStyleClasses :: NonEditableWorkingOverrideStyle -> Classes
274+nonEditableWorkingOverrideStyleClasses RegularNonEditableWorkingOverrideStyle = mempty
275+nonEditableWorkingOverrideStyleClasses LargeNonEditableWorkingOverrideStyle = "listing--larger"
276+
277+-- | Widget that shows overrides list. It does not depend on their type.
278+showNonEditableWorkingOverride ::
279+ MonadWidget t m =>
280+ -- | Loading?
281+ Bool ->
282+ NonEditableWorkingOverrideStyle ->
283+ -- | Overrides list.
284+ [WorkingOverride] ->
285+ m ()
286+showNonEditableWorkingOverride loading style cfg =
287+ divClass
288+ ( destructClasses $
289+ "listing" <> "listing--for-text" <> nonEditableWorkingOverrideStyleClasses style
290+ )
291+ $ do
292+ case cfg of
293+ [] ->
294+ divClass "listing__item" $
295+ elClass "span" "listing--info-text" $ text "no custom configuration"
296+ _ -> forM_ cfg $ \(WorkingOverrideKey keyType key, val) -> do
297+ let wrapper = case val of
298+ WorkingDeletedValue _ -> divClass "listing__item deleted"
299+ _ -> divClass "listing__item"
300+ wrapper $ do
301+ let keyWrapper = case keyType of
302+ CustomWorkingOverrideKey -> elClass "span" "listing__key"
303+ DefaultWorkingOverrideKey -> elClass "span" "listing__key default"
304+ keyWrapper $ do
305+ text key
306+ text ": "
307+
308+ case val of
309+ WorkingCustomValue txt -> elClass "span" "listing__value" $ text txt
310+ WorkingDefaultValue txt -> elClass "span" "listing__value default" $ text txt
311+ WorkingDeletedValue (Just txt) -> elClass "span" "listing__value default" $ text txt
312+ WorkingDeletedValue Nothing -> do
313+ elClass "div" "listing__placeholder listing__placeholder__value" $ pure ()
314+ elClass "div" "listing__spinner" $ pure ()
315+ when loading $
316+ divClass "listing__item" $ do
317+ elClass "div" "listing__placeholder" $ pure ()
318+ elClass "div" "listing__spinner" $ pure ()
octopod-frontend/src/Frontend/UIKit/Button/Action.hsnew file+63
@@ -0,0 +1,63 @@
1+module Frontend.UIKit.Button.Action
2+ ( actionButton,
3+ ActionButtonConfig (..),
4+ ActionButtonType (..),
5+ )
6+where
7+
8+import Control.Lens
9+import Data.Default
10+import Data.Generics.Labels ()
11+import Data.Text (Text)
12+import Frontend.Classes
13+import Frontend.UIKit.Button.Common
14+import GHC.Generics (Generic)
15+import Reflex.Dom
16+
17+data ActionButtonConfig t = ActionButtonConfig
18+ { buttonText :: Text
19+ , buttonEnabled :: Dynamic t Bool
20+ , buttonType :: Maybe ActionButtonType
21+ , buttonBaseTag :: BaseButtonTag
22+ }
23+ deriving stock (Generic)
24+
25+instance Reflex t => Default (ActionButtonConfig t) where
26+ def =
27+ ActionButtonConfig
28+ { buttonText = ""
29+ , buttonEnabled = pure True
30+ , buttonType = Nothing
31+ , buttonBaseTag = ButtonTag
32+ }
33+
34+data ActionButtonType
35+ = ArchiveActionButtonType
36+ | EditActionButtonType
37+ | RestoreActionButtonType
38+ | LogsActionButtonType
39+
40+buttonTypeClasses :: ActionButtonType -> Classes
41+buttonTypeClasses = \case
42+ ArchiveActionButtonType -> "action--archive"
43+ EditActionButtonType -> "action--edit"
44+ RestoreActionButtonType -> "action--restore"
45+ LogsActionButtonType -> "action--logs"
46+
47+actionButton ::
48+ (DomBuilder t m, PostBuild t m) =>
49+ ActionButtonConfig t ->
50+ m (Event t ())
51+actionButton cfg =
52+ buttonEl
53+ CommonButtonConfig
54+ { constantClasses =
55+ pure $
56+ "action"
57+ <> maybe mempty buttonTypeClasses (cfg ^. #buttonType)
58+ , enabledClasses = mempty
59+ , disabledClasses = "action--disabled"
60+ , buttonEnabled = cfg ^. #buttonEnabled
61+ , buttonText = pure $ cfg ^. #buttonText
62+ , buttonBaseTag = cfg ^. #buttonBaseTag
63+ }
octopod-frontend/src/Frontend/UIKit/Button/Common.hsnew file+49
@@ -0,0 +1,49 @@
1+module Frontend.UIKit.Button.Common
2+ ( CommonButtonConfig (..),
3+ BaseButtonTag (..),
4+ buttonEl,
5+ )
6+where
7+
8+import Control.Lens
9+import Data.Generics.Labels ()
10+import Data.Map (Map)
11+import Data.Text (Text)
12+import Frontend.Classes
13+import GHC.Generics (Generic)
14+import Reflex.Dom
15+
16+data CommonButtonConfig t = CommonButtonConfig
17+ { constantClasses :: Dynamic t Classes
18+ , enabledClasses :: Classes
19+ , disabledClasses :: Classes
20+ , buttonEnabled :: Dynamic t Bool
21+ , buttonText :: Dynamic t Text
22+ , buttonBaseTag :: BaseButtonTag
23+ }
24+ deriving stock (Generic)
25+
26+data BaseButtonTag = ButtonTag | ATag Text
27+
28+baseTag :: BaseButtonTag -> (Text, Map Text Text)
29+baseTag ButtonTag = ("button", "type" =: "button")
30+baseTag (ATag url) = ("a", "href" =: url <> "target" =: "_blank")
31+
32+buttonEl ::
33+ (DomBuilder t m, PostBuild t m) =>
34+ CommonButtonConfig t ->
35+ m (Event t ())
36+buttonEl cfg = do
37+ let (t, staticAttrs) = baseTag (cfg ^. #buttonBaseTag)
38+ attrsDyn = do
39+ enabled <- cfg ^. #buttonEnabled
40+ let (enabledClasses, enabledAttrs) = case enabled of
41+ True -> (cfg ^. #enabledClasses, mempty)
42+ False -> (cfg ^. #disabledClasses, "disabled" =: "")
43+ cs <- cfg ^. #constantClasses
44+ pure $
45+ staticAttrs
46+ <> "class" =: destructClasses (enabledClasses <> cs)
47+ <> enabledAttrs
48+ (bEl, _) <- elDynAttr' t attrsDyn $ dynText $ cfg ^. #buttonText
49+ pure $ domEvent Click bEl
octopod-frontend/src/Frontend/UIKit/Button/Dash.hsnew file+71
@@ -0,0 +1,71 @@
1+module Frontend.UIKit.Button.Dash
2+ ( dashButton,
3+ DashButtonType (..),
4+ DashButtonConfig (..),
5+ DashButtonStyle (..),
6+ )
7+where
8+
9+import Control.Lens
10+import Data.Default
11+import Data.Generics.Labels ()
12+import Data.Text (Text)
13+import Frontend.Classes
14+import Frontend.UIKit.Button.Common
15+import GHC.Generics (Generic)
16+import Reflex.Dom
17+
18+data DashButtonConfig t = DashButtonConfig
19+ { buttonText :: Text
20+ , buttonEnabled :: Dynamic t Bool
21+ , buttonType :: Maybe DashButtonType
22+ , buttonStyle :: DashButtonStyle
23+ }
24+ deriving stock (Generic)
25+
26+data DashButtonStyle
27+ = RegularDashButtonStyle
28+ | SmallDashButtonStyle
29+ | OverridesDashButtonStyle
30+
31+dashButtonStyleClasses :: DashButtonStyle -> Classes
32+dashButtonStyleClasses RegularDashButtonStyle = mempty
33+dashButtonStyleClasses SmallDashButtonStyle = "dash--smaller"
34+dashButtonStyleClasses OverridesDashButtonStyle = "overrides__add"
35+
36+instance Reflex t => Default (DashButtonConfig t) where
37+ def =
38+ DashButtonConfig
39+ { buttonText = ""
40+ , buttonEnabled = pure True
41+ , buttonType = Nothing
42+ , buttonStyle = RegularDashButtonStyle
43+ }
44+
45+data DashButtonType
46+ = AddDashButtonType
47+ | BackDashButtonType
48+
49+buttonTypeClasses :: DashButtonType -> Classes
50+buttonTypeClasses = \case
51+ AddDashButtonType -> "dash--add"
52+ BackDashButtonType -> "dash--back"
53+
54+dashButton ::
55+ (DomBuilder t m, PostBuild t m) =>
56+ DashButtonConfig t ->
57+ m (Event t ())
58+dashButton cfg =
59+ buttonEl
60+ CommonButtonConfig
61+ { constantClasses =
62+ pure $
63+ "dash"
64+ <> maybe mempty buttonTypeClasses (cfg ^. #buttonType)
65+ <> dashButtonStyleClasses (cfg ^. #buttonStyle)
66+ , enabledClasses = mempty
67+ , disabledClasses = "dash--disabled"
68+ , buttonEnabled = cfg ^. #buttonEnabled
69+ , buttonText = pure $ cfg ^. #buttonText
70+ , buttonBaseTag = ButtonTag
71+ }
octopod-frontend/src/Frontend/UIKit/Button/Expander.hsnew file+85
@@ -0,0 +1,85 @@
1+module Frontend.UIKit.Button.Expander
2+ ( expanderButton,
3+ ExpanderButtonConfig (..),
4+ ExpanderButtonStyle (..),
5+ ExpanderState (..),
6+ ExpanderButtonType (..),
7+ )
8+where
9+
10+import Control.Lens
11+import Control.Monad.Fix
12+import Data.Default
13+import Data.Generics.Labels ()
14+import Data.Text (Text)
15+import Frontend.Classes
16+import Frontend.UIKit.Button.Common
17+import GHC.Generics (Generic)
18+import Reflex.Dom
19+
20+data ExpanderButtonConfig t = ExpanderButtonConfig
21+ { buttonText :: Dynamic t Text
22+ , buttonInitialState :: ExpanderState
23+ , buttonType :: Maybe ExpanderButtonType
24+ , buttonStyle :: ExpanderButtonStyle
25+ }
26+ deriving stock (Generic)
27+
28+data ExpanderState = ExpandedState | ContractedState
29+ deriving stock (Eq)
30+
31+toggleState :: ExpanderState -> ExpanderState
32+toggleState ExpandedState = ContractedState
33+toggleState ContractedState = ExpandedState
34+
35+expanderButtonStateClasses :: ExpanderState -> Classes
36+expanderButtonStateClasses ExpandedState = "expander--open"
37+expanderButtonStateClasses ContractedState = mempty
38+
39+instance Reflex t => Default (ExpanderButtonConfig t) where
40+ def =
41+ ExpanderButtonConfig
42+ { buttonText = ""
43+ , buttonInitialState = ContractedState
44+ , buttonType = Nothing
45+ , buttonStyle = RegularExpanderButtonStyle
46+ }
47+
48+data ExpanderButtonType
49+ = ListingExpanderButton
50+
51+buttonTypeClasses :: ExpanderButtonType -> Classes
52+buttonTypeClasses = \case
53+ ListingExpanderButton -> "listing__more"
54+
55+buttonStyleClasses :: ExpanderButtonStyle -> Classes
56+buttonStyleClasses RegularExpanderButtonStyle = mempty
57+buttonStyleClasses StandaloneExpanderButtonStyle = "expander--stand-alone"
58+
59+data ExpanderButtonStyle
60+ = RegularExpanderButtonStyle
61+ | StandaloneExpanderButtonStyle
62+
63+expanderButton ::
64+ (DomBuilder t m, PostBuild t m, MonadFix m, MonadHold t m) =>
65+ ExpanderButtonConfig t ->
66+ m (Dynamic t ExpanderState)
67+expanderButton cfg = mdo
68+ stateDyn <- foldDyn (\() -> toggleState) (cfg ^. #buttonInitialState) toggleEv
69+ let constantClasses =
70+ "expander"
71+ <> maybe mempty buttonTypeClasses (cfg ^. #buttonType)
72+ <> buttonStyleClasses (cfg ^. #buttonStyle)
73+ toggleEv <-
74+ buttonEl
75+ CommonButtonConfig
76+ { constantClasses = do
77+ state <- stateDyn
78+ pure $ constantClasses <> expanderButtonStateClasses state
79+ , enabledClasses = mempty
80+ , disabledClasses = mempty
81+ , buttonEnabled = pure True
82+ , buttonText = cfg ^. #buttonText
83+ , buttonBaseTag = ButtonTag
84+ }
85+ pure stateDyn
octopod-frontend/src/Frontend/UIKit/Button/Large.hsnew file+96
@@ -0,0 +1,96 @@
1+module Frontend.UIKit.Button.Large
2+ ( largeButton,
3+ LargeButtonType (..),
4+ LargeButtonConfig (..),
5+ LargeButtonStyle (..),
6+ LargeButtonPriority (..),
7+ BaseButtonTag (..),
8+ )
9+where
10+
11+import Control.Lens
12+import Data.Default
13+import Data.Generics.Labels ()
14+import Data.Text (Text)
15+import Frontend.Classes
16+import Frontend.UIKit.Button.Common
17+import GHC.Generics (Generic)
18+import Reflex.Dom
19+
20+data LargeButtonConfig t = LargeButtonConfig
21+ { buttonText :: Text
22+ , buttonEnabled :: Dynamic t Bool
23+ , buttonType :: Dynamic t (Maybe LargeButtonType)
24+ , buttonPriority :: LargeButtonPriority
25+ , buttonStyle :: LargeButtonStyle
26+ , buttonBaseTag :: BaseButtonTag
27+ }
28+ deriving stock (Generic)
29+
30+data LargeButtonPriority = PrimaryLargeButton | SecondaryLargeButton
31+
32+buttonPriorityClasses :: LargeButtonPriority -> Classes
33+buttonPriorityClasses PrimaryLargeButton = mempty
34+buttonPriorityClasses SecondaryLargeButton = "button--secondary"
35+
36+data LargeButtonStyle
37+ = RegularLargeButtonStyle
38+ | PopupActionLargeButtonStyle
39+ | DialogActionLargeButtonStyle
40+ | PageActionLargeButtonStyle
41+
42+buttonStyleClasses :: LargeButtonStyle -> Classes
43+buttonStyleClasses RegularLargeButtonStyle = mempty
44+buttonStyleClasses PopupActionLargeButtonStyle = "popup__action"
45+buttonStyleClasses DialogActionLargeButtonStyle = "dialog__action"
46+buttonStyleClasses PageActionLargeButtonStyle = "page__action"
47+
48+instance Reflex t => Default (LargeButtonConfig t) where
49+ def =
50+ LargeButtonConfig
51+ { buttonText = ""
52+ , buttonEnabled = pure True
53+ , buttonType = pure Nothing
54+ , buttonStyle = RegularLargeButtonStyle
55+ , buttonPriority = PrimaryLargeButton
56+ , buttonBaseTag = ButtonTag
57+ }
58+
59+data LargeButtonType
60+ = AddLargeButtonType
61+ | ArchiveLargeButtonType
62+ | RestoreLargeButtonType
63+ | EditLargeButtonType
64+ | LogsLargeButtonType
65+ | SaveLargeButtonType
66+ | LoadingLargeButtonType
67+
68+buttonTypeClasses :: LargeButtonType -> Classes
69+buttonTypeClasses AddLargeButtonType = "button--add"
70+buttonTypeClasses ArchiveLargeButtonType = "button--archive"
71+buttonTypeClasses RestoreLargeButtonType = "button--restore"
72+buttonTypeClasses EditLargeButtonType = "button--edit"
73+buttonTypeClasses LogsLargeButtonType = "button--logs"
74+buttonTypeClasses SaveLargeButtonType = "button--save"
75+buttonTypeClasses LoadingLargeButtonType = "button--save-loading"
76+
77+largeButton ::
78+ (DomBuilder t m, PostBuild t m) =>
79+ LargeButtonConfig t ->
80+ m (Event t ())
81+largeButton cfg =
82+ buttonEl
83+ CommonButtonConfig
84+ { constantClasses = do
85+ bType <- cfg ^. #buttonType
86+ pure $
87+ "button"
88+ <> maybe mempty buttonTypeClasses bType
89+ <> buttonStyleClasses (cfg ^. #buttonStyle)
90+ <> buttonPriorityClasses (cfg ^. #buttonPriority)
91+ , enabledClasses = mempty
92+ , disabledClasses = "button--disabled"
93+ , buttonEnabled = cfg ^. #buttonEnabled
94+ , buttonText = pure $ cfg ^. #buttonText
95+ , buttonBaseTag = cfg ^. #buttonBaseTag
96+ }
octopod-frontend/src/Frontend/UIKit/Button/Sort.hsnew file+58
@@ -0,0 +1,58 @@
1+module Frontend.UIKit.Button.Sort
2+ ( sortButton,
3+ SortButtonConfig (..),
4+ SortButtonState (..),
5+ )
6+where
7+
8+import Control.Lens
9+import Data.Default
10+import Data.Generics.Labels ()
11+import Data.Text (Text)
12+import Frontend.Classes
13+import Frontend.UIKit.Button.Common
14+import GHC.Generics (Generic)
15+import Reflex.Dom
16+
17+data SortButtonConfig t = SortButtonConfig
18+ { buttonText :: Text
19+ , buttonEnabled :: Dynamic t Bool
20+ , buttonState :: Dynamic t (Maybe SortButtonState)
21+ }
22+ deriving stock (Generic)
23+
24+instance Reflex t => Default (SortButtonConfig t) where
25+ def =
26+ SortButtonConfig
27+ { buttonText = ""
28+ , buttonEnabled = pure True
29+ , buttonState = pure Nothing
30+ }
31+
32+data SortButtonState
33+ = SortAscButtonState
34+ | SortDescButtonState
35+
36+buttonSortStateClasses :: SortButtonState -> Classes
37+buttonSortStateClasses = \case
38+ SortAscButtonState -> "sort--active sort--asc"
39+ SortDescButtonState -> "sort--active sort--desc"
40+
41+sortButton ::
42+ (DomBuilder t m, PostBuild t m) =>
43+ SortButtonConfig t ->
44+ m (Event t ())
45+sortButton cfg =
46+ buttonEl
47+ CommonButtonConfig
48+ { constantClasses = do
49+ state <- cfg ^. #buttonState
50+ pure $
51+ "sort"
52+ <> maybe mempty buttonSortStateClasses state
53+ , enabledClasses = mempty
54+ , disabledClasses = mempty
55+ , buttonEnabled = cfg ^. #buttonEnabled
56+ , buttonText = pure $ cfg ^. #buttonText
57+ , buttonBaseTag = ButtonTag
58+ }
octopod-frontend/src/Frontend/UIKit/Button/Static.hsnew file+71
@@ -0,0 +1,71 @@
1+module Frontend.UIKit.Button.Static
2+ ( closeNotificationButton,
3+ closeClassicPopupButton,
4+ closePopupButton,
5+ deleteOverrideButton,
6+ undoOverrideButton,
7+ )
8+where
9+
10+import Frontend.UIKit.Button.Common
11+import Reflex.Dom
12+
13+closeNotificationButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
14+closeNotificationButton =
15+ buttonEl
16+ CommonButtonConfig
17+ { constantClasses = pure "notification__close"
18+ , enabledClasses = mempty
19+ , disabledClasses = mempty
20+ , buttonEnabled = pure True
21+ , buttonText = pure ""
22+ , buttonBaseTag = ButtonTag
23+ }
24+
25+closeClassicPopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
26+closeClassicPopupButton =
27+ buttonEl
28+ CommonButtonConfig
29+ { constantClasses = pure "classic-popup__close"
30+ , enabledClasses = mempty
31+ , disabledClasses = mempty
32+ , buttonEnabled = pure True
33+ , buttonText = pure ""
34+ , buttonBaseTag = ButtonTag
35+ }
36+
37+closePopupButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
38+closePopupButton =
39+ buttonEl
40+ CommonButtonConfig
41+ { constantClasses = pure "popup__close"
42+ , enabledClasses = mempty
43+ , disabledClasses = mempty
44+ , buttonEnabled = pure True
45+ , buttonText = pure ""
46+ , buttonBaseTag = ButtonTag
47+ }
48+
49+deleteOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
50+deleteOverrideButton =
51+ buttonEl
52+ CommonButtonConfig
53+ { constantClasses = pure "overrides__delete spot spot--cancel"
54+ , enabledClasses = mempty
55+ , disabledClasses = mempty
56+ , buttonEnabled = pure True
57+ , buttonText = pure ""
58+ , buttonBaseTag = ButtonTag
59+ }
60+
61+undoOverrideButton :: (DomBuilder t m, PostBuild t m) => m (Event t ())
62+undoOverrideButton =
63+ buttonEl
64+ CommonButtonConfig
65+ { constantClasses = pure "overrides__delete spot spot--undo"
66+ , enabledClasses = mempty
67+ , disabledClasses = mempty
68+ , buttonEnabled = pure True
69+ , buttonText = pure ""
70+ , buttonBaseTag = ButtonTag
71+ }
octopod-frontend/src/Frontend/Utils.hs+166−353
@@ -6,22 +6,14 @@
66--frontend modules.
77module Frontend.Utils
88 ( sidebar,
9− buttonClass,
10− buttonClassEnabled,
119 wrapRequestErrors,
1210 octopodTextInput,
1311 deploymentPopupBody,
1412 ClickedElement (..),
1513 pageNotification,
16− aButtonClassEnabled,
17− buttonClassEnabled',
1814 kubeDashboardUrl,
19− loadingCommonWidget,
20− errorCommonWidget,
21− aButtonDynClass',
2215 formatPosixToDate,
2316 overridesWidget,
24− aButtonClass',
2517 statusWidget,
2618 elementClick,
2719 showT,
@@ -29,12 +21,15 @@ module Frontend.Utils
2921 formatPosixToDateTime,
3022 dropdownWidget,
3123 dropdownWidget',
32− buttonDynClass,
3324 deploymentConfigProgressiveComponents,
3425 deploymentConfigProgressive,
3526 holdClearingWith,
3627 unitEv,
37− deploymentSection,
28+ (<&&>),
29+ ProgressiveFullConfig (..),
30+ RequestErrorHandler,
31+ deploymentOverridesWidget,
32+ applicationOverridesWidget,
3833 )
3934where
4035
@@ -42,18 +37,14 @@ import Common.Types as CT
4237import Control.Lens
4338import Control.Monad
4439import Control.Monad.Reader
45−import Data.Align
4640import qualified Data.Foldable as F
4741import Data.Functor
4842import Data.Generics.Labels ()
4943import Data.Generics.Sum
50−import Data.Map (Map)
5144import qualified Data.Map as M
52−import qualified Data.Map.Ordered.Strict as OM
53−import Data.Maybe (fromMaybe)
45+import Data.Maybe (fromMaybe, isNothing)
5446import Data.Monoid
5547import Data.Text as T (Text, intercalate, null, pack)
56−import Data.These
5748import Data.Time
5849import Data.UniqMap
5950import Data.Unique
@@ -61,6 +52,8 @@ import Data.Witherable
6152import Data.WorkingOverrides
6253import Frontend.API
6354import Frontend.GHCJS
55+import Frontend.UIKit
56+import GHC.Generics (Generic)
6457import GHCJS.DOM
6558import GHCJS.DOM.Element as DOM
6659import GHCJS.DOM.EventM (on, target)
@@ -162,148 +155,6 @@ sidebar showEv closeEv m = mdo
162155 ]
163156 pure $ switchDyn $ fst <$> resultEvDyn
164157
165−-- | Dark unclickable background for opened sidebar.
166−popupOverlay :: DomBuilder t m => m ()
167−popupOverlay =
168− elAttr "div" ("class" =: "popup__overlay" <> "aria-hidden" =: "true") blank
169−
170−-- | Button with customizable classes and label text.
171−buttonClass ::
172− (DomBuilder t m, PostBuild t m) =>
173− -- | Classes.
174− Text ->
175− -- | Label text.
176− Text ->
177− m (Event t ())
178−buttonClass cl lbl = do
179− (bEl, _) <-
180− elDynAttr'
181− "button"
182− (constDyn $ "class" =: cl <> "type" =: "button")
183− $ text lbl
184− return $ domEvent Click bEl
185−
186−-- | Advanced version of 'buttonClass' with dynamic arguments.
187−buttonDynClass ::
188− (DomBuilder t m, PostBuild t m) =>
189− -- | Classes.
190− Dynamic t Text ->
191− -- | Label text.
192− Dynamic t Text ->
193− m (Event t ())
194−buttonDynClass clDyn lblDyn = do
195− let attrDyn = ffor clDyn $ \cl -> "class" =: cl <> "type" =: "button"
196− (bEl, _) <-
197− elDynAttr' "button" attrDyn $
198− dynText lblDyn
199− return $ domEvent Click bEl
200−
201−-- | Advanced version of 'buttonClass' with a disabled state.
202−buttonClassEnabled ::
203− (DomBuilder t m, PostBuild t m) =>
204− -- | Classes.
205− Text ->
206− -- | Label text.
207− Text ->
208− -- | Enabled flag.
209− Dynamic t Bool ->
210− m (Event t ())
211−buttonClassEnabled cl lbl dDyn = do
212− let attrDyn = ffor dDyn $ \case
213− True -> "class" =: cl <> "type" =: "button"
214− False ->
215− "class" =: (cl <> " button--disabled")
216− <> "type" =: "button"
217− <> "disabled" =: ""
218− (bEl, _) <-
219− elDynAttr' "button" attrDyn $
220− text lbl
221− return $ domEvent Click bEl
222−
223−-- | Special version of 'buttonClassEnabled' that supports custom classes for
224−-- the disabled state.
225−buttonClassEnabled' ::
226− (DomBuilder t m, PostBuild t m) =>
227− -- | Classes.
228− Text ->
229− -- | Label text.
230− Text ->
231− -- | Enabled flag.
232− Dynamic t Bool ->
233− -- | Custom classes for disabled state.
234− Text ->
235− m (Event t ())
236−buttonClassEnabled' cl lbl dDyn disClass = do
237− let attrDyn = ffor dDyn $ \case
238− True -> "class" =: cl <> "type" =: "button"
239− False ->
240− "class" =: (cl <> " " <> disClass)
241− <> "type" =: "button"
242− <> "disabled" =: ""
243− (bEl, _) <-
244− elDynAttr' "button" attrDyn $
245− text lbl
246− return $ domEvent Click bEl
247−
248−-- | Version of 'buttonClass' for links that should look like buttons.
249−aButtonClass' ::
250− (DomBuilder t m, PostBuild t m) =>
251− -- | Classes.
252− Text ->
253− -- | Label text.
254− Text ->
255− -- | Extra attributes
256− Dynamic t (Map Text Text) ->
257− m (Event t ())
258−aButtonClass' cl lbl eAttrs = do
259− (bEl, _) <-
260− elDynAttr'
261− "a"
262− (fmap (<> ("class" =: cl)) eAttrs)
263− $ text lbl
264− return $ domEvent Click bEl
265−
266−-- | Version of 'buttonDynClass' for links that should look like
267−-- buttons.
268−aButtonDynClass' ::
269− (DomBuilder t m, PostBuild t m) =>
270− -- | Classes.
271− Dynamic t Text ->
272− -- | Label text.
273− Dynamic t Text ->
274− -- | Extra attributes
275− Dynamic t (Map Text Text) ->
276− m (Event t ())
277−aButtonDynClass' clDyn lblDyn eAttrs = do
278− let attrDyn' = ffor clDyn $ \cl -> "class" =: cl
279− attrDyn = (<>) <$> eAttrs <*> attrDyn'
280− (bEl, _) <-
281− elDynAttr' "a" attrDyn $
282− dynText lblDyn
283− return $ domEvent Click bEl
284−
285−-- | Version of 'buttonClassEnabled' for links that should look like
286−-- buttons.
287−aButtonClassEnabled ::
288− (DomBuilder t m, PostBuild t m) =>
289− -- | Classes.
290− Text ->
291− -- | Label text.
292− Text ->
293− -- | Enabled flag.
294− Dynamic t Bool ->
295− m (Event t ())
296−aButtonClassEnabled cl lbl dDyn = do
297− let attrDyn = ffor dDyn $ \case
298− True -> "class" =: cl
299− False ->
300− "class" =: (cl <> " button--disabled")
301− <> "disabled" =: ""
302− (bEl, _) <-
303− elDynAttr' "a" attrDyn $
304− text lbl
305− return $ domEvent Click bEl
306−
307158-- | Formats posix seconds to date in iso8601.
308159formatPosixToDate :: FormatTime t => t -> Text
309160formatPosixToDate = pack . formatTime defaultTimeLocale (iso8601DateFormat Nothing)
@@ -332,7 +183,7 @@ statusWidget stDyn = do
332183octopodTextInput ::
333184 MonadWidget t m =>
334185 -- | Input field classes.
335− Text ->
186+ Classes ->
336187 -- | Label text.
337188 Text ->
338189 -- | Placeholder for input field.
@@ -345,92 +196,9 @@ octopodTextInput ::
345196octopodTextInput clss lbl placeholder val errEv =
346197 elClass "section" "deployment__section" $ do
347198 elClass "h3" "deployment__sub-heading" $ text lbl
348− elClass "div" "deployment__widget" $
349− octopodTextInput' (pure False) clss placeholder (pure . fromMaybe "" $ val) errEv
350−
351−-- | The only text input field that is used in project forms. This input
352−-- provides automatic error message hiding after user starts typing.
353−octopodTextInput' ::
354− MonadWidget t m =>
355− -- | Disabled?
356− Dynamic t Bool ->
357− -- | Input field classes.
358− Text ->
359− -- | Placeholder for input field.
360− Text ->
361− -- | Possible init value.
362− (Dynamic t Text) ->
363− -- | Event carrying the error message.
364− Event t Text ->
365− m (Dynamic t Text, Dynamic t Bool)
366−octopodTextInput' disabledDyn clss placeholder inValDyn' errEv = mdo
367− inValDyn <- holdUniqDyn inValDyn'
368− let inValEv =
369− align (updated inValDyn) (updated valDyn)
370− & fmapMaybe
371− ( \case
372− This x -> Just x
373− These inV currV | inV /= currV -> Just inV
374− _ -> Nothing
375− )
376− let inpClass = " input"
377− inpErrClass = " input input--error"
378− isValid <-
379− holdDyn True $
380− leftmost
381− [ False <$ errEv
382− , True <$ updated valDyn
383− ]
384− classDyn <-
385− holdDyn (clss <> inpClass) $
386− leftmost
387− [ (clss <> inpErrClass) <$ errEv
388− , (clss <> inpClass) <$ updated valDyn
389− ]
390− inVal <- sample . current $ inValDyn
391− disabled <- sample . current $ disabledDyn
392− valDyn <- elDynClass "div" classDyn $ do
393− inp <-
394− inputElement $
395− def
396− & initialAttributes
397− .~ ( "type" =: "text"
398− <> "class" =: "input__widget"
399− <> "placeholder" =: placeholder
400− )
401− & inputElementConfig_setValue .~ inValEv
402− & inputElementConfig_initialValue .~ inVal
403− & inputElementConfig_elementConfig . elementConfig_initialAttributes
404− %~ (if disabled then M.insert "disabled" "disabled" else id)
405− & inputElementConfig_elementConfig . elementConfig_modifyAttributes
406− <>~ updated
407− ( do
408− disabled' <- disabledDyn
409− pure $
410− M.singleton "disabled" $
411− if disabled' then Just "disabled" else Nothing
412− )
413− widgetHold_ blank $
414− leftmost
415− [ divClass "input__output" . text <$> errEv
416− , blank <$ updated valDyn
417− ]
418− pure $ value inp
419− pure (valDyn, isValid)
420−
421−-- | Widget with a loading spinner.
422−loadingCommonWidget :: MonadWidget t m => m ()
423−loadingCommonWidget =
424− divClass "loading loading--enlarged loading--alternate" $
425− text "Loading..."
426−
427−-- | Widget with an error message.
428−errorCommonWidget :: MonadWidget t m => m ()
429−errorCommonWidget =
430− divClass "null null--data" $
431− divClass "null__content" $ do
432− elClass "b" "null__heading" $ text "Cannot retrieve the data"
433− divClass "null__message" $ text "Try to reload the page"
199+ elClass "div" "deployment__widget" $ do
200+ (value -> valDyn, validDyn) <- octopodTextInput' (pure []) (pure False) (pure clss) placeholder (pure . fromMaybe "" $ val) errEv
201+ pure (valDyn, validDyn)
434202
435203-- | Widget that can show and hide overrides if there are more than 3. This
436204-- widget is used in the deployments table and the deployment action table.
@@ -438,51 +206,62 @@ overridesWidget ::
438206 MonadWidget t m =>
439207 -- | List of overrides.
440208 Overrides l ->
209+ (Event t () -> m (Event t (DefaultConfig l))) ->
441210 m ()
442−overridesWidget (Overrides (OM.assocs -> envs)) = divClass "listing listing--for-text" $ do
443− let visible = take 3 envs
444− envLength = length envs
445− listing visible
446− when (envLength > 3) $ mdo
447− let hidden = drop 3 envs
448− showDyn <- toggle False toggleEv
449− dyn_ $
450− showDyn <&> \case
451− True -> listing hidden
452− False -> blank
453− let btnClassDyn =
454− ifThenElseDyn
455− showDyn
456− "listing__more expander expander--open"
457− "listing__more expander"
458− btnTextDyn =
459− ifThenElseDyn showDyn "Hide" $
460− "Show all (" <> showT envLength <> ")"
461− toggleEv <- buttonDynClass btnClassDyn btnTextDyn
462− blank
463− where
464− listing envs' = do
465− forM_ envs' $ \(var, val) ->
466− divClass "listing__item" $ do
467− el "b" $ text $ var <> ": "
468− case val of
469− ValueAdded v -> text v
470− ValueDeleted -> el "i" $ text "<deleted>"
471−
472−-- | @if-then-else@ helper for cases when bool value is wrapped in 'Dynamic'.
473−ifThenElseDyn ::
474− Reflex t =>
475− -- | Condition wrapped in `Dynamic`.
476− Dynamic t Bool ->
477− -- | `then` branch.
478− b ->
479− -- | `else` branch.
480− b ->
481− Dynamic t b
482−ifThenElseDyn bDyn t f =
483− bDyn <&> \case
484− True -> t
485− False -> f
211+overridesWidget ovs getDef = divClass "listing listing--for-text" $ mdo
212+ defMDyn <- getDef firstExpand >>= holdDynMaybe
213+ dyn_ $
214+ expandState <&> \case
215+ ExpandedState ->
216+ void $
217+ networkView $
218+ do
219+ defM <- defMDyn
220+ pure $
221+ showNonEditableWorkingOverride (isNothing defM) RegularNonEditableWorkingOverrideStyle $
222+ elemsUniq $ constructWorkingOverrides defM ovs
223+ ContractedState ->
224+ let ovsList = elemsUniq $ constructWorkingOverrides Nothing ovs
225+ in showNonEditableWorkingOverride False RegularNonEditableWorkingOverrideStyle $
226+ take 3 ovsList
227+
228+ expandState <-
229+ expanderButton
230+ ExpanderButtonConfig
231+ { buttonText = do
232+ state <- expandState
233+ pure $ case state of
234+ ExpandedState -> "Hide"
235+ ContractedState -> "Show all"
236+ , buttonInitialState = ContractedState
237+ , buttonType = Just ListingExpanderButton
238+ , buttonStyle = RegularExpanderButtonStyle
239+ }
240+ firstExpand <- headE $ ($> ()) $ ffilter (== ExpandedState) $ updated expandState
241+ pure ()
242+
243+deploymentOverridesWidget ::
244+ MonadWidget t m =>
245+ RequestErrorHandler t m ->
246+ Overrides 'DeploymentLevel ->
247+ m ()
248+deploymentOverridesWidget hReq depOvs =
249+ overridesWidget depOvs $ defaultDeploymentOverrides >=> hReq
250+
251+applicationOverridesWidget ::
252+ MonadWidget t m =>
253+ RequestErrorHandler t m ->
254+ Overrides 'DeploymentLevel ->
255+ Overrides 'ApplicationLevel ->
256+ m ()
257+applicationOverridesWidget hReq depOvs appOvs =
258+ overridesWidget appOvs $ \fire -> do
259+ depDefEv <- defaultDeploymentOverrides fire >>= hReq
260+ fmap switchDyn $
261+ networkHold (pure never) $
262+ depDefEv <&> \depDef -> do
263+ pb <- getPostBuild
264+ defaultApplicationOverrides (pure $ Right $ applyOverrides depOvs depDef) pb >>= hReq
486265
487266-- | Type of notification at the top of pages.
488267data DeploymentPageNotification
@@ -506,7 +285,7 @@ pageNotification notEv = mdo
506285 messageClassWidget txt cl =
507286 divClass ("page__output notification " <> cl) $ do
508287 text txt
509− buttonClass "notification__close" ""
288+ closeNotificationButton
510289 closeEv = switchDyn closeEvDyn
511290 closeEvDyn <-
512291 widgetHold (pure never) $
@@ -549,28 +328,25 @@ deploymentPopupBody hReq defTag defAppOv defDepOv errEv = mdo
549328
550329 (tagDyn, tOkEv) <- octopodTextInput "tag" "Tag" "Tag" (unDeploymentTag <$> defTag) tagErrEv
551330
552− let holdDCfg :: Dynamic t (Maybe (DefaultConfig l)) -> Overrides l -> m (Dynamic t (Overrides l))
553− holdDCfg dCfgDyn ovs = mdo
331+ let holdDCfg ::
332+ Dynamic t [Text] ->
333+ Dynamic t (Maybe (DefaultConfig l)) ->
334+ Overrides l ->
335+ m (Dynamic t (Overrides l))
336+ holdDCfg values dCfgDyn ovs = mdo
554337 ovsDyn <- holdDyn ovs ovsEv
555338 x <- attachDyn (current ovsDyn) dCfgDyn
556− ovsEv <- dyn (x <&> \(ovs', dCfg) -> envVarsInput dCfg ovs') >>= switchHold never >>= debounce 0.5
339+ ovsEv <- dyn (x <&> \(ovs', dCfg) -> envVarsInput values dCfg ovs') >>= switchHold never >>= debounce 0.5
557340 pure ovsDyn
558− holdDKeys n keysDyn = do
559− let loading = loadingCommonWidget
560− deploymentSection n $
561− widgetHold_ loading $
562− keysDyn <&> \case
563− Just keys -> el "ul" $
564− forM_ keys $ \key -> el "li" $ text key
565− Nothing -> loading
566−
567− deploymentOvsDyn <- deploymentSection "Deployment overrides" $ holdDCfg defDep defDepOv
568− holdDKeys "Deployment keys" (Just <$> depKeys)
341+
342+ depKeysDyn <- holdDyn [] depKeys
343+ deploymentOvsDyn <- deploymentSection "Deployment overrides" $ holdDCfg depKeysDyn defDep defDepOv
344+
569345 appKeys <- waitForValuePromptly depCfgEv $ \deploymentCfg -> do
570346 pb' <- getPostBuild
571347 applicationOverrideKeys (pure $ Right deploymentCfg) pb' >>= hReq >>= immediateNothing
572− applicationOvsDyn <- deploymentSection "App overrides" $ holdDCfg defAppM defAppOv
573− holdDKeys "App keys" appKeys
348+ appKeysDyn <- holdDyn [] $ catMaybes appKeys
349+ applicationOvsDyn <- deploymentSection "App overrides" $ holdDCfg appKeysDyn defAppM defAppOv
574350
575351 validDyn <- holdDyn True $ updated tOkEv
576352 pure $
@@ -625,23 +401,31 @@ deploymentConfigProgressive ::
625401 RequestErrorHandler t m ->
626402 Dynamic t (Overrides 'DeploymentLevel) ->
627403 Dynamic t (Overrides 'ApplicationLevel) ->
628− m (Event t FullConfig)
404+ m (Dynamic t ProgressiveFullConfig)
629405deploymentConfigProgressive hReq depOvsDyn appOvsDyn = do
630− (_, defAppEv, depCfgEv) <- deploymentConfigProgressiveComponents hReq depOvsDyn
406+ (depCfgEv, defAppEv, _) <- deploymentConfigProgressiveComponents hReq depOvsDyn
631407 defAppMDyn <- holdDynMaybe defAppEv
632− depCfgMDyn <- holdDynMaybe depCfgEv
633− pure . catMaybes . updated $ do
408+ defCfgMDyn <- holdDynMaybe depCfgEv
409+ pure $ do
634410 defAppM <- defAppMDyn
635411 appOvs <- appOvsDyn
636− depCfgM <- depCfgMDyn
637− pure $ do
638− defApp <- defAppM
639− depCfg <- depCfgM
640− pure
641− FullConfig
642− { appConfig = applyOverrides appOvs defApp
643− , depConfig = depCfg
644− }
412+ defDepM <- defCfgMDyn
413+ depOvs <- depOvsDyn
414+ pure
415+ ProgressiveFullConfig
416+ { appConfig = constructWorkingOverrides defAppM appOvs
417+ , appConfigLoading = isNothing defAppM
418+ , depConfig = constructWorkingOverrides defDepM depOvs
419+ , depConfigLoading = isNothing defDepM
420+ }
421+
422+data ProgressiveFullConfig = ProgressiveFullConfig
423+ { appConfig :: WorkingOverrides
424+ , appConfigLoading :: Bool
425+ , depConfig :: WorkingOverrides
426+ , depConfigLoading :: Bool
427+ }
428+ deriving stock (Generic)
645429
646430waitForValuePromptly :: (MonadHold t m, Adjustable t m) => Event t x -> (x -> m (Event t y)) -> m (Event t y)
647431waitForValuePromptly ev f = fmap switchPromptlyDyn $ networkHold (pure never) $ f <$> ev
@@ -682,65 +466,91 @@ errorHeader appErr = do
682466envVarsInput ::
683467 forall l t m.
684468 MonadWidget t m =>
469+ Dynamic t [Text] ->
685470 Maybe (DefaultConfig l) ->
686471 -- | Initial deployment overrides.
687472 Overrides l ->
688473 -- | Updated deployment overrides.
689474 m (Event t (Overrides l))
690−envVarsInput dCfg ovs = mdo
475+envVarsInput values dCfg ovs = mdo
691476 envsDyn <- foldDyn appEndo (constructWorkingOverrides dCfg ovs) $ leftmost [addEv, updEv]
692477 let addEv = clickEv $> Endo (fst . insertUniqStart newWorkingOverride)
693478 clickEv <-
694− buttonClassEnabled'
695− "overrides__add dash dash--add"
696− "Add an override"
697− addingIsEnabled
698− "dash--disabled"
479+ dashButton
480+ DashButtonConfig
481+ { buttonText = "Add an override"
482+ , buttonEnabled = addingIsEnabled
483+ , buttonType = Just AddDashButtonType
484+ , buttonStyle = OverridesDashButtonStyle
485+ }
699486 updEv <-
700487 switchDyn . fmap F.fold
701488 <$> listWithKey
702489 (uniqMap <$> envsDyn)
703− (\i x -> fmap (performUserOverrideAction (lookupDefaultConfig <$> dCfg) i) <$> envVarInput x)
490+ (\i x -> fmap (performUserOverrideAction (lookupDefaultConfig <$> dCfg) i) <$> envVarInput values x)
704491 let addingIsEnabled = all (\(WorkingOverrideKey _ x, _) -> not . T.null $ x) . elemsUniq <$> envsDyn
705492 case dCfg of
706493 Just _ -> pure ()
707− Nothing -> loadingCommonWidget
494+ Nothing -> loadingOverrides
708495 pure . updated $ destructWorkingOverrides <$> envsDyn
709496
710−deploymentSection :: DomBuilder t m => Text -> m a -> m a
711−deploymentSection n m = elClass "section" "deployment__section" $ do
712− elClass "h3" "deployment__sub-heading" $ text n
713− elClass "div" "deployment__widget" m
714−
715497-- | Widget for entering a key-value pair. The updated overrides list is
716498-- written to the 'EventWriter'.
717499envVarInput ::
718500 (MonadWidget t m) =>
501+ -- | The key values to suggest to the user
502+ Dynamic t [Text] ->
719503 -- | Current variable key and value.
720504 Dynamic t WorkingOverride ->
721505 m (Event t UserOverrideAction)
722−envVarInput val = do
723− let v =
724− val <&> snd <&> \case
725− WorkingCustomValue x -> x
726− WorkingDefaultValue x -> x
727− WorkingDeletedValue (Just x) -> x
728− WorkingDeletedValue Nothing -> "<loading deleted>"
729− k = val <&> \(WorkingOverrideKey _ x, _) -> x
730− disabledKey = val <&> \(WorkingOverrideKey t _, _) -> t == DefaultWorkingOverrideKey
731−
732− divClass "overrides__item" $ do
733− (keyTextDyn, _) <-
734− octopodTextInput' disabledKey "overrides__key" "key" k never
735− (valTextDyn, _) <-
736− octopodTextInput' (pure False) "overrides__value" "value" v never
737− closeEv <- buttonClass "overrides__delete spot spot--cancel" "Delete"
738− pure $
739− leftmost
740− [ UpdateKey <$> updated keyTextDyn
741− , UpdateValue <$> updated valTextDyn
742− , closeEv $> DeleteOverride
743− ]
506+envVarInput values val = do
507+ let kDyn = val <&> \(WorkingOverrideKey _ x, _) -> x
508+ -- Either <override present> <override deleted>
509+ d <-
510+ eitherDyn $
511+ val <&> snd <&> \case
512+ WorkingCustomValue v -> Right (v, EditedOverrideFieldType)
513+ WorkingDefaultValue v -> Right (v, DefaultOverrideFieldType)
514+ WorkingDeletedValue v -> Left v
515+ networkView >=> switchHold never $
516+ d <&> \case
517+ Right vtDyn -> do
518+ let (vDyn, vTypeDyn) = splitDynPure vtDyn
519+ (keyTextDyn, valTextDyn, closeEv) <-
520+ overrideField
521+ values
522+ OverrideField
523+ { fieldValue = kDyn
524+ , fieldError = never
525+ , fieldDisabled =
526+ val <&> \(WorkingOverrideKey t _, _) -> t == DefaultWorkingOverrideKey
527+ , fieldType =
528+ val <&> \(WorkingOverrideKey t _, _) -> case t of
529+ CustomWorkingOverrideKey -> EditedOverrideFieldType
530+ DefaultWorkingOverrideKey -> DefaultOverrideFieldType
531+ }
532+ OverrideField
533+ { fieldValue = vDyn
534+ , fieldError = never
535+ , fieldDisabled = pure False
536+ , fieldType = vTypeDyn
537+ }
538+ pure $
539+ leftmost
540+ [ UpdateKey <$> updated keyTextDyn
541+ , UpdateValue <$> updated valTextDyn
542+ , closeEv $> DeleteOverride
543+ ]
544+ Left vDyn -> do
545+ restoreEv <-
546+ networkView >=> switchHold never $ do
547+ v <- vDyn
548+ k <- kDyn
549+ pure $ deletedOverride k v
550+ pure $
551+ flip push restoreEv $ \() -> do
552+ v <- sample . current $ vDyn
553+ pure $ UpdateValue <$> v
744554
745555data UserOverrideAction = UpdateKey !Text | UpdateValue !Text | DeleteOverride
746556
@@ -790,3 +600,6 @@ holdClearingWith aEv clear =
790600
791601unitEv :: Reflex t => Dynamic t a -> Event t ()
792602unitEv = fmapCheap (const ()) . updated
603+
604+(<&&>) :: (Functor f1, Functor f2) => f1 (f2 a) -> (a -> b) -> f1 (f2 b)
605+x <&&> f = (fmap . fmap) f x
octopod-frontend/src/Main.hs+1−1
@@ -18,7 +18,7 @@ import qualified Data.Text as T
1818import Frontend.API
1919import Frontend.GHCJS
2020import Frontend.Route
21−import Frontend.Utils (errorCommonWidget, loadingCommonWidget)
21+import Frontend.UIKit
2222import Page.Deployment
2323import Page.Deployments
2424
octopod-frontend/src/Page/ClassicPopup.hs+13−4
@@ -12,7 +12,8 @@ where
1212
1313import Reflex.Dom
1414
15−import Frontend.Utils
15+import Data.Generics.Labels ()
16+import Frontend.UIKit
1617
1718-- | The root function of a popup.
1819classicPopup ::
@@ -51,7 +52,7 @@ popupWidget m =
5152 divClass "classic-popup__viewport" $
5253 divClass "classic-popup__slot" $ do
5354 (okEv, cancelEv) <- m
54− closeEv <- buttonClass "classic-popup__close" "Close"
55+ closeEv <- closeClassicPopupButton
5556 pure $ (okEv, leftmost [cancelEv, closeEv])
5657
5758-- | Popup that requires confirmation of deployment deletion.
@@ -65,8 +66,16 @@ confirmArchivePopup showEv txt = do
6566 divClass "dialog dialog--archive" $ do
6667 divClass "dialog__content" txt
6768 divClass "dialog__footer" $ do
68− okEv <- buttonClass "dialog__action button" "Archive"
69+ okEv <-
70+ largeButton $
71+ def
72+ & #buttonStyle .~~ DialogActionLargeButtonStyle
73+ & #buttonText .~~ "Archive"
6974 cancelEv <-
70− buttonClass "dialog__action button--secondary button" "Cancel"
75+ largeButton $
76+ def
77+ & #buttonStyle .~~ DialogActionLargeButtonStyle
78+ & #buttonPriority .~~ SecondaryLargeButton
79+ & #buttonText .~~ "Cancel"
7180 pure (okEv, cancelEv)
7281 classicPopup showEv body
octopod-frontend/src/Page/Deployment.hs+52−63
@@ -19,11 +19,12 @@ import Common.Utils
1919import Control.Monad.Reader
2020import Data.Align
2121import Data.Generics.Labels ()
22−import qualified Data.Map.Ordered.Strict as OM
2322import Data.Time
23+import Data.UniqMap
2424import Frontend.API
2525import Frontend.GHCJS
2626import Frontend.Route
27+import Frontend.UIKit
2728import Frontend.Utils
2829import Page.ClassicPopup
2930import Page.Elements.Links
@@ -117,29 +118,33 @@ deploymentHead dfiDyn sentEv =
117118 let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status"
118119 btnEnabledDyn <- holdDyn btnState $ leftmost [False <$ btnEv, sentEv]
119120 btnEv <-
120− aButtonClassEnabled
121− "page__action button button--secondary button--restore \
122− \classic-popup-handler"
123− "Recover from archive"
124− btnEnabledDyn
121+ largeButton $
122+ def
123+ & #buttonType .~~ pure (Just RestoreLargeButtonType)
124+ & #buttonPriority .~~ SecondaryLargeButton
125+ & #buttonStyle .~~ PageActionLargeButtonStyle
126+ & #buttonText .~~ "Recover from archive"
127+ & #buttonEnabled .~~ btnEnabledDyn
125128 void $ restoreEndpoint (Right . coerce <$> dname) btnEv
126129 pure (never, never)
127130 else mdo
128131 let btnState = not $ isPending . recordedStatus $ dfi ^. field @"status"
129132 btnEnabledDyn <- holdDyn btnState $ not <$> sentEv
130133 editEv <-
131− buttonClassEnabled'
132− "page__action button button--edit popup-handler"
133− "Edit deployment"
134− btnEnabledDyn
135− "button--disabled"
134+ largeButton $
135+ def
136+ & #buttonType .~~ pure (Just EditLargeButtonType)
137+ & #buttonStyle .~~ PageActionLargeButtonStyle
138+ & #buttonText .~~ "Edit deployment"
139+ & #buttonEnabled .~~ btnEnabledDyn
136140 archEv <-
137− buttonClassEnabled'
138− "page__action button button--secondary button--archive \
139− \classic-popup-handler"
140− "Move to archive"
141− btnEnabledDyn
142− "button--disabled"
141+ largeButton $
142+ def
143+ & #buttonType .~~ pure (Just ArchiveLargeButtonType)
144+ & #buttonPriority .~~ SecondaryLargeButton
145+ & #buttonStyle .~~ PageActionLargeButtonStyle
146+ & #buttonText .~~ "Move to archive"
147+ & #buttonEnabled .~~ btnEnabledDyn
143148 pure (R.tag (current dfiDyn) editEv, archEv)
144149 url' <- kubeDashboardUrl dfiDyn
145150 void . dyn $
@@ -148,10 +153,14 @@ deploymentHead dfiDyn sentEv =
148153 blank
149154 ( \url ->
150155 void $
151− aButtonDynClass'
152− "page__action button button--secondary button--logs"
153− "Details"
154− (pure $ "href" =: url <> "target" =: "_blank")
156+ largeButton $
157+ def
158+ { buttonText = "Details"
159+ , buttonType = pure $ Just LogsLargeButtonType
160+ , buttonPriority = SecondaryLargeButton
161+ , buttonStyle = PageActionLargeButtonStyle
162+ , buttonBaseTag = ATag url
163+ }
155164 )
156165 delEv <- confirmArchivePopup archEv $ do
157166 text "Are you sure you want to archive the"
@@ -183,10 +192,7 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $
183192 wrapRequestErrors $ \hReq -> do
184193 let nameDyn = dfiDyn <^.> dfiName
185194 depDyn = dfiDyn <^.> #deployment
186− cfgEv <- deploymentConfigProgressive hReq (depDyn <^.> #deploymentOverrides) (depDyn <^.> #appOverrides)
187− cfgMDyn <-
188− holdClearingWith cfgEv $
189− leftmost [unitEv $ depDyn <^.> #deploymentOverrides, unitEv $ depDyn <^.> #appOverrides]
195+ cfgDyn <- deploymentConfigProgressive hReq (depDyn <^.> #deploymentOverrides) (depDyn <^.> #appOverrides)
190196 divClass "deployment__summary" $ do
191197 divClass "deployment__stat" $ do
192198 elClass "b" "deployment__param" $ text "Status"
@@ -212,47 +218,29 @@ deploymentBody updEv dfiDyn = deploymentBodyWrapper $
212218 void $ simpleList urlsDyn renderMetadataLink
213219 void $
214220 networkView $
215− cfgMDyn <&> \cfgM -> do
216− let showVars :: Getting (Config l) FullConfig (Config l) -> _
217− showVars l = case cfgM of
218− Just cfg -> allEnvsWidget (pure $ cfg ^. l)
219− Nothing -> loadingCommonWidget
220− deploymentSection "App overrides" $ showVars #appConfig
221− deploymentSection "Deployment overrides" $ showVars #depConfig
221+ cfgDyn <&> \cfg -> do
222+ let showVars bL l =
223+ divClass "deployment__widget" $
224+ showNonEditableWorkingOverride (cfg ^. bL) LargeNonEditableWorkingOverrideStyle $
225+ elemsUniq (cfg ^. l)
226+ deploymentSection "Deployment overrides" $ showVars #depConfigLoading #depConfig
227+ deploymentSection "App overrides" $ showVars #appConfigLoading #appConfig
222228 deploymentSection "Actions" $
223229 divClass "table table--actions" $
224− actionsTable updEv nameDyn
230+ actionsTable hReq updEv nameDyn
225231
226−-- | Widget that shows overrides list. It does not depend on their type.
227−allEnvsWidget ::
228− MonadWidget t m =>
229− -- | Overrides list.
230− Dynamic t (Config l) ->
231− m ()
232−allEnvsWidget envsDyn =
233− divClass "deployment__widget" $
234− divClass "listing listing--for-text listing--larger" $
235− void $
236− simpleList (OM.assocs . unConfig <$> envsDyn) $ \envDyn -> do
237− let varDyn = fst <$> envDyn
238− valDyn = snd <$> envDyn
239− divClass "listing__item" $ do
240− el "b" $ do
241− dynText varDyn
242− text ": "
243− dynText valDyn
244−-- ^ Widget with a table of actions that can be performed on a deployment.
232+-- | Widget with a table of actions that can be performed on a deployment.
245233-- It requests deployment data.
246234-- If a request fails it shows an error message,
247235-- otherwise it calls 'actionsTableData', passing the received data.
248−
249236actionsTable ::
250237 MonadWidget t m =>
238+ RequestErrorHandler t m ->
251239 -- | Event notifying about the need to update data.
252240 Event t () ->
253241 Dynamic t DeploymentName ->
254242 m ()
255−actionsTable updEv nameDyn = do
243+actionsTable hReq updEv nameDyn = do
256244 pb <- getPostBuild
257245 respEv <- infoEndpoint (Right <$> nameDyn) pb
258246 let okEv = join . fmap logs <$> fmapMaybe reqSuccess respEv
@@ -262,7 +250,7 @@ actionsTable updEv nameDyn = do
262250 widgetHold_ actionsTableLoading $
263251 leftmost
264252 [ actionsTableError <$ errEv
265− , actionsTableData updEv nameDyn <$> okEv
253+ , actionsTableData hReq updEv nameDyn <$> okEv
266254 ]
267255
268256-- | Header of the actions table.
@@ -272,8 +260,8 @@ actionsTableHead =
272260 el "tr" $ do
273261 el "th" $ text "Action type"
274262 el "th" $ text "Image tag"
275− el "th" $ text "App overrides"
276263 el "th" $ text "Deployment overrides"
264+ el "th" $ text "App overrides"
277265 el "th" $ text "Exit code"
278266 el "th" $ text "Created"
279267 el "th" $ text "Deployment duration"
@@ -301,24 +289,25 @@ actionsTableError = do
301289-- It updates data every time when the supplied event fires.
302290actionsTableData ::
303291 MonadWidget t m =>
292+ RequestErrorHandler t m ->
304293 -- | Event notifying about the need to update data.
305294 Event t () ->
306295 Dynamic t DeploymentName ->
307296 -- | Initial logs.
308297 [DeploymentLog] ->
309298 m ()
310−actionsTableData updEv nameDyn initLogs = do
299+actionsTableData hReq updEv nameDyn initLogs = do
311300 respEv <- infoEndpoint (Right <$> nameDyn) updEv
312− let okEv = join . fmap logs <$> fmapMaybe reqSuccess respEv
301+ let okEv = (>>= logs) <$> fmapMaybe reqSuccess respEv
313302 logsDyn <- holdDyn initLogs okEv
314303 el "tbody" $
315304 void $
316305 simpleList logsDyn $ \logDyn -> do
317− dyn_ $ actinRow <$> logDyn
306+ dyn_ $ actinRow hReq <$> logDyn
318307
319308-- | Data row of the actions table.
320−actinRow :: MonadWidget t m => DeploymentLog -> m ()
321−actinRow DeploymentLog {..} = do
309+actinRow :: RequestErrorHandler t m -> MonadWidget t m => DeploymentLog -> m ()
310+actinRow hReq DeploymentLog {..} = do
322311 el "tr" $ do
323312 el "td" $ do
324313 text $ actionToText action
@@ -327,8 +316,8 @@ actinRow DeploymentLog {..} = do
327316 <> if exitCode == 0 then "status--success" else "status--failure"
328317 divClass statusClass blank
329318 el "td" $ text $ coerce deploymentTag
330− el "td" $ overridesWidget $ deploymentAppOverrides
331− el "td" $ overridesWidget $ deploymentDepOverrides
319+ el "td" $ deploymentOverridesWidget hReq deploymentDepOverrides
320+ el "td" $ applicationOverridesWidget hReq deploymentDepOverrides deploymentAppOverrides
332321 el "td" $ text $ showT $ exitCode
333322 el "td" $ text $ formatPosixToDateTime createdAt
334323 el "td" $ text $ formatDuration duration
octopod-frontend/src/Page/Deployments.hs+73−45
@@ -33,6 +33,7 @@ import Data.These
3333import Frontend.API
3434import Frontend.GHCJS
3535import Frontend.Route
36+import Frontend.UIKit
3637import Frontend.Utils
3738import Page.ClassicPopup
3839import Page.Elements.Links
@@ -77,18 +78,19 @@ deploymentsWidget ::
7778 [DeploymentFullInfo] ->
7879 m ()
7980deploymentsWidget updAllEv dfis = do
80− (showNewDeploymentEv, editEv) <- deploymentsWidgetWrapper $ mdo
81− pageNotification $
82− leftmost
83− [ DPMError
84− "Deployment list update failed, deployment list\
85− \ may be slightly outdated."
86− <$ errUpdEv
87− , DPMClear <$ okUpdEv
88− ]
89− (showNewDeploymentEv', termDyn) <- deploymentsHeadWidget True okUpdEv
90− (okUpdEv, errUpdEv, editEv) <- deploymentsListWidget updAllEv termDyn dfis
91− pure (showNewDeploymentEv', editEv)
81+ (showNewDeploymentEv, editEv) <- deploymentsWidgetWrapper $
82+ wrapRequestErrors $ \hReq -> mdo
83+ pageNotification $
84+ leftmost
85+ [ DPMError
86+ "Deployment list update failed, deployment list\
87+ \ may be slightly outdated."
88+ <$ errUpdEv
89+ , DPMClear <$ okUpdEv
90+ ]
91+ (showNewDeploymentEv', termDyn) <- deploymentsHeadWidget True okUpdEv
92+ (okUpdEv, errUpdEv, editEv) <- deploymentsListWidget hReq updAllEv termDyn dfis
93+ pure (showNewDeploymentEv', editEv)
9294 void $ newDeploymentPopup showNewDeploymentEv never
9395 void $ editDeploymentPopup editEv never
9496
@@ -172,12 +174,13 @@ deploymentsListWidget ::
172174 , SetRoute t (R Routes) m
173175 , MonadReader ProjectConfig m
174176 ) =>
177+ RequestErrorHandler t m ->
175178 Event t () ->
176179 Dynamic t Text ->
177180 -- | Initial deployment data
178181 [DeploymentFullInfo] ->
179182 m (Event t (), Event t (), Event t DeploymentFullInfo)
180−deploymentsListWidget updAllEv termDyn ds = dataWidgetWrapper $ mdo
183+deploymentsListWidget hReq updAllEv termDyn ds = dataWidgetWrapper $ mdo
181184 retryEv <- delay 10 errUpdEv
182185 updRespEv <- listEndpoint $ leftmost [updAllEv, () <$ retryEv]
183186 let okUpdEv = fmapMaybe reqSuccess updRespEv
@@ -192,8 +195,8 @@ deploymentsListWidget updAllEv termDyn ds = dataWidgetWrapper $ mdo
192195 <$> filteredDyn
193196 searchSorting = termDyn $> Just (SortDesc (view #score))
194197 clickedEv <- elementClick
195− editEv <- activeDeploymentsWidget searchSorting clickedEv activeDsDyn
196− archivedDeploymentsWidget searchSorting clickedEv archivedDsDyn
198+ editEv <- activeDeploymentsWidget hReq searchSorting clickedEv activeDsDyn
199+ archivedDeploymentsWidget hReq searchSorting clickedEv archivedDsDyn
197200 pure (() <$ okUpdEv, () <$ errUpdEv, editEv)
198201
199202data SearchedDeploymentFullInfo = SearchedDeploymentFullInfo
@@ -235,6 +238,7 @@ activeDeploymentsWidget ::
235238 , SetRoute t (R Routes) m
236239 , MonadReader ProjectConfig m
237240 ) =>
241+ RequestErrorHandler t m ->
238242 Dynamic t (Maybe (SortDir SearchedDeploymentFullInfo)) ->
239243 -- | Event that carries the clicked DOM element. This event is required by
240244 -- `dropdownWidget'`.
@@ -243,7 +247,7 @@ activeDeploymentsWidget ::
243247 -- | Returns an event carrying editable deployment
244248 -- to \"edit deployment\" sidebar.
245249 m (Event t DeploymentFullInfo)
246−activeDeploymentsWidget searchSorting clickedEv dsDyn =
250+activeDeploymentsWidget hReq searchSorting clickedEv dsDyn =
247251 divClass "data__primary" $
248252 tableWrapper (updated searchSorting $> SortingChanged) $ \sortDyn -> do
249253 let colSortDyn = fmap (contramap (view #deployment)) <$> sortDyn
@@ -255,7 +259,7 @@ activeDeploymentsWidget searchSorting clickedEv dsDyn =
255259 dyn $
256260 emptyDyn <&> \case
257261 False -> do
258− editEvs <- simpleList dsSortedDyn (activeDeploymentWidget clickedEv)
262+ editEvs <- simpleList dsSortedDyn (activeDeploymentWidget hReq clickedEv)
259263 pure $ switchDyn $ leftmost <$> editEvs
260264 True -> do
261265 emptyTableBody $ noDeploymentsWidget
@@ -275,6 +279,7 @@ activeDeploymentWidget ::
275279 , SetRoute t (R Routes) m
276280 , MonadReader ProjectConfig m
277281 ) =>
282+ RequestErrorHandler t m ->
278283 -- | Event that carries the clicked DOM element. This event is required by
279284 -- `dropdownWidget'`.
280285 Event t ClickedElement ->
@@ -282,7 +287,7 @@ activeDeploymentWidget ::
282287 -- | Returns event carrying editable deployment
283288 -- that is required by \"edit deployment\" sidebar.
284289 m (Event t DeploymentFullInfo)
285−activeDeploymentWidget clickedEv dDyn' = do
290+activeDeploymentWidget hReq clickedEv dDyn' = do
286291 dDyn <- holdUniqDyn dDyn'
287292 editEvEv <- dyn $
288293 ffor dDyn $ \s@SearchedDeploymentFullInfo {deployment = d@DeploymentFullInfo {..}} -> do
@@ -297,9 +302,12 @@ activeDeploymentWidget clickedEv dDyn' = do
297302 forM_ (unDeploymentMetadata metadata) (renderMetadataLink . pure)
298303 el "td" tag'
299304 el "td" $
300− overridesWidget $ deployment ^. field @"appOverrides" . coerced
305+ deploymentOverridesWidget hReq (deployment ^. field @"deploymentOverrides" . coerced)
301306 el "td" $
302− overridesWidget $ deployment ^. field @"deploymentOverrides" . coerced
307+ applicationOverridesWidget
308+ hReq
309+ (deployment ^. field @"deploymentOverrides" . coerced)
310+ (deployment ^. field @"appOverrides" . coerced)
303311 el "td" $
304312 text $ formatPosixToDate createdAt
305313 el "td" $
@@ -316,8 +324,18 @@ activeDeploymentWidget clickedEv dDyn' = do
316324 )
317325 $ text "Actions"
318326 body = do
319− btnEditEv <- buttonClassEnabled' "action action--edit" "Edit" (pure enabled) "action--disabled"
320− btnArcEv <- buttonClassEnabled' "action action--archive" "Move to archive" (pure enabled) "action--disabled"
327+ btnEditEv <-
328+ actionButton $
329+ def
330+ & #buttonText .~~ "Edit"
331+ & #buttonEnabled .~~ pure enabled
332+ & #buttonType .~~ Just EditActionButtonType
333+ btnArcEv <-
334+ actionButton $
335+ def
336+ & #buttonText .~~ "Move to archive"
337+ & #buttonEnabled .~~ pure enabled
338+ & #buttonType .~~ Just ArchiveActionButtonType
321339 url' <- kubeDashboardUrl (view #deployment <$> dDyn)
322340 void . dyn $
323341 url'
@@ -325,10 +343,12 @@ activeDeploymentWidget clickedEv dDyn' = do
325343 blank
326344 ( \url ->
327345 void $
328− aButtonClass'
329− "action action--logs"
330− "Details"
331− (pure $ "href" =: url <> "target" =: "_blank")
346+ actionButton
347+ def
348+ { buttonText = "Details"
349+ , buttonType = Just LogsActionButtonType
350+ , buttonBaseTag = ATag url
351+ }
332352 )
333353 pure $
334354 leftmost
@@ -370,13 +390,14 @@ archivedDeploymentsWidget ::
370390 ( MonadWidget t m
371391 , SetRoute t (R Routes) m
372392 ) =>
393+ RequestErrorHandler t m ->
373394 Dynamic t (Maybe (SortDir SearchedDeploymentFullInfo)) ->
374395 -- | Event that carries the clicked DOM element. This event is required by
375396 -- `dropdownWidget'`.
376397 Event t ClickedElement ->
377398 Dynamic t [SearchedDeploymentFullInfo] ->
378399 m ()
379−archivedDeploymentsWidget searchSorting clickedEv dsDyn = do
400+archivedDeploymentsWidget hReq searchSorting clickedEv dsDyn = do
380401 showDyn <- toggleButton
381402 let classDyn = ffor showDyn $ \case
382403 True -> "data__archive data__archive--open"
@@ -394,7 +415,7 @@ archivedDeploymentsWidget searchSorting clickedEv dsDyn = do
394415 void $
395416 simpleList
396417 dsSortedDyn
397− (archivedDeploymentWidget clickedEv)
418+ (archivedDeploymentWidget hReq clickedEv)
398419 True -> emptyTableBody $ noDeploymentsWidget
399420
400421-- | Row with archived deployment.
@@ -402,10 +423,11 @@ archivedDeploymentWidget ::
402423 ( MonadWidget t m
403424 , SetRoute t (R Routes) m
404425 ) =>
426+ RequestErrorHandler t m ->
405427 Event t ClickedElement ->
406428 Dynamic t SearchedDeploymentFullInfo ->
407429 m ()
408−archivedDeploymentWidget clickedEv dDyn' = do
430+archivedDeploymentWidget hReq clickedEv dDyn' = do
409431 dDyn <- holdUniqDyn dDyn'
410432 dyn_ $
411433 ffor dDyn $ \s@SearchedDeploymentFullInfo {deployment = DeploymentFullInfo {..}} -> do
@@ -418,9 +440,12 @@ archivedDeploymentWidget clickedEv dDyn' = do
418440 el "td" $ text "..."
419441 el "td" tag'
420442 el "td" $
421− overridesWidget $ deployment ^. field @"appOverrides" . coerced
443+ deploymentOverridesWidget hReq (deployment ^. field @"deploymentOverrides" . coerced)
422444 el "td" $
423− overridesWidget $ deployment ^. field @"deploymentOverrides" . coerced
445+ applicationOverridesWidget
446+ hReq
447+ (deployment ^. field @"deploymentOverrides" . coerced)
448+ (deployment ^. field @"appOverrides" . coerced)
424449 el "td" $
425450 text $ formatPosixToDate createdAt
426451 el "td" $
@@ -435,12 +460,12 @@ archivedDeploymentWidget clickedEv dDyn' = do
435460 <> "id" =: elId
436461 )
437462 $ text "Actions"
438− body = do
439− btnArcEv <-
440− buttonClass
441− "action action--archive"
442− "Restore from archive"
443− pure btnArcEv
463+ body =
464+ actionButton
465+ def
466+ { buttonText = "Restore from archive"
467+ , buttonType = Just ArchiveActionButtonType
468+ }
444469 btnEv <- dropdownWidget' clickedEv btn body
445470 void $ restoreEndpoint (constDyn $ Right $ dName) btnEv
446471 let route = DashboardRoute :/ Just dName
@@ -485,8 +510,8 @@ tableHeader = do
485510 sortHeader (view dfiName) "Name" SortAsc
486511 el "th" $ text "Links"
487512 el "th" $ text "Tag"
488− el "th" $ text "App overrides"
489513 el "th" $ text "Deployment overrides"
514+ el "th" $ text "App overrides"
490515 sortHeader (view $ field @"createdAt") "Created" SortDesc
491516 sortHeaderInitially (view $ field @"updatedAt") "Changed" SortDesc
492517 el "th" $
@@ -570,13 +595,16 @@ sortHeaderWithInitial f l defaultSorting initSortingM = do
570595 sortBtnEv
571596 sortingChanged
572597 tellMultiEvent . fmapMaybe id $ updated sortDyn
573− let classDyn =
574− fmap ("sort " <>) $
575− sortDyn <&> \case
576− Just (SortDesc _) -> "sort--active sort--desc"
577− Just (SortAsc _) -> "sort--active sort--asc"
578− Nothing -> ""
579− sortBtnEv <- buttonDynClass classDyn (pure l)
598+ sortBtnEv <-
599+ sortButton $
600+ def
601+ & #buttonText .~~ l
602+ & #buttonState
603+ .~~ ( sortDyn <&&> \case
604+ SortDesc _ -> SortDescButtonState
605+ SortAsc _ -> SortAscButtonState
606+ )
607+
580608 tellMultiEvent $ sortBtnEv $> SortingChanged
581609 pure ()
582610
octopod-frontend/src/Page/Popup/EditDeployment.hs+18−4
@@ -16,8 +16,10 @@ import Prelude as P
1616
1717import Common.Types
1818import Common.Utils
19+import Data.Generics.Labels ()
1920import Data.Maybe
2021import Frontend.API
22+import Frontend.UIKit
2123import Frontend.Utils
2224import Reflex.Network
2325import Servant.Reflex
@@ -34,7 +36,7 @@ editDeploymentPopup ::
3436editDeploymentPopup showEv hideEv = sidebar showEv hideEv $ \dfi -> mdo
3537 divClass "popup__body" $ mdo
3638 let dname = dfi ^. dfiName
37− (closeEv', saveEv) <- editDeploymentPopupHeader dname enabledDyn
39+ (closeEv', saveEv) <- editDeploymentPopupHeader dname enabledDyn sentDyn
3840 deploymentMDyn <- editDeploymentPopupBody dfi respEv
3941 respEv <-
4042 holdDyn (pure never) >=> networkView >=> switchHold never $
@@ -64,15 +66,27 @@ editDeploymentPopupHeader ::
6466 DeploymentName ->
6567 -- | Form validation state.
6668 Dynamic t Bool ->
69+ -- | Loading
70+ Dynamic t Bool ->
6771 -- | \"Close\" event and \"Save\" click event.
6872 m (Event t (), Event t ())
69−editDeploymentPopupHeader dname validDyn =
73+editDeploymentPopupHeader dname validDyn loadingDyn =
7074 divClass "popup__head" $ do
71− closeEv <- buttonClass "popup__close" "Close popup"
75+ closeEv <- closePopupButton
7276 elClass "h2" "popup__project" $ text $ "Edit " <> coerce dname
7377 saveEv <-
7478 divClass "popup__operations" $
75− buttonClassEnabled "popup__action button button--save" "Save" validDyn
79+ largeButton $
80+ def
81+ & #buttonStyle .~~ PopupActionLargeButtonStyle
82+ & #buttonText .~~ "Save"
83+ & #buttonEnabled .~~ validDyn
84+ & #buttonType
85+ .~~ ( loadingDyn <&> \case
86+ False -> Just SaveLargeButtonType
87+ True -> Just LoadingLargeButtonType
88+ )
89+
7690 divClass "popup__menu drop drop--actions" blank
7791 pure (closeEv, saveEv)
7892
octopod-frontend/src/Page/Popup/NewDeployment.hs+16−4
@@ -18,6 +18,7 @@ import Common.Types
1818import Common.Validation (isNameValid)
1919import Data.Maybe
2020import Frontend.API
21+import Frontend.UIKit
2122import Frontend.Utils
2223import Reflex.Network
2324import Servant.Reflex
@@ -34,7 +35,7 @@ newDeploymentPopup showEv hideEv = void $
3435 sidebar showEv hideEv $
3536 const $ mdo
3637 divClass "popup__body" $ mdo
37− (closeEv', saveEv) <- newDeploymentPopupHeader enabledDyn
38+ (closeEv', saveEv) <- newDeploymentPopupHeader enabledDyn sentDyn
3839 deploymentMDyn <- newDeploymentPopupBody respEv
3940 respEv <-
4041 holdDyn (pure never) >=> networkView >=> switchHold never $
@@ -59,14 +60,25 @@ newDeploymentPopup showEv hideEv = void $
5960newDeploymentPopupHeader ::
6061 MonadWidget t m =>
6162 Dynamic t Bool ->
63+ -- | Loading
64+ Dynamic t Bool ->
6265 m (Event t (), Event t ())
63−newDeploymentPopupHeader enabledDyn =
66+newDeploymentPopupHeader enabledDyn loadingDyn =
6467 divClass "popup__head" $ do
65− closeEv <- buttonClass "popup__close" "Close popup"
68+ closeEv <- closePopupButton
6669 elClass "h2" "popup__project" $ text "Create new deployment"
6770 saveEv <-
6871 divClass "popup__operations" $
69− buttonClassEnabled "popup__action button button--save" "Save" enabledDyn
72+ largeButton $
73+ def
74+ & #buttonStyle .~~ PopupActionLargeButtonStyle
75+ & #buttonText .~~ "Save"
76+ & #buttonEnabled .~~ enabledDyn
77+ & #buttonType
78+ .~~ ( loadingDyn <&> \case
79+ False -> Just SaveLargeButtonType
80+ True -> Just LoadingLargeButtonType
81+ )
7082 divClass "popup__menu drop drop--actions" blank
7183 pure (closeEv, saveEv)
7284