mirror of https://github.com/cncf/landscapeapp.git
Compare commits
641 Commits
Author | SHA1 | Date |
---|---|---|
|
a882d0675b | |
|
5ffa213fc9 | |
|
2344353df5 | |
|
b7e063d94e | |
|
e915165065 | |
|
3de416fa3a | |
|
7dd727758c | |
|
5235155c90 | |
|
d8e4f84e5c | |
|
61014620c2 | |
|
8137985380 | |
|
8c4b4941de | |
|
b2c7d52080 | |
|
5e05834a46 | |
|
450ca5e141 | |
|
d524f88001 | |
|
12667a2b52 | |
|
52f2254d91 | |
|
10155fd1ea | |
|
bc541000c9 | |
|
b3638c04aa | |
|
bd563a8465 | |
|
485cb1158d | |
|
1a3e59e157 | |
|
b784d7f896 | |
|
b45ce81784 | |
|
808d219202 | |
|
2bda551a74 | |
|
3775e220a3 | |
|
0b41bc54da | |
|
97e8e882a3 | |
|
62fa27892c | |
|
8ce89db6e3 | |
|
0b66094876 | |
|
d4409f142a | |
|
28973ed0bc | |
|
7a983a6583 | |
|
af91ca7e16 | |
|
d4850f64fd | |
|
af77ba322a | |
|
73ac6d4efe | |
|
ffbb154c51 | |
|
90d9bead25 | |
|
a17dc8d542 | |
|
0b0314f636 | |
|
d8c453c471 | |
|
b85dbd9100 | |
|
3a01088cda | |
|
b0f6c86335 | |
|
06d592b3ba | |
|
60ea7c8551 | |
|
f9b1fbe8ab | |
|
d9020382c6 | |
|
1c5e8aaee3 | |
|
5b8254f1b2 | |
|
e12aaca02f | |
|
2775c25b5b | |
|
4ffbff6f9c | |
|
1ac6a2c97f | |
|
9aa1006f60 | |
|
b5802a5545 | |
|
fe9351f824 | |
|
8434098eaf | |
|
8dc5fd6c82 | |
|
f922f3831d | |
|
fd3fc93889 | |
|
4723c94b56 | |
|
97dd8d8f8b | |
|
aebf1d588e | |
|
1eb67b3668 | |
|
a12ce99e58 | |
|
862a33eacd | |
|
495c7997e8 | |
|
bd8f8f8d03 | |
|
cc125f24dd | |
|
250bbe68b8 | |
|
f987c05cb7 | |
|
e577cbd700 | |
|
2ae215c8c2 | |
|
901182882c | |
|
a657ae8540 | |
|
628c6f71a5 | |
|
60e5c99514 | |
|
2dc0d29598 | |
|
d330c59d41 | |
|
e0429f758d | |
|
95e60ef4a9 | |
|
9304e3a777 | |
|
c5b75d14e2 | |
|
899dca8561 | |
|
b762217ab1 | |
|
8a8003b41f | |
|
c99ae2514a | |
|
fdab31e0f7 | |
|
e449569b86 | |
|
d6c2ecad33 | |
|
ad5d51afe7 | |
|
a49ff4b891 | |
|
5a572110fe | |
|
f6dbef23f3 | |
|
461c9d6d8a | |
|
dae74b9d6d | |
|
21d8621271 | |
|
60d7704d7d | |
|
526822323c | |
|
47a6b6fb00 | |
|
17f088a0de | |
|
f6cd147f77 | |
|
1088b323ae | |
|
8c75b38bd2 | |
|
e347630c9e | |
|
674dd36513 | |
|
2ba49cc7a6 | |
|
1ab80e01c1 | |
|
c5e8289c77 | |
|
1c6df12abf | |
|
5e5110ca52 | |
|
875014bac0 | |
|
05723ec72f | |
|
68452b5dfd | |
|
53d01e8982 | |
|
0d0004ae18 | |
|
921e8a3cdf | |
|
544ba79b1d | |
|
645e77ea29 | |
|
8cd5ba7a86 | |
|
12de9ea4f8 | |
|
848670d1e6 | |
|
b03a7878f2 | |
|
c71dea1f6e | |
|
14c675ecf5 | |
|
0889f7833a | |
|
6b234663e9 | |
|
2b0075056a | |
|
fba9b1c416 | |
|
d338c0bb39 | |
|
fa34affd2d | |
|
1437c53605 | |
|
04ef289bae | |
|
90daf28206 | |
|
cb1b161a76 | |
|
8a281f8be2 | |
|
a59ccb133f | |
|
7b4580f15d | |
|
b1748d9ad8 | |
|
8316ec3a9c | |
|
84ee4cae5d | |
|
8728f7a67d | |
|
34a2801c12 | |
|
4274e2dce0 | |
|
35e442b220 | |
|
9dd4cbbce7 | |
|
f420554102 | |
|
51b07fd90f | |
|
5e8272e951 | |
|
dc9ab23e3c | |
|
944b9d4ca5 | |
|
1d976569e6 | |
|
4afbf066e1 | |
|
1311cc87b3 | |
|
b36a6dde0f | |
|
d30b8ad818 | |
|
5a4955b941 | |
|
ae5ce3d9b0 | |
|
6bd23218e7 | |
|
f0fe09fbf7 | |
|
ca1d9882af | |
|
e286ddaef9 | |
|
b4f92d7bb9 | |
|
fab5d5ab34 | |
|
3eb7b5d0f0 | |
|
5b9f12d80d | |
|
a292620f7a | |
|
7a51987a03 | |
|
282460a37a | |
|
84749ce870 | |
|
ebb53fe075 | |
|
c72a1e6148 | |
|
1d9f8c31d6 | |
|
745e5057a4 | |
|
2eeafd345d | |
|
abc521cefc | |
|
1aa6cf747e | |
|
d2719d926b | |
|
03321ab164 | |
|
54a7575b94 | |
|
e8e823e25b | |
|
e8a6db4982 | |
|
c1c2fb26d1 | |
|
9009ceb9ad | |
|
991798616a | |
|
4c5e1df907 | |
|
11dba210df | |
|
e4e706e992 | |
|
3cd5090092 | |
|
dea1363831 | |
|
b0a6aa9ef8 | |
|
619f7b7847 | |
|
7e645dbc3e | |
|
6dba098fcc | |
|
0793fafd05 | |
|
1f5285428e | |
|
9adb65ee38 | |
|
892362ea1f | |
|
c080c125aa | |
|
550d8f30d7 | |
|
a79556b99e | |
|
dec187b4e5 | |
|
52efed17d6 | |
|
611cf5f654 | |
|
20f32d35d0 | |
|
54fade6a94 | |
|
1f7c082cca | |
|
0d1e0e77e6 | |
|
853af61ab4 | |
|
480aea9d04 | |
|
67a3b089de | |
|
27f62a94ae | |
|
b21c5fd597 | |
|
926351f335 | |
|
409f06a363 | |
|
87f8445b71 | |
|
cc71725661 | |
|
e793eb733b | |
|
3eb689d578 | |
|
ee27648dfc | |
|
bed59adea3 | |
|
3178e88459 | |
|
7babd5b764 | |
|
0d7155766b | |
|
adff0ebd1b | |
|
86e23443e4 | |
|
ae6b3f006e | |
|
c484efebba | |
|
ce1d6dbdfc | |
|
dafa28aa1e | |
|
9065ea761a | |
|
ab0c184625 | |
|
403a5651e0 | |
|
a4e97097f8 | |
|
2f058973f0 | |
|
6bed68ba99 | |
|
9d20d4fe1a | |
|
6d9843cddf | |
|
e9a3448952 | |
|
5dcf4a478a | |
|
cdbce7aa84 | |
|
eb7d99d9ba | |
|
9b5c3c6ffb | |
|
71b54d5a62 | |
|
fe515ca7d2 | |
|
97f425b465 | |
|
c7e219b52c | |
|
9a150a523c | |
|
0588a0a63c | |
|
3d8522ba7e | |
|
dfce515043 | |
|
e180f8c6e5 | |
|
03cb35fad4 | |
|
fe5fdec626 | |
|
624413a037 | |
|
6bac7f4231 | |
|
6356a9eeec | |
|
6f01c906ef | |
|
fcd219cc15 | |
|
bb7befeafc | |
|
53072942dd | |
|
ca3853a504 | |
|
a8b5d5979b | |
|
18fbd506e3 | |
|
353484daab | |
|
e2889a85de | |
|
34e404002e | |
|
2ea8b8a4fe | |
|
46f6bc0790 | |
|
102c441cec | |
|
4ca0570ab2 | |
|
7d3e5489b2 | |
|
b91e8574b3 | |
|
cfc9be9e4b | |
|
0f4caabf66 | |
|
ab84dbd421 | |
|
15fd6a7b04 | |
|
d3841d6134 | |
|
041aee358f | |
|
6c78d61d9b | |
|
f175ac012d | |
|
d715ddb9a2 | |
|
b79b530773 | |
|
f0e891ac68 | |
|
84df15804a | |
|
21abb35b19 | |
|
d0f6963c95 | |
|
4ea4636537 | |
|
3a9ccd4cb2 | |
|
81829f4c97 | |
|
e29b88103f | |
|
b80b2ab739 | |
|
0e49b73a60 | |
|
74280f2655 | |
|
269bc5c4a7 | |
|
ca08b4b8ef | |
|
93fd1b7669 | |
|
73c8cdf11d | |
|
552e3eef71 | |
|
15816250e7 | |
|
375a726566 | |
|
15072e1fbc | |
|
593d621cd6 | |
|
e100971bc8 | |
|
30c5c449e1 | |
|
43d8bc22d8 | |
|
738a9a2dd3 | |
|
7e4d00d970 | |
|
179a5ac15f | |
|
57dcb94859 | |
|
1d6a0a5c7e | |
|
f4087e464c | |
|
fb1231d0d7 | |
|
768d27e5e5 | |
|
f625029e9c | |
|
1eff4b4261 | |
|
a8313f3374 | |
|
24ac7a7005 | |
|
3ce07ab2b6 | |
|
9b299d1b8f | |
|
c21212ece4 | |
|
f76e296894 | |
|
d87adfcda3 | |
|
a54a0004bd | |
|
5a90978744 | |
|
6364d40750 | |
|
76a617461a | |
|
eef21412d7 | |
|
96d4a303ee | |
|
3e474ccea4 | |
|
cfd7480136 | |
|
f9fdcdbd59 | |
|
5f66d6ac18 | |
|
de11917ba7 | |
|
5db9201602 | |
|
cbd680513a | |
|
33f21969c4 | |
|
62e8b76d43 | |
|
c4d81d9aa1 | |
|
3913522f9f | |
|
7ea839aa60 | |
|
9dbc2c965c | |
|
06f143713f | |
|
e2ec71d6b3 | |
|
23ea56455c | |
|
0f1e969ce8 | |
|
0b309dd934 | |
|
d35c12be33 | |
|
07c9a8a2b7 | |
|
1ce8814051 | |
|
a1e8f1a0e7 | |
|
455a516dc9 | |
|
9c048a3e85 | |
|
be1560e673 | |
|
c20cfa01a6 | |
|
01874e0e3b | |
|
843562a311 | |
|
1bebfa3157 | |
|
66e31f9e84 | |
|
10f520eb44 | |
|
239a6f8945 | |
|
2dd94b2aa6 | |
|
0a1686ed11 | |
|
66588446d4 | |
|
2bcb74724a | |
|
89d96db72a | |
|
1952334a5b | |
|
116169d8dc | |
|
b4340202a8 | |
|
d5752e767d | |
|
6c6dbda39a | |
|
c41f6841da | |
|
76054075c9 | |
|
3289bc70d3 | |
|
4b18fff019 | |
|
2ea74c622f | |
|
6218cb44f6 | |
|
74eee81754 | |
|
8368ae1e31 | |
|
98d4a5b542 | |
|
393974c70c | |
|
a675519cf3 | |
|
67243e245d | |
|
04600d61af | |
|
0932ea7b2c | |
|
638d4083eb | |
|
236c2c55e2 | |
|
adc9f0ae84 | |
|
cb0b007ab1 | |
|
4b73f5a5b1 | |
|
b128ddb8ec | |
|
a8ffa7f348 | |
|
993280bd3e | |
|
d296f5b329 | |
|
50f8f1de92 | |
|
480bef0b2b | |
|
1edbdbc1b6 | |
|
d8f51c300a | |
|
bfeb028bdb | |
|
43f65a168e | |
|
ca6defb2e1 | |
|
3a7f1bbf78 | |
|
7b250a3814 | |
|
9b3457db73 | |
|
2c3977217a | |
|
35d3cde36c | |
|
6e74c695b3 | |
|
4dd6bdef7a | |
|
0f02dc7a3c | |
|
dbcf5f97b5 | |
|
1dbe339543 | |
|
b1c9d6f578 | |
|
3206bb6916 | |
|
22f1dede4c | |
|
a57eaea9f2 | |
|
2dd5b3539e | |
|
3ed651b31c | |
|
3455457d27 | |
|
709cc6ae1a | |
|
b26e8cb723 | |
|
173f4c9428 | |
|
afb7f94cc9 | |
|
690d221952 | |
|
83200685a6 | |
|
11e47ff08c | |
|
0c61a45a27 | |
|
977fe9bc0c | |
|
bca0240a40 | |
|
712225e18e | |
|
6de44ddd9a | |
|
47ab527a96 | |
|
87812f17ce | |
|
a7de1ec0fe | |
|
9a000ba213 | |
|
0b0a37ea84 | |
|
df4068fa56 | |
|
bfb5c91ec5 | |
|
f212b81ce3 | |
|
0b6ea66e35 | |
|
97fc474895 | |
|
ecc3ae39ab | |
|
075aec7fad | |
|
08280cde3f | |
|
8fa13c6305 | |
|
47a8aaf1ee | |
|
a9410d6595 | |
|
6b89f6b7b9 | |
|
440064ad04 | |
|
3ef56b57c7 | |
|
8ce37a0062 | |
|
6db0830b48 | |
|
ce0b79386f | |
|
50913e4202 | |
|
0e01a542b3 | |
|
2e61e37f4c | |
|
7426aca10e | |
|
777f88ad38 | |
|
5f0338e7cc | |
|
6cc843050d | |
|
a99b934b79 | |
|
667b3d3d3b | |
|
e29eb88635 | |
|
af05433430 | |
|
58af5f2cb9 | |
|
01218888e4 | |
|
f0ab12bbeb | |
|
9b7f2580ba | |
|
3a8028d93d | |
|
048f4b091a | |
|
1451bcb653 | |
|
cb5c6540d3 | |
|
a2d3449a29 | |
|
f6d436eb9d | |
|
5f1349e20c | |
|
3cefeff106 | |
|
37c6a9985c | |
|
37340e9528 | |
|
7d63c8bd19 | |
|
7b141f3be9 | |
|
0064ad4463 | |
|
84f5570e60 | |
|
ce3c367d49 | |
|
c49c6b4e62 | |
|
6b1446f152 | |
|
f6b811c371 | |
|
bf25e70131 | |
|
4890c29436 | |
|
ab250c8eae | |
|
2330307874 | |
|
7d29956281 | |
|
c7a6581e1a | |
|
b53439ac71 | |
|
0d85b0c25f | |
|
ed35f69e9e | |
|
565c4edddd | |
|
9526bf5287 | |
|
1939df675b | |
|
f9059cfb43 | |
|
c359270d66 | |
|
838deafa8a | |
|
2ae1853c61 | |
|
d7b859b78f | |
|
7204bb4867 | |
|
9b5f6d926d | |
|
edf389cc27 | |
|
a0b46903c9 | |
|
df3895d8e5 | |
|
c25954631d | |
|
13808315ba | |
|
d6dc2ca2cc | |
|
fec7c7beb2 | |
|
5fdd8d7335 | |
|
60c53ef6f7 | |
|
68a30c95e7 | |
|
da81be6743 | |
|
4a8aeccac2 | |
|
781bbc9030 | |
|
e6b97cbfeb | |
|
435fef8c82 | |
|
b53079a979 | |
|
aa52865117 | |
|
1c05daeded | |
|
80ead92e7f | |
|
171c070afa | |
|
bdf698179b | |
|
0f8d03d236 | |
|
02376e7cf1 | |
|
12effe5b09 | |
|
af378f51e4 | |
|
74aef4afcf | |
|
11f7ff8e52 | |
|
fe90b2246e | |
|
5fe795b681 | |
|
bb567278d2 | |
|
9547b22364 | |
|
99cb6c5a09 | |
|
948b4c33d7 | |
|
c550cd3dae | |
|
3e6dfff5c5 | |
|
4c75b7d7a5 | |
|
02ba87d98b | |
|
6a203c123f | |
|
3f3121f2ad | |
|
8462d3ef8b | |
|
4d033eb0a7 | |
|
165d081397 | |
|
f97e85abfd | |
|
cd74231b3d | |
|
91c6e9069c | |
|
82b76a0222 | |
|
9bbb274ea1 | |
|
c42bad1e4d | |
|
1fb96119fe | |
|
477debd098 | |
|
c210533da6 | |
|
dd42e5937b | |
|
b635d69b9c | |
|
2fe3b8b434 | |
|
71d555b489 | |
|
47fb46f70b | |
|
f506ef87c0 | |
|
651c46e103 | |
|
69de9493b9 | |
|
18378186ec | |
|
72e033e890 | |
|
5e4d9b9918 | |
|
c0d0e582eb | |
|
49d5b2b286 | |
|
b24508a0c0 | |
|
9c6365789b | |
|
8381f8dd5f | |
|
23c486f3f7 | |
|
9fabb818d5 | |
|
032adab9fe | |
|
6bef34bbc0 | |
|
f81b0113c8 | |
|
933ac237f2 | |
|
9dbf4d050f | |
|
3f84198bc1 | |
|
3493647a70 | |
|
ce83288889 | |
|
a24c08199a | |
|
5b8cd6d1e2 | |
|
c8fb3daa46 | |
|
84bff5988e | |
|
f6bfad6d98 | |
|
5c1d73f085 | |
|
0f3c32b32a | |
|
a03d0b1977 | |
|
df630babc4 | |
|
f28121ef9d | |
|
4e588ee686 | |
|
1b136bed88 | |
|
8c607c1ef9 | |
|
b15f470baf | |
|
edd1bcdf61 | |
|
acc6a0790a | |
|
0708ac4ea9 | |
|
2b5394176f | |
|
f68184e7c0 | |
|
311fce2345 | |
|
d1ae8cc8f9 | |
|
28cdea2552 | |
|
5f471b9a64 | |
|
95c8ae372d | |
|
26c55c0a00 | |
|
7ad1a5f5cb | |
|
abc6f105a5 | |
|
d2866d49c7 | |
|
b3730faec6 | |
|
22066e2912 | |
|
c71bf3b05b | |
|
6633a86253 | |
|
1d689a3560 | |
|
e54124a5bc | |
|
664ea9e73e | |
|
7871c176e2 | |
|
8aa9723d58 | |
|
7e31fadf19 | |
|
15a9bafe57 | |
|
98486e0dcc | |
|
b188515184 | |
|
0b1a9fb6f3 | |
|
893c58ee5c | |
|
077b890e52 | |
|
b121ef108f | |
|
1e168dcb20 | |
|
8e9690cbc6 | |
|
70bc49b666 | |
|
748b6f9ffa | |
|
e79c67cd20 | |
|
11d8c16b0f | |
|
240183fa1e | |
|
41103b49d5 | |
|
480e059a04 |
13
.babelrc
13
.babelrc
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"ignore": [".yarn", ".pnp.js"],
|
||||
"presets": [ ["babel-preset-latest-node", {target: "current"}] ],
|
||||
"plugins": [
|
||||
["module-resolver", {
|
||||
"root": ["./src"],
|
||||
"alias": {
|
||||
"project/settings.yml": "./tools/resolve-settings.js",
|
||||
"project/lookup.json": "./tools/resolve-lookup.js",
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
netlify/jsyaml.js
|
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
"extends": "eslint:recommended",
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
},
|
||||
"rules": {
|
||||
"no-useless-escape": 0,
|
||||
"no-prototype-builtins": 0,
|
||||
"no-empty": 0,
|
||||
"no-control-regex": 0
|
||||
}
|
||||
}
|
|
@ -28,6 +28,8 @@ node_modules
|
|||
#dist folder
|
||||
dist
|
||||
|
||||
public
|
||||
|
||||
# IDEA/Webstorm project files
|
||||
.idea
|
||||
*.iml
|
||||
|
@ -46,3 +48,7 @@ dist
|
|||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
.pnp.*
|
||||
|
||||
.next
|
||||
out
|
||||
tmp
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,2 +1,3 @@
|
|||
yarnPath: ".yarn/releases/yarn-berry.js"
|
||||
enableProgressBars: false
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
||||
|
|
13
INSTALL.md
13
INSTALL.md
|
@ -7,21 +7,14 @@
|
|||
|
||||
## Install on Linux
|
||||
1. `git clone git@github.com:cncf/landscape.git`
|
||||
2. Please follow [this script](https://github.com/cncf/landscapeapp/blob/master/update_server/setup.bash) to install correct versions of `nodejs` and other packages on Linux.
|
||||
2. Please follow [this script](https://github.com/cncf/landscapeapp/blob/HEAD/update_server/setup.bash) to install correct versions of `nodejs` and other packages on Linux.
|
||||
|
||||
## Local development
|
||||
1. `git pull`
|
||||
2. `npm install` (installs dependencies)
|
||||
* `npm run open:src` (starts a development server) or
|
||||
* `npm build`, then `npm run open:dist` (compiles and opens a production build)
|
||||
|
||||
## Review build details
|
||||
1. `npm run build`
|
||||
1. `open dist/report.html`
|
||||
Please follow the [directions](https://github.com/cncf/landscapeapp#installing-locally) to setup aliases for running your landscape.
|
||||
|
||||
## Updating data
|
||||
|
||||
After making your changes to `landscape.yml`, run `npm run fetch` to fetch any needed data and generate [processed_landscape.yml](https://github.com/cncf/landscape/blob/master/processed_landscape.yml) and [data.json](https://github.com/cncf/landscapeapp/blob/master/src/data.json).
|
||||
After making your changes to `landscape.yml`, run `npm run fetch` to fetch any needed data and generate [processed_landscape.yml](https://github.com/cncf/landscape/blob/HEAD/processed_landscape.yml) and [data/items.json](https://landscape.cncf.io/data/items.json).
|
||||
|
||||
`npm run fetch` runs in 4 modes of increasingly aggressive downloading, with a default to easy. Reading data from the cache (meaning from processed_landscape.yml) means that no new data is fetched if the project/product already exists. The modes are:
|
||||
|
||||
|
|
190
README.md
190
README.md
|
@ -2,13 +2,82 @@
|
|||
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/2434) [](https://www.npmjs.com/package/interactive-landscape) [](https://david-dm.org/cncf/landscapeapp) [](https://app.netlify.com/sites/landscapeapp/deploys)
|
||||
|
||||
The landscapeapp is an upstream NPM [module](https://www.npmjs.com/package/interactive-landscape) that supports building interactive landscape websites such as the [CNCF Cloud Native Landscape](https://landscape.cncf.io) ([source](https://github.com/cncf/landscape)) and the [LF Artificial Intelligence Landscape](https://landscape.lfai.foundation) ([source](https://github.com/lfai/lfai-landscape)). The application is managed by [Dan Kohn](https://www.dankohn.com) of [CNCF](https://www.cncf.io) and is under active development by [Andrey Kozlov](https://github.com/ZeusTheTrueGod) (who did most of the development to date) and [Jordi Noguera](https://jordinl.com).
|
||||
- [Adding and managing landscape entries](#adding-and-managing-landscape-entries)
|
||||
- [Logos](#logos)
|
||||
- [SVGs Can't Include Text](#svgs-cant-include-text)
|
||||
- [CloudConvert](#cloudconvert)
|
||||
- [Adobe Illustrator](#adobe-illustrator)
|
||||
- [Inkscape](#inkscape)
|
||||
- [Crunchbase Requirement](#crunchbase-requirement)
|
||||
- [External Data](#external-data)
|
||||
- [Creating a New Landscape](#creating-a-new-landscape)
|
||||
- [API Keys](#api-keys)
|
||||
- [Installing Locally](#installing-locally)
|
||||
- [Adding to a google search console](#adding-to-a-google-search-console)
|
||||
- [Vulnerability reporting](#vulnerability-reporting)
|
||||
- [Continuous Integration and NPM Publishing](#continuous-integration-and-npm-publishing)
|
||||
- [Building an individual landscape](#building-an-individual-landscape)
|
||||
- [Running "remotely" on our build server (fast and by default)](#running-remotely-on-our-build-server-fast-and-by-default)
|
||||
- [Running "locally" on Netlify instances (if the remote server is broken)](#running-locally-on-netlify-instances-if-the-remote-server-is-broken)
|
||||
- [Building this repo, `landscapeapp` on a Netlify](#building-this-repo-landscapeapp-on-a-netlify)
|
||||
- [Setting up our build server to speed up Netlify builds](#setting-up-our-build-server-to-speed-up-netlify-builds)
|
||||
- [Keeping Project Up to Date](#keeping-project-up-to-date)
|
||||
- [Embed landscape in a web site](#embed-landscape-in-a-web-site)
|
||||
- [Generating a Guide](#generating-a-guide)
|
||||
|
||||
|
||||
The landscapeapp is an upstream NPM [module](https://www.npmjs.com/package/interactive-landscape) that supports building interactive landscape websites such as the [CNCF Cloud Native Landscape](https://landscape.cncf.io) ([source](https://github.com/cncf/landscape)) and the [LF Artificial Intelligence Landscape](https://landscape.lfai.foundation) ([source](https://github.com/lfai/lfai-landscape)). The application is under active development by [Andrey Kozlov](https://github.com/AndreyKozlov1984) and [Jordi Noguera](https://jordinl.com).
|
||||
|
||||
In addition to creating fully interactive sites, the landscapeapp builds static images on each update. See examples in [ADOPTERS.md](ADOPTERS.md). All current [Linux Foundation](https://linuxfoundation.org) landscapes are listed in [landscapes.yml](landscapes.yml).
|
||||
|
||||
## Images
|
||||
## Adding and managing landscape entries
|
||||
|
||||
The most challenging part of creating a new landscape is finding SVG images for all projects and companies. These landscapes represent a valuable resource to a community in assembling all related projects, creating a taxonomy, and providing the up-to-date logos, and unfortunately, there are no shortcuts.
|
||||
When creating new entries, the only 4 required fields are `name`, `homepage_url`, `logo`, and `crunchbase`.
|
||||
|
||||
```yaml
|
||||
- item:
|
||||
name: <entry name>
|
||||
homepage_url: <website for entry>
|
||||
# filename in hosted_logos folder. Put the svg file into the hosted_logos
|
||||
folder and reference its name.
|
||||
logo: <logo for entry>
|
||||
crunchbase: <twitter for entry>
|
||||
```
|
||||
|
||||
Additional keys that can be set are defined below:
|
||||
|
||||
```yaml
|
||||
# url for the Twitter account; Only add if the value in Crunchbase is incorrect
|
||||
twitter:
|
||||
# url to the repo for the project; will fetch stats if it starts with https://github.com/. If you add a `repo_url` the card will be white instead of grey.
|
||||
repo_url:
|
||||
# url to the GitHub organization for the project; when using `repo_url`, `project_org` can be set pointing to an organization on GitHub, this will have the effect of pulling the information for all the repos belonging to that organization but using `repo_url` for information regarding license and best practices.
|
||||
project_org:
|
||||
# additional repos for the project; will fetch stats if they start with https://github.com/
|
||||
additional_repos:
|
||||
# Stock Ticker for the organization of the project/entry; normally pulls from Crunchbase but can be overridden here. For delisted and many foreign countries, you'll need to add `stock_ticker` with the value to look up on Yahoo Finance to find the market cap.
|
||||
stock_ticker:
|
||||
# description of the entry; if not set pulls from the GitHub repo description
|
||||
description:
|
||||
# default branch to reference if not the main one for the repo
|
||||
branch:
|
||||
# if the entry is a project hosted by the project, let's you set the maturity level. Should be a value in relations.values.children.id in settings.yml
|
||||
project:
|
||||
# url for the CII Best Practices entry if it's not directly mapped to the repo_url
|
||||
url_for_bestpractices:
|
||||
# set to false if a repo_url is given but the entry is a project that isn't open source
|
||||
open_source:
|
||||
# allows multiple entries with the same repo_url; set for each instance
|
||||
allow_duplicate_repo:
|
||||
# set to true if you are using an anonymous organization. You will also need anonymous_organization set in settings.yml
|
||||
unnamed_organization:
|
||||
```
|
||||
|
||||
For some of the key, there is some guidance as listed below.
|
||||
|
||||
### Logos
|
||||
|
||||
The most challenging part of creating a new landscape is finding SVG images for all projects and companies. These landscapes represent a valuable resource to a community in assembling all related projects, creating a taxonomy, and providing up-to-date logos, and unfortunately, there are no shortcuts.
|
||||
|
||||
Do *not* try to convert PNGs to SVGs. You can't automatically go from a low-res to a high-res format, and you'll just waste time and come up with a substandard result. Instead, invest your time finding SVGs and then (when necessary) having a graphic designer recreate images when high res ones are not available.
|
||||
|
||||
|
@ -16,19 +85,28 @@ Tips for finding high quality images:
|
|||
|
||||
- Google images is often the best way to find a good version of the logo (but ensure it's the up-to-date version). Search for [grpc logo filetype:svg](https://www.google.com/search?q=grpc+logo&tbs=ift:svg,imgo:1&tbm=isch) but substitute your project or product name for grpc.
|
||||
- Wikipedia also is a good source for high quality logos ( search in either the main [Wikipedia](https://en.wikipedia.org/w/index.php?sort=relevance&search=svg&title=Special%3ASearch&profile=advanced&fulltext=1&advancedSearch-current=%7B%7D&ns6=1) or [Wikipedia Commons](https://commons.wikimedia.org/w/index.php?sort=relevance&search=svg&title=Special%3ASearch&profile=advanced&fulltext=1&advancedSearch-current=%7B%7D&ns0=1&ns6=1&ns12=1&ns14=1&ns100=1&ns106=1) ).
|
||||
- VectorLogoZone ( https://www.vectorlogo.zone/ )
|
||||
- Also search for 'svg' in the GitHub for the project, as sometimes projects will embed them there.
|
||||
|
||||
For new landscapes of any size, you will probably need a graphic artist to rebuild some of the logos for you.
|
||||
|
||||
If the project is hosted/sponsored by an organization but doesn't have a logo, best practice is to use that organization's logo with the title of the project underneath ( [example](https://landscape.cncf.io/selected=netflix-eureka) ). You can use a tool such as [Inkscape](https://inkscape.org/) to add the text.
|
||||
|
||||
If you get an error with the image that it has a PNG embeded, you will need to find a different SVG that doesn't include a PNG or work with a graphic artist to rebuild the logo.
|
||||
If you get an error with the image that it has a PNG embedded, you will need to find a different SVG that doesn't include a PNG or work with a graphic artist to rebuild the logo.
|
||||
|
||||
## SVGs Can't Include Text
|
||||
#### SVGs Can't Include Text
|
||||
|
||||
SVGs need to not rely on external fonts so that they will render correctly in any web browser, whether or not the correct fonts are installed. That means that all embedded text and tspan elements need to be converted to objects. Use of SVGs with embedded text will fail with an error. You can convert the SVGs as follows:
|
||||
SVGs need to not rely on external fonts so that they will render correctly in any web browser, whether or not the correct fonts are installed. That means that all embedded text and tspan elements need to be converted to objects. Use of SVGs with embedded text will fail with an error. You can convert the SVGs as using one of the tools below.
|
||||
|
||||
Here are the steps in Adobe Illustrator to create convert text to objects:
|
||||
##### CloudConvert
|
||||
|
||||
1. Go to https://cloudconvert.com/, and click 'Select File' and select the SVG file.
|
||||
2. Next to 'Convert to', click the dropdown and select 'SVG'
|
||||
3. There will be wrench icon that appears. Click that.
|
||||
4. For the option 'Text To Path', select 'Yes' and then click 'Okay'
|
||||
5. Click 'Convert' to do the conversion and the download the converted file.
|
||||
|
||||
##### Adobe Illustrator
|
||||
|
||||
1. Select all text
|
||||
1. With the text selected, go to Object > Expand in the top menu
|
||||
|
@ -37,23 +115,35 @@ Here are the steps in Adobe Illustrator to create convert text to objects:
|
|||
1. This will open a SVG options box, make sure to set Decimal to 5 (that is the highest possible, so to ensure that sufficient detail is preserved)
|
||||
1. Click Okay to export
|
||||
|
||||
Here are the steps for Inkscape:
|
||||
##### Inkscape
|
||||
|
||||
1. Select the text
|
||||
1. Ctrl+K (path combine)
|
||||
1. Ctrl+J (dynamic offset)
|
||||
1. Save
|
||||
|
||||
## New Entries
|
||||
|
||||
When creating new entries, the only 4 required fields are `name`, `homepage_url`, `logo`, and `crunchbase`. It's generally easier to have the landscape fetch an SVG by adding it's URL rather than saving it yourself in the `hosted_logos` folder. Only add a `twitter` if the value in Crunchbase is incorrect. For delisted and many foreign countries, you'll need to add `stock_ticker` with the value to look up on Yahoo Finance to find the market cap. If you add a `repo_url` the card will be white instead of grey. Additonally, when using `repo_url`, `project_org` can be set pointing to an organization on GitHub, this will have the effect of pulling the information for all the repos belonging to that organization but using `repo_url` for information regarding license and best practices.
|
||||
|
||||
## Crunchbase Requirement
|
||||
### Crunchbase Requirement
|
||||
|
||||
We require all landscape entries to include a [Crunchbase](https://www.crunchbase.com/) url. We use the Crunchbase API to fetch the backing organization and headquarters location and (if they exist), Twitter, LinkedIn, funding, parent organization, and stock ticker. For open source, non-affiliated projects, we will just create a nonprofit organization representing the project (if one doesn't already exist), and set the location to the lead developer.
|
||||
|
||||
Using an external source for this info saves effort in most cases, because most organizations are already listed. Going forward, the data is being independently maintained and updated over time.
|
||||
|
||||
If for certain reason Crunchbase should not be used - we rely on `organization: { name: 'My Organization Name' }` instead of a `crunchbase` field
|
||||
|
||||
#### Overriding industries from Crunchbase
|
||||
|
||||
To override industries returned from Crunchbase for a specific Crunchbase entry, add it to an `crunchbase_overrides` top-level entry on `landscape.yml`. For instance, the following will set `industries` for Linux Foundation to Linux and Cloud Computing:
|
||||
|
||||
```yaml
|
||||
crunchbase_overrides:
|
||||
https://www.crunchbase.com/organization/linux-foundation:
|
||||
industries:
|
||||
- Linux
|
||||
- Cloud Computing
|
||||
```
|
||||
|
||||
`crunchbase_overrides` must be a top-level key on `landscape.yml`, so it should be a sibling of `landscape`. That's to prevent having to override multiple items that share the same Crunchbase URL.
|
||||
|
||||
## External Data
|
||||
|
||||
The canonical source for all data is `landscape.yml`. Once a day, the landscapeapp update_server pulls data for projects and companies from the following sources:
|
||||
|
@ -69,20 +159,18 @@ The update server enhances the source data with the fetched data and saves the r
|
|||
|
||||
If you want to create an interactive landscape for your project or organization:
|
||||
1. Note ahead of time that the hardest part of building a landscape is getting hi-res images for every project. You *cannot* convert from a PNG or JPEG into an SVG. You need to get an SVG, AI, or EPS file. When those aren't available, you will need a graphic designer to recreate several images. Don't just use an auto-tracer to try to convert PNG to SVG because there is some artistry involved in making it look good. Please review this [primer](https://www.cncf.io/blog/2019/07/17/what-image-formats-should-you-be-using-in-2019/) on image formats.
|
||||
1. Create a repo `youracronym-landscape` so it's distinct from other landscapes stored in the same directory. From inside your new directory, copy over files from a simpler landscape like https://github.com/graphql/graphql-landscape with `cp -r ../graphql-landscape/* ../graphql-landscape/.github ../graphql-landscape/.gitignore ../graphql-landscape/.npmrc ../graphql-landscape/.nvmrc .`.
|
||||
2. If you're working with the [LF](https://www.linuxfoundation.org/), give admin privileges to the new repo to [dankohn](https://github.com/dankohn) and write privleges to [AndreyKozlov1984](https://github.com/AndreyKozlov1984), [jordinl83](https://github.com/jordinl83), and [CNCF-Bot](https://github.com/CNCF-Bot) and ping Dan after creating an account at [slack.cncf.io](https://slack.cncf.io). Alex Contini and Dan are available there to help you recreate SVGs based on a PNG of the company's logo, if necessary, and to fix other problems.
|
||||
2. Set the repo to only support merge commits and turn off DCO support, since it doesn't work well with the GitHub web interface:
|
||||
2. Create a repo `youracronym-landscape` so it's distinct from other landscapes stored in the same directory. From inside your new directory, copy over files from a simpler landscape like https://github.com/graphql/graphql-landscape with `cp -r ../graphql-landscape/* ../graphql-landscape/.github ../graphql-landscape/.gitignore ../graphql-landscape/.npmrc ../graphql-landscape/.nvmrc .`.
|
||||
3. If you're working with the [LF](https://www.linuxfoundation.org/), give admin privileges to the new repo to [dankohn](https://github.com/dankohn) and write privileges to [AndreyKozlov1984](https://github.com/AndreyKozlov1984), [jordinl83](https://github.com/jordinl83), and [CNCF-Bot](https://github.com/CNCF-Bot) and ping Dan after creating an account at [slack.cncf.io](https://slack.cncf.io). Alex Contini and Dan are available there to help you recreate SVGs based on a PNG of the company's logo, if necessary, and to fix other problems.
|
||||
4. Set the repo to only support merge commits and turn off DCO support, since it doesn't work well with the GitHub web interface:
|
||||

|
||||
3. For LF projects, Dan will set you up in Netlify to build on every commit. Build command is `npm install -g npm && npm ci && npm run build` and publish directory is `dist`. Environment variables that need to be set are `CRUNCHBASE_KEY`, `GITHUB_KEY`, `GITHUB_TOKEN`, and `TWITTER_KEYS`. Dan recommends these notifications:
|
||||

|
||||
5. Edit `settings.yml` and `landscape.yml` for your topic.
|
||||
6. [Generate](https://www.qrcode-monkey.com) a QR code, setting colors to black. Save as SVG and overwrite images/qr.svg.
|
||||
7. Run `y reset-tweet-count` to start the count of tweets mentioning your landscape at zero.
|
||||
8. Edit [landscapes.yml](landscapes.yml) to add your project.
|
||||
|
||||
## API Keys
|
||||
### API Keys
|
||||
|
||||
You want to add the following to your `~/.bash_profile`. If you're with the LF, ask Dan Kohn on CNCF [Slack](https://slack.cncf.io) for the Crunchbase and Twitter keys.
|
||||
You want to add the following to your `~/.bash_profile`. If you're with the LF, ask someone on CNCF [Slack](https://slack.cncf.io) for the Crunchbase and Twitter keys.
|
||||
|
||||
For the GitHub key, please go to https://github.com/settings/tokens and create a key (you can call it `personal landscape`) with *no* permissions. That is, don't click any checkboxes, because you only need to access public repos.
|
||||
|
||||
|
@ -92,7 +180,7 @@ export TWITTER_KEYS=keys-here
|
|||
export GITHUB_KEY=key-here
|
||||
```
|
||||
|
||||
## Installing Locally
|
||||
### Installing Locally
|
||||
|
||||
You can administer a landscape without ever needing to install the software locally. However, a local install is helpful for rapid development, as it reduces the 5 minute build time on Netlify to 10 seconds or less locally. In particular, you want a local install when you're reconfiguring the layout. We recommend installing one or more landscapes as sibling directories to the landscapeapp. Then, you want to install the npm modules for landscapeapp but not for any of the landscapes. Here are the [install](INSTALL.md) directions.
|
||||
|
||||
|
@ -104,33 +192,17 @@ dev$ cd landscapeapp
|
|||
dev$ npm install -g yarn@latest
|
||||
dev$ yarn
|
||||
```
|
||||
Now, to use the local landscapeapp you can add the following to your `~/.bash_profile`:
|
||||
```sh
|
||||
function y { export PROJECT_PATH=`pwd` && (cd ../landscapeapp && yarn run "$@")}
|
||||
export -f y
|
||||
# yf does a normal build and full test run
|
||||
alias yf='y fetch'
|
||||
alias yl='y check-links'
|
||||
alias yq='y remove-quotes'
|
||||
# yp does a build and then opens up the landscape in your browser ( can view the PDF and PNG files )
|
||||
alias yp='y build && y open:dist'
|
||||
# yo does a quick build and opens up the landscape in your browser
|
||||
alias yo='y open:src'
|
||||
alias a='for path in /Users/your-username/dev/{landscapeapp,cdf-landscape,lfai-landscape}; do echo $path; git -C $path pull -p; done; (cd /Users/your-username/dev/landscapeapp && yarn);'
|
||||
|
||||
```
|
||||
Reload with `. ~/.bash_profile` and then use `yo`, `yf`, etc. to run functions on the landscape in your landscape directory. `a` will do a git pull on each of the project directories you specify and install any necessary node modules for landscapeapp.
|
||||
|
||||
## Adding to a google search console
|
||||
### Adding to a google search console
|
||||
Go to the google search console, add a new property, enter the url of the
|
||||
given project, for example, https://landscape.cncf.io
|
||||
|
||||
Next, google will want to verify that it is your site, thus you need to choose
|
||||
an `html tag verification` option and copy a secret code from it and put it to
|
||||
the `settings.yml` of a given landscape project. Then commit the change to the master branch and
|
||||
wait till Netlify deploys the master branch. The key is named `google_site_veryfication` and it is
|
||||
somewhere around line 14 in settings.yml. After netlify succesfully deploys
|
||||
that dashbaord, verify the html tag in a google console. Do not forget to add
|
||||
the `settings.yml` of a given landscape project. Then commit the change to the default branch and
|
||||
wait till Netlify deploys the default branch. The key is named `google_site_veryfication` and it is
|
||||
somewhere around line 14 in settings.yml. After netlify successfully deploys
|
||||
that dashboard, verify the html tag in a google console. Do not forget to add
|
||||
Dan@linuxfoundation.org as someone who has a full access from a `Settings`
|
||||
menu for a given search console.
|
||||
|
||||
|
@ -157,7 +229,7 @@ Note, that script `netlify/landscape.js` from THIS repo is used to run an
|
|||
individual build on every landscape.
|
||||
|
||||
A file netlify.toml specifies which commands are used and how to make a build.
|
||||
We start from the `netlify` folder and then download the landscape.js script from the master branch
|
||||
We start from the `netlify` folder and then download the landscape.js script from the default branch
|
||||
of a landscapeapp repo and then run a `node netlify/landscape.js`
|
||||
script because otherwise, Netlify will run an unnecessary `npm install`
|
||||
In order to make a build as fast as possible, we designed a way to run it on our
|
||||
|
@ -217,7 +289,7 @@ Without BUILD_SERVER variable, the following steps are done, from a file netlify
|
|||
Then we use rsync to send the current checkout of a repo to our remote server
|
||||
Then for every individual landscape, we run a `build.sh` file on a remote
|
||||
server, in each own docker container for every landscape. That is done in parallel. The file `build.sh` checks out the
|
||||
master branch of a given landscape and then runs `npm run build` with a
|
||||
default branch of a given landscape and then runs `npm run build` with a
|
||||
PROJECT_PATH pointed to the given landscape
|
||||
|
||||
When all builds had been finished, the output is returned to the `dist/${landscape.name}`
|
||||
|
@ -260,7 +332,7 @@ is usually done:
|
|||
a red color, i.e. have a major update. They may require to implement certain
|
||||
changes in our code.
|
||||
|
||||
# Embed landscape in a web site
|
||||
## Embed landscape in a web site
|
||||
|
||||
You can embed the landscape in a website in a few different ways...
|
||||
|
||||
|
@ -278,3 +350,31 @@ You can embed the landscape in a website in a few different ways...
|
|||
<iframe src="https://landscape.openmainframeproject.org/category=open-mainframe-project-member-company&format=logo-mode&grouping=category&embed=yes" frameborder="0" id="landscape" scrolling="no" style="width: 1px; min-width: 100%; opacity: 1; visibility: visible; overflow: hidden; height: 1717px;"></iframe>
|
||||
<script src="https://landscape.openmainframeproject.org/iframeResizer.js"></script>
|
||||
```
|
||||
|
||||
## Generating a Guide
|
||||
|
||||
A Guide can be generated by adding a file `guide.md`. `guide.md` will be mostly regular markdown with some custom behavior:
|
||||
|
||||
### No headings level 1 allowed
|
||||
|
||||
No [Headings](https://www.markdownguide.org/basic-syntax/#headings) level 1 allowed, use level 2 or higher.
|
||||
|
||||
### Linking a category from the landscape to a section on the guide
|
||||
|
||||
If a section on the guide refers to a category on the landscape, an info icon will be added on the category on the landscape and such icon will redirect to the entry on the guide for that category.
|
||||
|
||||
In order to associate the category and the section on the guide, the section on the guide should be wrapped between `<section data-category="$categoryId">` and `</section>`, where `$categoryId` is the id of the category.
|
||||
|
||||
Don't include a title for the section, a level 2 heading will be automatically generated using the name of the category.
|
||||
|
||||
### Linking a subcategory from the landscape to a section on the guide
|
||||
|
||||
If a section on the guide refers to a subcategory on the landscape, an info icon will be added on the subcategory on the landscape and such icon will redirect to the entry on the guide for that subcategory.
|
||||
|
||||
In order to associate the subcategory and the section on the guide, the section on the guide should be wrapped between `<section data-subcategory="$subcategoryId" data-buzzwords="$buzzword1,$buzzword2">` and `</section>`, where `$subcategoryId` is the id of the subcategory. Buzzwords is a comma-separated list of words that describe the subcategory, a table will be automatically generated at the bottom of the section including those buzzwords and the list of projects hosted by the organization. The cards with all the logos for that subcategory will also be included at the bottom of the section.
|
||||
|
||||
Don't include a title for the section, level 3 heading will be automatically generated using the name of the subcategory.
|
||||
|
||||
### Automatic generation of guide navigation
|
||||
|
||||
The guide will include a side-navigation generated automatically from all the headings levels 2 and 3 found on the guide. Level 3 headings will be nested under the closest level 2 heading above.
|
||||
|
|
8
_headers
8
_headers
|
@ -1,12 +1,6 @@
|
|||
/*
|
||||
X-Robots-Tag: all
|
||||
Access-Control-Allow-Origin: https://landscapes.dev
|
||||
|
||||
/*.js
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/*.css
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
Access-Control-Allow-Origin: *
|
||||
|
||||
/*.svg
|
||||
Content-Type: image/svg+xml; charset=utf-8
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
/ /prerender.html 200!
|
||||
/* /index.html 200
|
File diff suppressed because it is too large
Load Diff
17
build.sh
17
build.sh
|
@ -3,21 +3,6 @@ rm -rf $2 || true
|
|||
timeout 120s git clone --quiet https://github.com/$1 $2
|
||||
cd $2
|
||||
git remote -v
|
||||
# git remote rm origin 2>/dev/null 1>/dev/null || true
|
||||
# git remote add origin https://github.com/$1
|
||||
git checkout origin/$3
|
||||
cd ..
|
||||
export PROJECT_PATH=$2
|
||||
export PROJECT_PATH=$PWD/$2
|
||||
PROJECT_NAME=$2 yarn build
|
||||
# rm -rf ./$2
|
||||
# echo "/$2/* /$2/index.html 200" >> dist/_redirects
|
||||
# echo "<div><a href="$2/"><h1>$2</h1></a></div>" >> dist/index.html
|
||||
# echo "User-agent: *" > dist/$2/robots.txt
|
||||
# echo "Disallow: /" >> dist/$2/robots.txt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Code of Conduct
|
||||
|
||||
We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||
We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/HEAD/code-of-conduct.md).
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
[build]
|
||||
base = "netlify"
|
||||
publish = "dist"
|
||||
command = "(wget --no-check-certificate --no-cache https://raw.githubusercontent.com/cncf/landscapeapp/master/netlify/landscape.js) && node landscape.js"
|
||||
ignore = "false"
|
||||
environment = { RUBY_VERSION = "2.6.2", NODE_VERSION="14.3" }
|
||||
|
||||
[functions]
|
||||
directory = "functions"
|
||||
|
||||
[[redirects]]
|
||||
from = "/api/*"
|
||||
to = "/.netlify/functions/:splat"
|
||||
status = 200
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
"verbose": true
|
||||
}
|
|
@ -4,8 +4,9 @@
|
|||
git config --global user.email "info@cncf.io"
|
||||
git config --global user.name "CNCF-Bot"
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
|
||||
|
||||
apt-get update
|
||||
apt-get -y install build-essential gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||
apt-get -y install build-essential gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget gawk aha nginx libgbm-dev
|
||||
|
||||
# Then we run our wrapper to process all landscapes
|
||||
. ~/.nvm/nvm.sh
|
||||
|
@ -15,4 +16,4 @@ nvm use
|
|||
npm install -g npm
|
||||
npm install -g yarn
|
||||
yarn
|
||||
yarn run babel-node tools/landscapes.js
|
||||
yarn node tools/landscapes.js
|
||||
|
|
|
@ -1,24 +1,39 @@
|
|||
ip: 147.75.106.211
|
||||
ip: 147.75.72.237
|
||||
landscapes:
|
||||
# name: how we name a landscape project, used on a build server for logs and settings
|
||||
# repo: a github repo for a specific landscape
|
||||
# netlify: full | skip - do we build it on a netlify build or not
|
||||
# hook: - id for a build hook, so it will be triggered after a master build
|
||||
# hook: - id for a build hook, so it will be triggered after a default branch build
|
||||
- landscape:
|
||||
name: aswf
|
||||
name: lfedge
|
||||
repo: State-of-the-Edge/lfedge-landscape
|
||||
hook: 5c80e31894c5c7758edb31e4
|
||||
- landscape:
|
||||
name: lfenergy
|
||||
repo: lf-energy/lfenergy-landscape
|
||||
hook: 606487bb9da603110d4b8139
|
||||
- landscape:
|
||||
name: lf
|
||||
repo: jmertic/lf-landscape
|
||||
hook: 606487123224e20fb8f3896e
|
||||
- landscape:
|
||||
name: dlt
|
||||
repo: dltlandscape/dlt-landscape
|
||||
hook: demo
|
||||
- landscape:
|
||||
name: aswf-landscape
|
||||
repo: AcademySoftwareFoundation/aswf-landscape
|
||||
hook: 5d5c7ca6dc2c51cf02381f63
|
||||
required: true
|
||||
- landscape:
|
||||
name: cncf
|
||||
repo: cncf/landscape
|
||||
hook: 5c1bd968fdd72a78a54bdcd1
|
||||
hook: 608aa68eb6e5723d5d8a7e00
|
||||
required: true
|
||||
- landscape:
|
||||
name: cdf
|
||||
repo: cdfoundation/cdf-landscape
|
||||
hook: 5d8e8ecbeb12d77369f06dba
|
||||
required: true
|
||||
- landscape:
|
||||
name: finos-landscape
|
||||
repo: finos/FINOS-landscape
|
||||
hook: 5eda7140b186b66e915a15b8
|
||||
- landscape:
|
||||
name: hl-landscape
|
||||
repo: hyperledger-landscape/hl-landscape
|
||||
|
@ -28,42 +43,41 @@ landscapes:
|
|||
repo: graphql/graphql-landscape
|
||||
hook: 5d5c7ccf64ecb5bd3d2592f7
|
||||
required: true
|
||||
- landscape:
|
||||
name: lf
|
||||
repo: jmertic/lf-landscape
|
||||
hook: 5ed4f433987c7ad56b356a74
|
||||
- landscape:
|
||||
name: lfai
|
||||
repo: lfai/landscape
|
||||
hook: 5ee219e3a8ae10e0246a9533
|
||||
hook: 60648e5b74c76017210a2f53
|
||||
required: true
|
||||
- landscape:
|
||||
name: lfedge
|
||||
repo: State-of-the-Edge/lfedge-landscape
|
||||
hook: 5c80e31894c5c7758edb31e4
|
||||
- landscape:
|
||||
name: lfenergy
|
||||
repo: lf-energy/lfenergy-landscape
|
||||
hook: 5d98c95de9ff92d31e713003
|
||||
- landscape:
|
||||
name: lfph
|
||||
repo: lfph/lfph-landscape
|
||||
hook: 5ec5145565b543b0b01aa9d7
|
||||
required: true
|
||||
- landscape:
|
||||
name: openjsf
|
||||
repo: openjs-foundation/openjsf-landscape
|
||||
hook: 5de319ba9e61a5ddc3207294
|
||||
- landscape:
|
||||
name: omp
|
||||
repo: openmainframeproject/omp-landscape
|
||||
hook: 5d6a6f73080982abfbd46290
|
||||
required: true
|
||||
hook: 6064862525138e14cfe87a39
|
||||
- landscape:
|
||||
name: openssf
|
||||
repo: ossf/ossf-landscape
|
||||
hook: 613768338a16cb9182b21c3d
|
||||
- landscape:
|
||||
name: presto
|
||||
repo: prestodb/presto-landscape
|
||||
hook: 5ecd46d9f5ea8819a9d610a6
|
||||
- landscape:
|
||||
name: tarscloud
|
||||
repo: TarsCloud/TARS_landscape
|
||||
hook: 5f0e8a006cae8a2b52061c81
|
||||
- landscape:
|
||||
name: ucf
|
||||
repo: ucfoundation/ucf-landscape
|
||||
hook: 5d96662e28b476477790dd8a
|
||||
- landscape:
|
||||
name: lfn
|
||||
repo: lfnetworking/member_landscape
|
||||
hook: 60788f5fa3cbd68181e3c209
|
||||
- landscape:
|
||||
name: riscv
|
||||
repo: riscv-admin/riscv-landscape
|
||||
hook: demo
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
</script>
|
||||
|
||||
<script>
|
||||
let reportsIp = null;
|
||||
const fetchFile = async path => await (await fetch(`https://raw.githubusercontent.com/${path}`)).text()
|
||||
|
||||
const tableEl = document.querySelector("tbody")
|
||||
|
@ -114,7 +115,7 @@
|
|||
const linkTo = (url, title) => `<a target="_blank" href="${url}">${title}</a>`
|
||||
const imageLinkTo = (url, src, alt) => linkTo(url, `<img src="${src}" alt="${alt}" />`)
|
||||
|
||||
const fetchLandscapeInfo = ({ repo }) => {
|
||||
const fetchLandscapeInfo = ({ repo, name }) => {
|
||||
const rowEl = document.createElement("tr")
|
||||
const dateTimeFormat = new Intl.DateTimeFormat('en', { month: 'short', day: '2-digit', hour: '2-digit', minute:'2-digit', hour12: false })
|
||||
tableEl.append(rowEl)
|
||||
|
@ -144,7 +145,7 @@
|
|||
|
||||
repoEl.innerHTML = linkTo(`https://github.com/${repo}`, repo)
|
||||
|
||||
fetchFile(`${repo}/master/settings.yml`)
|
||||
fetchFile(`${repo}/HEAD/settings.yml`)
|
||||
.then(async settings => {
|
||||
const { company_url, short_name, short_domain, website } = jsyaml.load(settings).global
|
||||
organizationEl.innerHTML = imageLinkTo(company_url, `${website}/images/right-logo.svg`, short_name)
|
||||
|
@ -155,18 +156,18 @@
|
|||
</div>
|
||||
${imageLinkTo(website, `${website}/images/landscape_preview.png`, short_domain)}`
|
||||
const landscapeIndex = await (await fetch(website)).text()
|
||||
const publishedAt = landscapeIndex.match(/lastUpdated\s*=\s*"([^"]*)"/)[1]
|
||||
const publishedAt = landscapeIndex.match(/Updated:\s*([^"]*)/)[1]
|
||||
publishedEl.innerHTML = publishedAt ? dateTimeFormat.format(new Date(publishedAt)) : 'UNKNOWN'
|
||||
})
|
||||
|
||||
fetchFile(`${repo}/master/README.md`)
|
||||
fetchFile(`${repo}/HEAD/README.md`)
|
||||
.then(async readme => {
|
||||
const statusUrl = readme.match(/https:\/\/api.netlify.com\/api\/v1\/badges[^)]*/)[0]
|
||||
const deploysUrl = readme.match(/https:\/\/app.netlify.com\/sites[^)]*/)[0]
|
||||
statusEl.innerHTML = imageLinkTo(deploysUrl, statusUrl, "Netlify Status")
|
||||
})
|
||||
|
||||
fetchFile(`${repo}/master/processed_landscape.yml`)
|
||||
fetchFile(`${repo}/HEAD/processed_landscape.yml`)
|
||||
.then(processedLandscape => {
|
||||
const { updated_at, twitter_options } = jsyaml.load(processedLandscape)
|
||||
const updatedAt = updated_at ? new Date(updated_at) : null
|
||||
|
@ -176,13 +177,18 @@
|
|||
numTweetsEl.innerHTML = (twitter_options ? twitter_options.count : 0).toString()
|
||||
|
||||
updatedEl.innerHTML = `<div class="${className}">
|
||||
${updated_at ? dateTimeFormat.format(updatedAt) : 'UNKNOWN'}
|
||||
<div> ${updated_at ? dateTimeFormat.format(updatedAt) : 'UNKNOWN' } </div>
|
||||
<a href="http://${reportsIp}/${name}.html">VIEW DAILY AUTOUPDATE LOGS</a>
|
||||
</div>`
|
||||
})
|
||||
}
|
||||
|
||||
fetchFile("cncf/landscapeapp/master/landscapes.yml")
|
||||
.then(data => jsyaml.load(data).landscapes.forEach(landscape => fetchLandscapeInfo(landscape)))
|
||||
fetchFile("cncf/landscapeapp/HEAD/landscapes.yml")
|
||||
.then(function(data) {
|
||||
const content = jsyaml.load(data);
|
||||
reportsIp = content.ip;
|
||||
content.landscapes.forEach(landscape => fetchLandscapeInfo(landscape));
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -9,3 +9,5 @@ server via rsync.
|
|||
|
||||
Most chances is that we will switch to a different build tool soon, so this is
|
||||
an experimental approach to speedup netlify builds.
|
||||
|
||||
|
||||
|
|
|
@ -2,3 +2,5 @@
|
|||
ignore = "false"
|
||||
environment = { RUBY_VERSION = "2.6.2", NODE_VERSION = "14.3" }
|
||||
|
||||
[functions]
|
||||
directory = "netlify/functions"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,8 @@
|
|||
// We will execute this script from a landscape build,
|
||||
// "prepublish": "cp yarn.lock _yarn.lock",
|
||||
// "postpublish": "rm _yarn.lock || true"
|
||||
const LANDSCAPEAPP = process.env.LANDSCAPEAPP || "latest"
|
||||
const remote = `root@${process.env.BUILD_SERVER}`;
|
||||
const dockerImage = 'netlify/build:xenial';
|
||||
const dockerImage = 'netlify/build:focal';
|
||||
const dockerHome = '/opt/buildhome';
|
||||
|
||||
const secrets = [
|
||||
|
@ -31,27 +30,20 @@ const debug = function() {
|
|||
}
|
||||
}
|
||||
|
||||
const runLocal = function(command, options = {}) {
|
||||
const { assignFn, showOutputFn } = options;
|
||||
const runLocal = function(command, showProgress) {
|
||||
|
||||
// report the output once every 5 seconds
|
||||
let lastOutput = { s: '', time: new Date().getTime() };
|
||||
let displayIfRequired = function(text) {
|
||||
lastOutput.s = lastOutput.s + text;
|
||||
if (showOutputFn && showOutputFn()) {
|
||||
if (lastOutput.done || new Date().getTime() > lastOutput.time + 5 * 1000) {
|
||||
console.info(lastOutput.s);
|
||||
lastOutput.s = "";
|
||||
lastOutput.time = new Date().getTime();
|
||||
};
|
||||
if (showProgress) {
|
||||
console.info(text);
|
||||
}
|
||||
lastOutput.s = lastOutput.s + text;
|
||||
}
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
var spawn = require('child_process').spawn;
|
||||
var child = spawn('bash', ['-lc',`set -e \n${command}`]);
|
||||
if (assignFn) {
|
||||
assignFn(child);
|
||||
}
|
||||
let output = [];
|
||||
child.stdout.on('data', function(data) {
|
||||
const text = maskSecrets(data.toString('utf-8'));
|
||||
|
@ -84,63 +76,6 @@ const runLocalWithoutErrors = async function(command) {
|
|||
return result.text.trim();
|
||||
}
|
||||
|
||||
let buildDone = false;
|
||||
let localPid;
|
||||
let remoteFailed = false;
|
||||
let localFailed = false;
|
||||
|
||||
async function getPids() {
|
||||
const result = await runLocalWithoutErrors(`ps`);
|
||||
const lines = result.split('\n').map( (x) => x.trim()).filter( (x) => x).slice(1);
|
||||
const pids = lines.map( (line) => line.split(' ')[0]);
|
||||
console.info('pids:', pids);
|
||||
return pids;
|
||||
}
|
||||
|
||||
let initialPids;
|
||||
|
||||
const makeLocalBuild = async function() {
|
||||
const localOutput = await runLocal(`
|
||||
# mkdir -p copy
|
||||
# rsync -az --exclude="copy" . copy
|
||||
# cd copy
|
||||
. ~/.nvm/nvm.sh
|
||||
npm pack interactive-landscape@${LANDSCAPEAPP}
|
||||
tar xzf interactive*
|
||||
cd package
|
||||
cp _yarn.lock yarn.lock
|
||||
nvm install \`cat .nvmrc\`
|
||||
nvm use \`cat .nvmrc\`
|
||||
npm install -g npm --no-progress
|
||||
npm install -g yarn@latest
|
||||
~/.nvm/versions/node/\`cat .nvmrc\`/bin/yarn >/dev/null
|
||||
export NODE_OPTIONS="--unhandled-rejections=strict"
|
||||
export JEST_OPTIONS="-i"
|
||||
export USE_OLD_PUPPETEER=1
|
||||
PROJECT_PATH=.. ~/.nvm/versions/node/\`cat .nvmrc\`/bin/yarn build
|
||||
`, { assignFn: (x) => localPid = x, showOutputFn: () => remoteFailed });
|
||||
|
||||
if (!buildDone) {
|
||||
console.info('Local build finished, exit code:', localOutput.exitCode);
|
||||
if (localOutput.exitCode !== 0) {
|
||||
console.info(localOutput.text);
|
||||
localFailed = true;
|
||||
if (!remoteFailed) {
|
||||
return;
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
buildDone = true;
|
||||
await runLocalWithoutErrors(`
|
||||
rm -rf netlify/dist || true
|
||||
cp -r dist netlify
|
||||
`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.info('Ignore local build');
|
||||
}
|
||||
}
|
||||
const key = `
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
${(process.env.BUILDBOT_KEY || '').replace(/\s/g,'\n')}
|
||||
|
@ -149,7 +84,7 @@ ${(process.env.BUILDBOT_KEY || '').replace(/\s/g,'\n')}
|
|||
require('fs').writeFileSync('/tmp/buildbot', key);
|
||||
require('fs').chmodSync('/tmp/buildbot', 0o600);
|
||||
|
||||
const runRemote = async function(command, options) {
|
||||
const runRemote = async function(command, count = 3) {
|
||||
const bashCommand = `
|
||||
nocheck=" -o StrictHostKeyChecking=no "
|
||||
ssh -i /tmp/buildbot $nocheck ${remote} << 'EOSSH'
|
||||
|
@ -157,8 +92,14 @@ const runRemote = async function(command, options) {
|
|||
${command}
|
||||
EOSSH
|
||||
`
|
||||
return await runLocal(bashCommand, options);
|
||||
const result = await runLocal(bashCommand, true);
|
||||
if (result.exitCode === 255 && count > 0) {
|
||||
console.info(`Attempts to retry more: ${count}`);
|
||||
return await runRemote(command, count - 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const runRemoteWithoutErrors = async function(command) {
|
||||
const result = await runRemote(command);
|
||||
console.info(result.text.trim());
|
||||
|
@ -169,15 +110,8 @@ const runRemoteWithoutErrors = async function(command) {
|
|||
|
||||
const makeRemoteBuildWithCache = async function() {
|
||||
await runLocalWithoutErrors(`
|
||||
echo extracting
|
||||
mkdir tmpRemote
|
||||
cd tmpRemote
|
||||
rm -rf package || true
|
||||
npm pack interactive-landscape@${LANDSCAPEAPP}
|
||||
tar xzf interactive*.tgz
|
||||
cd ..
|
||||
mv tmpRemote/package packageRemote
|
||||
cp packageRemote/_yarn.lock packageRemote/yarn.lock
|
||||
rm -rf packageRemote || true
|
||||
git clone -b deploy --single-branch https://github.com/cncf/landscapeapp packageRemote
|
||||
`);
|
||||
|
||||
//how to get a hash based on our files
|
||||
|
@ -210,53 +144,6 @@ const makeRemoteBuildWithCache = async function() {
|
|||
const hash = getHash();
|
||||
const tmpHash = require('crypto').createHash('sha256').update(getTmpFile()).digest('hex');
|
||||
// lets guarantee npm install for this folder first
|
||||
{
|
||||
const buildCommand = [
|
||||
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
|
||||
". ~/.nvm/nvm.sh",
|
||||
`nvm install ${nvmrc}`,
|
||||
`nvm use ${nvmrc}`,
|
||||
`npm install -g npm --no-progress`,
|
||||
`npm install -g yarn@latest`,
|
||||
`cd /opt/repo/packageRemote`,
|
||||
`yarn >/dev/null`
|
||||
].join(' && ');
|
||||
const npmInstallCommand = `
|
||||
mkdir -p /root/builds/node_cache
|
||||
ls -l /root/builds/node_cache/${hash}/yarnLocal/unplugged 2>/dev/null || (
|
||||
mkdir -p /root/builds/node_cache/${tmpHash}/{yarnLocal,nvm,yarnGlobal}
|
||||
cp -r /root/builds/${folder}/packageRemote/.yarn/* /root/builds/node_cache/${tmpHash}/yarnLocal
|
||||
chmod -R 777 /root/builds/node_cache/${tmpHash}
|
||||
docker run --shm-size 1G --rm -t \
|
||||
-v /root/builds/node_cache/${tmpHash}/yarnLocal:/opt/repo/packageRemote/.yarn \
|
||||
-v /root/builds/node_cache/${tmpHash}/nvm:${dockerHome}/.nvm \
|
||||
-v /root/builds/node_cache/${tmpHash}/yarnGlobal:${dockerHome}/.yarn \
|
||||
-v /root/builds/${folder}:/opt/repo \
|
||||
${dockerImage} /bin/bash -lc "${buildCommand}"
|
||||
|
||||
ln -s /root/builds/node_cache/${tmpHash} /root/builds/node_cache/${hash} || (
|
||||
rm -rf /root/builds/node_cache/${tmpHash}
|
||||
)
|
||||
echo "packages for ${hash} had been installed"
|
||||
)
|
||||
chmod -R 777 /root/builds/node_cache/${hash}
|
||||
`;
|
||||
debug(npmInstallCommand);
|
||||
console.info(`Remote with cache: Installing npm packages if required`);
|
||||
const output = await runRemote(npmInstallCommand);
|
||||
console.info(`Remote with cache: Output from npm install: exit code: ${output.exitCode}`);
|
||||
if (output.exitCode !== 0) {
|
||||
console.info(output.text);
|
||||
throw new Error('Remote with cahce: npm install failed');
|
||||
}
|
||||
|
||||
const lines = output.text.split('\n');
|
||||
const index = lines.indexOf(lines.filter( (line) => line.match(/added \d+ packages in/))[0]);
|
||||
const filteredLines = lines.slice(index !== -1 ? index : 0).join('\n');
|
||||
console.info(filteredLines || 'Reusing an existing folder for node');
|
||||
|
||||
}
|
||||
|
||||
// do not pass REVIEW_ID because on failure we will run it locally and report
|
||||
// from there
|
||||
const vars = [
|
||||
|
@ -272,10 +159,16 @@ const makeRemoteBuildWithCache = async function() {
|
|||
const outputFolder = 'landscape' + getTmpFile();
|
||||
const buildCommand = [
|
||||
`cd /opt/repo/packageRemote`,
|
||||
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
|
||||
`. ~/.nvm/nvm.sh`,
|
||||
`cat .nvmrc`,
|
||||
`nvm install ${nvmrc}`,
|
||||
`nvm use ${nvmrc}`,
|
||||
`npm install -g agentkeepalive --save`,
|
||||
`npm install -g npm@9 --no-progress`,
|
||||
`npm install -g yarn@latest`,
|
||||
`yarn`,
|
||||
`git config --global --add safe.directory /opt/repo`,
|
||||
`export NODE_OPTIONS="--unhandled-rejections=strict"`,
|
||||
`PROJECT_PATH=.. yarn run build`,
|
||||
`cp -r /opt/repo/dist /dist`
|
||||
|
@ -285,16 +178,12 @@ const makeRemoteBuildWithCache = async function() {
|
|||
mkdir -p /root/builds/${outputFolder}
|
||||
chmod -R 777 /root/builds/${outputFolder}
|
||||
chmod -R 777 /root/builds/${folder}
|
||||
chmod -R 777 /root/builds/node_cache/${hash}
|
||||
|
||||
docker run --shm-size 1G --rm -t \
|
||||
${vars.map( (v) => ` -e ${v}="${process.env[v]}" `).join(' ')} \
|
||||
-e NVM_NO_PROGRESS=1 \
|
||||
-e NETLIFY=1 \
|
||||
-e PARALLEL=TRUE \
|
||||
-v /root/builds/node_cache/${hash}/yarnLocal:/opt/repo/packageRemote/.yarn \
|
||||
-v /root/builds/node_cache/${hash}/nvm:${dockerHome}/.nvm \
|
||||
-v /root/builds/node_cache/${hash}/yarnGlobal:${dockerHome}/.yarn \
|
||||
-v /root/builds/${folder}:/opt/repo \
|
||||
-v /root/builds/${outputFolder}:/dist \
|
||||
${dockerImage} /bin/bash -lc "${buildCommand}"
|
||||
|
@ -323,70 +212,46 @@ const makeRemoteBuildWithCache = async function() {
|
|||
console.info(await runLocalWithoutErrors(
|
||||
`
|
||||
mkdir -p distRemote
|
||||
rsync -az -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " ${remote}:/root/builds/${outputFolder}/dist/* distRemote
|
||||
rsync -az --chmod=a+r -p -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " ${remote}:/root/builds/${outputFolder}/dist/* distRemote
|
||||
`
|
||||
));
|
||||
await runRemoteWithoutErrors(
|
||||
await runRemote(
|
||||
`
|
||||
rm -rf /root/builds/${folder}
|
||||
rm -rf /root/builds/${outputFolder}
|
||||
`
|
||||
)
|
||||
if (!buildDone) {
|
||||
buildDone = true;
|
||||
const newPids = await getPids();
|
||||
const pidsToKill = newPids.filter( (x) => !initialPids.includes(x));
|
||||
console.info(await runLocal(`kill -9 ${pidsToKill.join(' ')}`));
|
||||
|
||||
localPid.kill();
|
||||
|
||||
const pause = function(i) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, 5 * 1000);
|
||||
})
|
||||
};
|
||||
await pause(); // allow the previous process to be killed
|
||||
await runLocalWithoutErrors(`ps`);
|
||||
|
||||
console.info('Remote build done!');
|
||||
console.info(output.text);
|
||||
await runLocalWithoutErrors(`
|
||||
console.info('Remote build done!');
|
||||
console.info(output.text);
|
||||
await runLocalWithoutErrors(`
|
||||
rm -rf netlify/dist || true
|
||||
rm -rf dist || true
|
||||
mkdir -p netlify/dist
|
||||
mkdir -p dist
|
||||
cp -r distRemote/* netlify/dist
|
||||
cp -r distRemote/* dist
|
||||
ls -la netlify/dist
|
||||
ls -la dist
|
||||
mv netlify/dist/functions netlify/functions
|
||||
cp -r netlify/functions functions # Fix netlify bug
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const path = require('path');
|
||||
console.info('starting', process.cwd());
|
||||
process.chdir('..');
|
||||
await runLocal('rm package*.json');
|
||||
|
||||
initialPids = await getPids();
|
||||
|
||||
const cleanPromise = runRemoteWithoutErrors(`
|
||||
find builds/node_cache -maxdepth 1 -mtime +1 -exec rm -rf {} +;
|
||||
find builds/ -maxdepth 1 -not -path "builds/node_cache" -mtime +1 -exec rm -rf {} +;
|
||||
`).catch(function(ex) {
|
||||
`).catch(function() {
|
||||
console.info('Failed to clean up a builds folder');
|
||||
});
|
||||
|
||||
await Promise.all([makeRemoteBuildWithCache().catch(function(ex) {
|
||||
console.info('Remote build failed! Continuing with a local build', ex);
|
||||
remoteFailed = true;
|
||||
if (localFailed) {
|
||||
process.exit(1);
|
||||
}
|
||||
}), makeLocalBuild(), cleanPromise]);
|
||||
|
||||
console.info('build failed', ex);
|
||||
process.exit(1);
|
||||
}), cleanPromise]);
|
||||
}
|
||||
|
||||
main().catch(function(ex) {
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
const path = require('path');
|
||||
const generateIndex = require('./generateIndex')
|
||||
const run = function(x) {
|
||||
console.info(require('child_process').execSync(x).toString())
|
||||
if (process.env.KEY3) {
|
||||
require('fs').mkdirSync(process.env.HOME + '/.ssh', { recursive: true});
|
||||
require('fs').writeFileSync(process.env.HOME + '/.ssh/bot3',
|
||||
"-----BEGIN RSA PRIVATE KEY-----\n" +
|
||||
process.env.KEY3.replace(/\s/g,"\n") +
|
||||
"\n-----END RSA PRIVATE KEY-----\n\n"
|
||||
);
|
||||
require('fs').chmodSync(process.env.HOME + '/.ssh/bot3', 0o600);
|
||||
console.info('Made a bot3 file');
|
||||
}
|
||||
|
||||
const path = require('path')
|
||||
const { readdirSync, writeFileSync } = require('fs')
|
||||
const generateIndex = require('./generateIndex')
|
||||
const debug = function() {
|
||||
if (process.env.DEBUG_BUILD) {
|
||||
console.info.apply(console, arguments);
|
||||
|
@ -15,18 +24,14 @@ const pause = function(i) {
|
|||
})
|
||||
};
|
||||
|
||||
console.info('starting', process.cwd());
|
||||
run('npm init -y');
|
||||
console.info('installing js-yaml', process.cwd());
|
||||
run('npm install js-yaml');
|
||||
const yaml = require('js-yaml');
|
||||
const yaml = require('./jsyaml');
|
||||
process.chdir('..');
|
||||
console.info('starting real script', process.cwd());
|
||||
const landscapesInfo = yaml.safeLoad(require('fs').readFileSync('landscapes.yml'));
|
||||
const landscapesInfo = yaml.load(require('fs').readFileSync('landscapes.yml'));
|
||||
|
||||
const dockerImage = 'netlify/build:xenial';
|
||||
const dockerImage = 'netlify/build:focal';
|
||||
const dockerHome = '/opt/buildhome';
|
||||
|
||||
|
||||
async function main() {
|
||||
const nvmrc = require('fs').readFileSync('.nvmrc', 'utf-8').trim();
|
||||
const secrets = [
|
||||
|
@ -60,7 +65,7 @@ ${process.env.BUILDBOT_KEY.replace(/\s/g,'\n')}
|
|||
|
||||
// now our goal is to run this on a remote server. Step 1 - xcopy the repo
|
||||
const folder = new Date().getTime();
|
||||
const remote = 'root@147.75.76.177';
|
||||
const remote = 'root@147.75.199.15';
|
||||
|
||||
const runRemote = async function(command) {
|
||||
const bashCommand = `
|
||||
|
@ -73,6 +78,24 @@ EOSSH
|
|||
const result = await runLocal(bashCommand);
|
||||
let newOutput = [];
|
||||
for (var l of result.text.split('\n')) {
|
||||
if (l.match(/Counting objects: /)) {
|
||||
continue;
|
||||
}
|
||||
if (l.match(/ExperimentalWarning: Custom ESM Loaders is an experimental feature./)) {
|
||||
continue
|
||||
}
|
||||
if (l.match(/Compressing objects: /)) {
|
||||
continue;
|
||||
}
|
||||
if (l.match(/Receiving objects: /)) {
|
||||
continue;
|
||||
}
|
||||
if (l.match(/Resolving deltas: /)) {
|
||||
continue;
|
||||
}
|
||||
if (l.match(/Could not resolve ".*?" in file/)) {
|
||||
continue;
|
||||
}
|
||||
newOutput.push(l);
|
||||
if (l.includes('mesg: ttyname failed: Inappropriate ioctl for device')) {
|
||||
newOutput = [];
|
||||
|
@ -142,7 +165,7 @@ EOSSH
|
|||
|
||||
await runLocalWithoutErrors(`
|
||||
rm -rf dist || true
|
||||
mkdir -p dist
|
||||
mkdir -p dist netlify/functions
|
||||
`);
|
||||
await runRemoteWithoutErrors(`mkdir -p /root/builds`);
|
||||
await runRemoteWithoutErrors(`docker pull ${dockerImage}`);
|
||||
|
@ -153,7 +176,6 @@ EOSSH
|
|||
await runRemoteWithoutErrors(`chmod -R 777 /root/builds/${folder}`);
|
||||
|
||||
// lets guarantee npm install for this folder first
|
||||
const branch = process.env.BRANCH;
|
||||
{
|
||||
const buildCommand = [
|
||||
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
|
||||
|
@ -162,7 +184,8 @@ EOSSH
|
|||
`nvm use ${nvmrc}`,
|
||||
`npm install -g yarn --no-progress --silent`,
|
||||
`cd /opt/repo`,
|
||||
`yarn >/dev/null`
|
||||
`yarn >/dev/null`,
|
||||
`yarn eslint`
|
||||
].join(' && ');
|
||||
const npmInstallCommand = `
|
||||
mkdir -p /root/builds/${folder}_node
|
||||
|
@ -193,16 +216,20 @@ EOSSH
|
|||
const outputFolder = landscape.name + new Date().getTime();
|
||||
const buildCommand = [
|
||||
`cd /opt/repo`,
|
||||
`git config --global --add safe.directory /opt/repo`,
|
||||
`. ~/.nvm/nvm.sh`,
|
||||
`nvm use`,
|
||||
`export NODE_OPTIONS="--unhandled-rejections=strict"`,
|
||||
`bash build.sh ${landscape.repo} ${landscape.name} master`,
|
||||
`bash build.sh ${landscape.repo} ${landscape.name}`,
|
||||
`cp -r /opt/repo/${landscape.name}/dist /dist`
|
||||
].join(' && ');
|
||||
const nodeModulesFolder = `${folder}_node`;
|
||||
const dockerCommand = `
|
||||
mkdir -p /root/builds/${outputFolder}
|
||||
chmod -R 777 /root/builds/${outputFolder}
|
||||
mkdir -p /tmp/${outputFolder}/public /tmp/${outputFolder}/out /tmp/${outputFolder}/.next
|
||||
chmod -R 777 /tmp/${outputFolder}
|
||||
|
||||
REPO_PATH=/root/builds/${folder}
|
||||
OUTPUT_PATH=/root/builds/${outputFolder}
|
||||
|
||||
|
@ -213,11 +240,12 @@ EOSSH
|
|||
-e PARALLEL=TRUE \
|
||||
-v /root/builds/${nodeModulesFolder}/nvm:${dockerHome}/.nvm \
|
||||
-v /root/builds/${nodeModulesFolder}/yarnGlobal:${dockerHome}/.yarn \
|
||||
-v /tmp/${outputFolder}/public:/opt/repo/public \
|
||||
-v /tmp/${outputFolder}/out:/opt/repo/out \
|
||||
-v /tmp/${outputFolder}/.next:/opt/repo/.next \
|
||||
-v /root/builds/${folder}:/opt/repo \
|
||||
-v /root/builds/${outputFolder}:/dist \
|
||||
${dockerImage} /bin/bash -lc "${buildCommand}"
|
||||
|
||||
|
||||
`;
|
||||
|
||||
console.info(`processing ${landscape.name} at ${landscape.repo}`);
|
||||
|
@ -229,8 +257,12 @@ EOSSH
|
|||
|
||||
output = await runRemote(dockerCommand);
|
||||
output.landscape = landscape;
|
||||
console.info(`Output from: ${output.landscape.name}, exit code: ${output.exitCode}`);
|
||||
console.info(output.text);
|
||||
if (output.exitCode) {
|
||||
console.info(`Output from: ${output.landscape.name}, exit code: ${output.exitCode}`);
|
||||
console.info(output.text);
|
||||
} else {
|
||||
console.info(`Done: ${output.landscape.name}`);
|
||||
}
|
||||
if (output.exitCode === 255) { // a single ssh failure
|
||||
output = await runRemote(dockerCommand);
|
||||
output.landscape = landscape;
|
||||
|
@ -243,9 +275,12 @@ EOSSH
|
|||
|
||||
await runLocal(
|
||||
`
|
||||
rsync -az -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " ${remote}:/root/builds/${outputFolder}/dist/ dist/${landscape.name}
|
||||
rsync -az --chmod=a+r -p -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " ${remote}:/root/builds/${outputFolder}/dist/${landscape.name}/ dist/${landscape.name}
|
||||
`
|
||||
);
|
||||
|
||||
await runLocal(`mv dist/${landscape.name}/functions/* netlify/functions`)
|
||||
|
||||
await runRemote(
|
||||
`
|
||||
rm -rf /root/builds/${outputFolder}
|
||||
|
@ -263,46 +298,44 @@ EOSSH
|
|||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const redirects = results.map((result) => `
|
||||
/${result.landscape.name}/ /${result.landscape.name}/prerender.html 200!
|
||||
/${result.landscape.name} /${result.landscape.name}/prerender.html 200!
|
||||
/${result.landscape.name}/* /${result.landscape.name}/index.html 200
|
||||
`).join('\n');
|
||||
|
||||
const index = generateIndex(results)
|
||||
const robots = `
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
`;
|
||||
require('fs').writeFileSync('dist/_redirects', redirects);
|
||||
require('fs').writeFileSync('dist/index.html', index);
|
||||
require('fs').writeFileSync('dist/robots.html', robots);
|
||||
require('fs').copyFileSync(path.resolve(__dirname, '..', '_headers'), 'dist/_headers')
|
||||
|
||||
const notFoundRedirects = landscapesInfo.landscapes.map(({ name }) => `/${name}/* /${name}/404.html 404`)
|
||||
const functionRedirects = readdirSync('netlify/functions').map(file => {
|
||||
const prefixedName = file.replace(/\..*/, '')
|
||||
const [landscape, functionName] = prefixedName.split('--')
|
||||
const newPath = `/${landscape}/api/${functionName}`
|
||||
return `${newPath} /.netlify/functions/${prefixedName} 200`
|
||||
})
|
||||
writeFileSync('dist/_redirects', [...functionRedirects, ...notFoundRedirects].join('\n'))
|
||||
|
||||
require('fs').writeFileSync("dist/robots.txt", "User-agent: *");
|
||||
// comment below when about to test a googlebot rendering
|
||||
require('fs').appendFileSync("dist/robots.txt", "Disallow: /");
|
||||
|
||||
runLocalWithoutErrors('cp -r dist netlify');
|
||||
await runLocalWithoutErrors('cp -r dist netlify');
|
||||
|
||||
if (process.env.BRANCH === 'master') {
|
||||
console.info(await runLocal('git remote -v'));
|
||||
await runLocalWithoutErrors(`
|
||||
git config --global user.email "info@cncf.io"
|
||||
git config --global user.name "CNCF-bot"
|
||||
git remote rm github 2>/dev/null || true
|
||||
git remote add github "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/cncf/landscapeapp"
|
||||
git fetch github
|
||||
# git diff # Need to comment this when a diff is too large
|
||||
git checkout -- .
|
||||
npm version patch || npm version patch || npm version patch
|
||||
git commit -m 'Update to a new version [skip ci]' --allow-empty --amend
|
||||
git branch -D tmp || true
|
||||
git checkout -b tmp
|
||||
git push github HEAD:master || true
|
||||
git push github HEAD:master --tags --force
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
git diff
|
||||
npm -q publish || (sleep 5 && npm -q publish) || (sleep 30 && npm -q publish)
|
||||
echo 'Npm package published'
|
||||
git remote add github "git@github.com:cncf/landscapeapp.git"
|
||||
echo 1
|
||||
GIT_SSH_COMMAND='ssh -i ~/.ssh/bot3 -o IdentitiesOnly=yes' git fetch github
|
||||
echo 2
|
||||
git --no-pager show HEAD
|
||||
echo 3
|
||||
GIT_SSH_COMMAND='ssh -i ~/.ssh/bot3 -o IdentitiesOnly=yes' git push github github/master:deploy
|
||||
`);
|
||||
// just for debug purpose
|
||||
//now we have a different hash, because we updated a version, but for build purposes we have exactly same npm modules
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
// this "server.js" script should be able to run everything itself, without
|
||||
// having to bother with any packages or similar problems.
|
||||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
|
||||
const oldCreateConnection = require('https').globalAgent.createConnection;
|
||||
require('https').globalAgent.createConnection = function(options, cb) {
|
||||
options.highWaterMark = 1024 * 1024;
|
||||
options.readableHighWaterMark = 1024 * 1024;
|
||||
return oldCreateConnection.apply(this, [options, cb]);
|
||||
}
|
||||
|
||||
|
||||
// we will get a content of all files, in a form of entries
|
||||
// "file", "content", "md5"
|
||||
async function getContent() {
|
||||
const dirs = ["images", "hosted_logos", "cached_logos"];
|
||||
const files = ["landscape.yml", "settings.yml", "processed_landscape.yml", "guide.md"];
|
||||
const all = [];
|
||||
for (let dir of dirs) {
|
||||
const filesInDir = await fs.readdir(dir);
|
||||
for (let file of filesInDir) {
|
||||
if (file !== '.' && file !== '..') {
|
||||
const content = await fs.readFile(`${dir}/${file}`, { encoding: 'base64'});
|
||||
const md5 = require('crypto').createHash('md5').update(content).digest("hex");
|
||||
all.push({
|
||||
file: `${dir}/${file}`,
|
||||
content: content,
|
||||
md5: md5
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let file of files) {
|
||||
let content;
|
||||
try {
|
||||
content = await fs.readFile(file, { encoding: 'base64'});
|
||||
} catch(ex) {
|
||||
|
||||
}
|
||||
if (content) {
|
||||
const md5 = require('crypto').createHash('md5').update(content).digest("hex");
|
||||
all.push({
|
||||
file: file,
|
||||
content: content,
|
||||
md5: md5
|
||||
});
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
function get(path) {
|
||||
return new Promise(function(resolve) {
|
||||
const base = process.env.DEBUG_SERVER ? 'http://localhost:3000' : 'https://weblandscapes.ddns.net';
|
||||
const http = require(base.indexOf('http://') === 0 ? 'http' : 'https');
|
||||
const originalPath = path;
|
||||
path = `${base}/api/console/download/${path}`;
|
||||
const req = http.request(path, function(res) {
|
||||
const path1 = originalPath.replace('?', '.html?');
|
||||
if (res.statusCode === 404 && path.indexOf('.html') === -1) {
|
||||
get(path1).then( (x) => resolve(x));
|
||||
} else {
|
||||
resolve({
|
||||
res: res,
|
||||
headers: res.headers,
|
||||
statusCode: res.statusCode
|
||||
});
|
||||
}
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function post({path, request}) {
|
||||
return new Promise(function(resolve) {
|
||||
const base = process.env.DEBUG_SERVER ? 'http://localhost:3000' : 'https://weblandscapes.ddns.net';
|
||||
const http = require(base.indexOf('http://') === 0 ? 'http' : 'https');
|
||||
|
||||
let data = '';
|
||||
const req = http.request({
|
||||
hostname: base.split('://')[1].replace(':3000', ''),
|
||||
port: base.indexOf('3000') !== -1 ? '3000' : 443,
|
||||
path: path,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
}, function(res) {
|
||||
res.on('data', function(chunk) {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('close', function() {
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
req.write(JSON.stringify(request));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// a build is done on a server side, instead of a client side, this
|
||||
// is a key moment
|
||||
// 1. get content (that is fast)
|
||||
// 2. send a list of hashes
|
||||
// 3. get back a list of existing hashes
|
||||
// 4. send a list of (file, content, hash) , but skip the content when a hash is on the server
|
||||
// 5. get a build result on a server
|
||||
let previousGlobalHash;
|
||||
let lastOutput;
|
||||
let currentPath;
|
||||
async function build() {
|
||||
const files = await getContent();
|
||||
if (!previousGlobalHash) {
|
||||
console.info(`Starting a new build...`);
|
||||
}
|
||||
if (JSON.stringify(files.map( (x) => x.md5)) === previousGlobalHash) {
|
||||
return;
|
||||
}
|
||||
if (previousGlobalHash) {
|
||||
console.info(`Changes detected, starting a new build`);
|
||||
}
|
||||
previousGlobalHash = JSON.stringify(files.map( (x) => x.md5));
|
||||
const availableIds = await post({path: '/api/console/ids', request: { ids: files.map( (x) => x.md5 ) }});
|
||||
const availableSet = new Set(availableIds.existingIds);
|
||||
const filesWithoutExtraContent = files.map( (file) => ({
|
||||
file: file.file,
|
||||
md5: file.md5,
|
||||
content: availableSet.has(file.md5) ? '' : file.content
|
||||
}));
|
||||
const result = await post({path: '/api/console/preview', request: { files: filesWithoutExtraContent }});
|
||||
if (result.success) {
|
||||
currentPath = result.path;
|
||||
console.info(`${new Date().toISOString()} build result: ${result.success ? 'success' : 'failure'} `);
|
||||
} else {
|
||||
lastOutput = result.output;
|
||||
console.info(`${new Date().toISOString()} build result: ${result.success ? 'success' : 'failure'} `);
|
||||
console.info(result.output);
|
||||
}
|
||||
}
|
||||
|
||||
function server() {
|
||||
const http = require('http');
|
||||
http.createServer(async function (request, response) {
|
||||
if (!currentPath) {
|
||||
response.writeHead(404);
|
||||
if (lastOutput) {
|
||||
response.end(lastOutput);
|
||||
} else {
|
||||
response.end('Site is not ready');
|
||||
}
|
||||
} else {
|
||||
let filePath = request.url.split('?')[0];
|
||||
const url = path.join(currentPath, 'dist', filePath + '?' + request.url.split('?')[1]);
|
||||
console.info(`Fetching ${url}`);
|
||||
const output = await get(url);
|
||||
response.writeHead(output.statusCode, output.headers);
|
||||
output.res.pipe(response);
|
||||
}
|
||||
|
||||
}).listen(process.env.PORT || 8001);
|
||||
console.log(`Development server running at http://127.0.0.1:${process.env.PORT || 8001}/`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
server();
|
||||
//eslint-disable-next-line no-constant-condition
|
||||
while(true) {
|
||||
await build();
|
||||
}
|
||||
}
|
||||
main().catch(function(ex) {
|
||||
console.info(ex);
|
||||
});
|
||||
// how will a server work?
|
248
package.json
248
package.json
|
@ -1,192 +1,82 @@
|
|||
{
|
||||
"name": "interactive-landscape",
|
||||
"version": "1.0.450",
|
||||
"version": "1.0.657",
|
||||
"description": "Visualization tool for building interactive landscapes",
|
||||
"engines": {
|
||||
"npm": ">=3",
|
||||
"node": ">= 10.5"
|
||||
},
|
||||
"scripts": {
|
||||
"autocrop-images": "babel-node tools/autocropImages",
|
||||
"open:src": "yarn yaml2json && babel-node tools/srcServer.js",
|
||||
"open:dist": "babel-node tools/distServer.js",
|
||||
"landscapes": "babel-node tools/landscapes.js",
|
||||
"lint": "esw webpack.config.* src tools --color",
|
||||
"lint:watch": "yarn lint -- --watch",
|
||||
"eslint": "eslint src tools specs netlify",
|
||||
"autocrop-images": "node tools/autocropImages",
|
||||
"dev": "yarn server",
|
||||
"open:src": "yarn server",
|
||||
"server": "node server.js",
|
||||
"landscapes": "node tools/landscapes.js",
|
||||
"update-github-colors": "curl https://raw.githubusercontent.com/Diastro/github-colors/master/github-colors.json > tools/githubColors.json",
|
||||
"migrate": "babel-node tools/migrate",
|
||||
"fetch": "babel-node tools/validateLandscape && yarn migrate && babel-node tools/checkWrongCharactersInFilenames && babel-node tools/addExternalInfo.js && yarn yaml2json",
|
||||
"fetch": "node tools/validateLandscape && node tools/checkWrongCharactersInFilenames && node tools/addExternalInfo.js && yarn yaml2json",
|
||||
"fetchAll": "LEVEL=complete yarn fetch",
|
||||
"clean-dist": "yarn remove-dist && mkdir \"$PROJECT_PATH\"/dist",
|
||||
"update": "(rm /tmp/landscape.json || true) && babel-node tools/validateLandscape && yarn migrate && yarn remove-quotes && LEVEL=medium babel-node tools/addExternalInfo.js && yarn prune && yarn check-links && yarn yaml2json && babel-node tools/calculateNumberOfTweets && babel-node tools/updateTimestamps",
|
||||
"yaml2json": "babel-node tools/generateJson.js",
|
||||
"remove-quotes": "babel-node tools/removeQuotes",
|
||||
"prune": "babel-node tools/pruneExtraEntries",
|
||||
"check-links": "babel-node tools/checkLinks",
|
||||
"remove-dist": "rimraf \"$PROJECT_PATH\"/dist",
|
||||
"light-update": "(rm /tmp/landscape.json || true) && node tools/validateLandscape && yarn remove-quotes && LEVEL=crunchbase node tools/addExternalInfo.js && yarn prune && yarn yaml2json && node tools/updateTimestamps",
|
||||
"update": "(rm /tmp/landscape.json || true) && node tools/validateLandscape && yarn remove-quotes && LEVEL=medium node tools/addExternalInfo.js && yarn prune && yarn check-links && yarn yaml2json && node tools/updateTimestamps",
|
||||
"yaml2json": "node tools/generateJson.js",
|
||||
"remove-quotes": "node tools/removeQuotes",
|
||||
"prune": "node tools/pruneExtraEntries",
|
||||
"check-links": "node tools/checkLinks",
|
||||
"precommit": "yarn fetch",
|
||||
"host-images": "babel-node tools/hostImages.js && yarn fetch",
|
||||
"start-ci": "yarn exec bash -c \"(yarn run babel-node tools/ciServer &) && sleep 10\"",
|
||||
"stop-old-ci": "yarn run babel-node tools/stopOldCiServer",
|
||||
"stop-ci": "yarn exec bash -c \"kill -9 `cat /tmp/ci.pid` && rm /tmp/ci.pid\"",
|
||||
"integration-test": "jest ${JEST_OPTIONS:-} --reporters=./tools/jestReporter.js --reporters=jest-standard-reporter",
|
||||
"check-landscape": "babel-node tools/checkLandscape",
|
||||
"render-landscape": "babel-node tools/renderLandscape",
|
||||
"funding": "babel-node tools/fundingForMasterBranch",
|
||||
"copy-dist": "cp -r _headers _redirects \"$PROJECT_PATH\"/data.json \"$PROJECT_PATH\"/images src/embed.html \"$PROJECT_PATH\"/dist/ && cp -r \"$PROJECT_PATH\"/cached_logos \"$PROJECT_PATH\"/dist/logos && yarn copy-iframe-resizer",
|
||||
"copy-iframe-resizer": "cp .yarn/unplugged/iframe-resizer*/node_modules/iframe-resizer/js/iframeResizer.min.js \"$PROJECT_PATH\"/dist/iframeResizer.js && yarn exec bash -c 'echo \"`cat \"$PROJECT_PATH\"/dist/iframeResizer.js`\n`cat src/iframeResizer.js`\" > \"$PROJECT_PATH\"/dist/iframeResizer.js'",
|
||||
"setup-robots": "babel-node tools/sitemap && babel-node tools/addRobots",
|
||||
"quick-build": "babel-node tools/build.js && yarn copy-dist && yarn setup-robots",
|
||||
"prerender": "babel-node tools/prerender",
|
||||
"build": "((echo 1 && yarn fetch && yarn clean-dist && babel-node tools/build.js && yarn copy-dist && yarn setup-robots && yarn stop-old-ci && yarn start-ci && babel-node tools/parallelWithRetry integration-test check-landscape render-landscape funding) || (yarn stop-old-ci && false)) && babel-node tools/parallelWithRetry prerender && babel-node tools/parallelWithRetry integration-test && yarn stop-ci",
|
||||
"show-report": "open dist/report.html",
|
||||
"analyze-bundle": "babel-node ./tools/analyzeBundle.js",
|
||||
"start-ci": "yarn exec bash -c \"(node tools/distServer.js &) && sleep 10\"",
|
||||
"stop-old-ci": "node tools/stopOldDistServer.js",
|
||||
"stop-ci": "yarn exec bash -c \"kill -9 `cat /tmp/ci.pid` >/dev/null 2>/dev/null && rm /tmp/ci.pid \"",
|
||||
"integration-test": "jest --runInBand --reporters=jest-standard-reporter",
|
||||
"test": "jest",
|
||||
"check-landscape": "node tools/checkLandscape",
|
||||
"render-landscape": "node tools/renderLandscape",
|
||||
"funding": "node tools/fundingForMasterBranch",
|
||||
"prepare-landscape": "node tools/prepareLandscape.js && node tools/renderItems.js",
|
||||
"setup-robots": "node tools/sitemap && node tools/addRobots",
|
||||
"build": "yarn fetch && yarn prepare-landscape && node tools/renderAcquisitions && yarn setup-robots && yarn export-functions && yarn stop-old-ci && yarn start-ci && node tools/parallelWithRetry integration-test check-landscape render-landscape funding && yarn stop-ci",
|
||||
"export-functions": "node ./tools/exportFunctions",
|
||||
"latest": "yarn",
|
||||
"reset-tweet-count": "babel-node tools/resetTweetCount.js",
|
||||
"reset-tweet-count": "node tools/resetTweetCount.js",
|
||||
"prepublish": "cp yarn.lock _yarn.lock",
|
||||
"postpublish": "rm _yarn.lock || true"
|
||||
"postpublish": "rm _yarn.lock || true",
|
||||
"preview": "yarn fetch && yarn prepare-landscape && yarn export-functions"
|
||||
},
|
||||
"author": "CNCF",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.10.1",
|
||||
"@babel/core": "7.10.2",
|
||||
"@babel/node": "7.10.1",
|
||||
"@babel/plugin-proposal-class-properties": "7.10.1",
|
||||
"@babel/plugin-proposal-decorators": "7.10.1",
|
||||
"@babel/plugin-proposal-do-expressions": "7.10.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.10.1",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.10.1",
|
||||
"@babel/plugin-proposal-function-bind": "7.10.1",
|
||||
"@babel/plugin-proposal-function-sent": "7.10.1",
|
||||
"@babel/plugin-proposal-json-strings": "7.10.1",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "7.10.1",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.10.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.10.1",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.10.1",
|
||||
"@babel/plugin-proposal-pipeline-operator": "7.10.1",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.10.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/plugin-syntax-import-meta": "7.10.1",
|
||||
"@babel/plugin-transform-async-to-generator": "7.10.1",
|
||||
"@babel/plugin-transform-react-constant-elements": "7.10.1",
|
||||
"@babel/plugin-transform-regenerator": "7.10.1",
|
||||
"@babel/plugin-transform-runtime": "7.10.1",
|
||||
"@babel/polyfill": "7.10.1",
|
||||
"@babel/preset-env": "7.10.2",
|
||||
"@babel/preset-react": "7.10.1",
|
||||
"@babel/register": "7.10.1",
|
||||
"@babel/runtime": "^7.10.2",
|
||||
"@date-io/date-fns": "^1.3.13",
|
||||
"@material-ui/core": "4.10.2",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.56",
|
||||
"@material-ui/pickers": "3.2.10",
|
||||
"@rooks/use-window-size": "3.6.0",
|
||||
"autoprefixer": "9.8.0",
|
||||
"axe-puppeteer": "1.1.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-jest": "26.0.1",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"babel-plugin-module-resolver": "4.0.0",
|
||||
"babel-plugin-root-import": "6.5.0",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"babel-preset-latest-node": "4.1.0",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"anchorme": "^2.1.2",
|
||||
"axios": "^0.27.2",
|
||||
"bluebird": "3.7.2",
|
||||
"caniuse-lite": "1.0.30001084",
|
||||
"chalk": "4.1.0",
|
||||
"change-case": "4.1.1",
|
||||
"chart.js": "2.9.3",
|
||||
"chartjs-adapter-date-fns": "1.0.0",
|
||||
"classnames": "2.2.6",
|
||||
"change-case": "^4.1.2",
|
||||
"cheerio": "^1.0.0-rc.11",
|
||||
"colors": "1.4.0",
|
||||
"connect": "3.7.0",
|
||||
"connect-history-api-fallback": "1.6.0",
|
||||
"connected-react-router": "6.8.0",
|
||||
"css-loader": "3.6.0",
|
||||
"cssnano": "4.1.10",
|
||||
"current-device": "0.10.1",
|
||||
"date-fns": "2.14.0",
|
||||
"debug": "4.1.1",
|
||||
"ejs": "3.1.3",
|
||||
"ejs-loader": "0.5.0",
|
||||
"eslint": "7.2.0",
|
||||
"eslint-plugin-import": "2.21.2",
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"eslint-watch": "7.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "latest",
|
||||
"event-emitter": "0.3.5",
|
||||
"expect-puppeteer": "^4.4.0",
|
||||
"express": "^4.17.1",
|
||||
"extract-zip": "2.0.1",
|
||||
"favicons-webpack-plugin": "3.0.1",
|
||||
"feed": "4.2.0",
|
||||
"file-loader": "6.0.0",
|
||||
"expect-puppeteer": "^6.1.0",
|
||||
"feed": "^4.2.2",
|
||||
"format-number": "3.0.0",
|
||||
"get-contrast-ratio": "^0.2.1",
|
||||
"git-branch": "2.0.1",
|
||||
"history": "4.10.1",
|
||||
"iframe-resizer": "4.2.11",
|
||||
"jest": "26.0.1",
|
||||
"jest-cli": "26.0.1",
|
||||
"jest-puppeteer": "4.4.0",
|
||||
"jest-standard-reporter": "1.0.4",
|
||||
"js-yaml": "3.14.0",
|
||||
"jsdom": "16.2.2",
|
||||
"json-loader": "0.5.7",
|
||||
"json2csv": "5.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"measure-text-width": "0.0.4",
|
||||
"millify": "3.2.1",
|
||||
"mini-css-extract-plugin": "0.9.0",
|
||||
"minimatch": "3.0.4",
|
||||
"node-emoji": "1.10.0",
|
||||
"node-sass": "4.14.1",
|
||||
"open": "7.0.4",
|
||||
"pnp-webpack-plugin": "^1.6.4",
|
||||
"postcss-loader": "3.0.0",
|
||||
"prop-types": "15.7.2",
|
||||
"puppeteer": "4.0.0",
|
||||
"query-string": "6.13.1",
|
||||
"raf": "3.4.1",
|
||||
"react": "16.13.1",
|
||||
"react-chartjs-2": "2.9.0",
|
||||
"react-dom": "16.13.1",
|
||||
"react-ga": "3.0.0",
|
||||
"react-hot-loader": "4.12.21",
|
||||
"react-key-handler": "1.2.0-beta.3",
|
||||
"react-redux": "7.2.0",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-snap": "1.23.0",
|
||||
"react-test-renderer": "16.13.1",
|
||||
"react-twitter-widgets": "1.9.5",
|
||||
"recompose": "0.30.0",
|
||||
"redux": "4.0.5",
|
||||
"redux-immutable-state-invariant": "2.1.0",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.5",
|
||||
"iframe-resizer": "^4.3.2",
|
||||
"jest": "^28.1.0",
|
||||
"jest-cli": "^28.1.0",
|
||||
"jest-standard-reporter": "^2.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json2csv": "^5.0.7",
|
||||
"lodash": "^4.17.21",
|
||||
"node-emoji": "^1.11.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"puppeteer": "^14.2.1",
|
||||
"query-string": "^7.1.1",
|
||||
"relative-date": "1.1.3",
|
||||
"request": "2.88.2",
|
||||
"request-promise": "4.2.5",
|
||||
"reselect": "4.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"sass-loader": "8.0.2",
|
||||
"serve-static": "1.14.1",
|
||||
"sitemap": "6.1.5",
|
||||
"style-loader": "1.2.1",
|
||||
"svg-autocrop": "2.0.18",
|
||||
"terser-webpack-plugin": "3.0.6",
|
||||
"sanitize-html": "^2.7.0",
|
||||
"showdown": "^2.1.0",
|
||||
"sitemap": "^7.1.1",
|
||||
"svg-autocrop": "^2.0.41",
|
||||
"traverse": "0.6.6",
|
||||
"url-loader": "4.1.0",
|
||||
"webpack": "4.43.0",
|
||||
"webpack-bundle-analyzer": "3.8.0",
|
||||
"webpack-dev-middleware": "3.7.2",
|
||||
"webpack-hot-middleware": "2.25.0",
|
||||
"webpack-md5-hash": "0.0.6",
|
||||
"yaml-loader": "0.6.0",
|
||||
"yarn": "1.22.4"
|
||||
"yarn": "^1.22.18"
|
||||
},
|
||||
"keywords": [
|
||||
"landscape",
|
||||
|
@ -203,31 +93,5 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/cncf/landscapeapp"
|
||||
},
|
||||
"jest": {
|
||||
"haste": {},
|
||||
"testPathIgnorePatterns": [
|
||||
"<rootDir>/node_modules"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"<rootDir>/node_modules"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/tools/assetsTransformer.js",
|
||||
"\\.(css)$": "<rootDir>/tools/assetsTransformer.js"
|
||||
},
|
||||
"setupFiles": [
|
||||
"raf/polyfill"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"html-webpack-plugin": "4.3.0"
|
||||
},
|
||||
"dependenciesMeta": {
|
||||
"iframe-resizer": {
|
||||
"unplugged": true
|
||||
},
|
||||
"open": {
|
||||
"unplugged": true
|
||||
}
|
||||
}
|
||||
"packageManager": "yarn@3.2.1"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
'postcss-import', 'postcss-simple-vars', 'postcss-nested'
|
||||
],
|
||||
};
|
|
@ -0,0 +1,119 @@
|
|||
// acts as a dev server and a dist server
|
||||
// netlify does not use this
|
||||
|
||||
const projectPath = process.env.PROJECT_PATH;
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const fnFile = (file) => {
|
||||
const functionsPath = path.join(projectPath, 'dist', process.env.PROJECT_NAME || '', 'functions' );
|
||||
const destFile = [process.env.PROJECT_NAME, file].filter(_ => _).join('--');
|
||||
return path.join(functionsPath, destFile);
|
||||
}
|
||||
|
||||
http.createServer(function (request, response) {
|
||||
|
||||
if (request.url.indexOf('/api/ids') !== -1) {
|
||||
console.log('api request starting...', request.url);
|
||||
const query = request.url.split('?')[1] || '';
|
||||
|
||||
if (!process.env.INLINE_API) {
|
||||
require('child_process').exec(`node ${fnFile("ids.js")} '${query}'`, {}, function(e, output, err) {
|
||||
console.info(err);
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.end(output);
|
||||
});
|
||||
} else {
|
||||
const output = require('./src/api/ids.js').processRequest(query);
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.end(JSON.stringify(output));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (request.url.indexOf('/api/export') !== -1) {
|
||||
console.log('api request starting...', request.url);
|
||||
const query = request.url.split('?')[1] || '';
|
||||
|
||||
if (!process.env.INLINE_API) {
|
||||
require('child_process').exec(`node ${fnFile("export.js")} '${query}'`, {}, function(e, output, err) {
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/css',
|
||||
'Content-Disposition': 'attachment; filename=interactive-landscape.csv'
|
||||
});
|
||||
response.end(output);
|
||||
});
|
||||
} else {
|
||||
const output = require('./src/api/export.js').processRequest(query);
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/css',
|
||||
'Content-Disposition': 'attachment; filename=interactive-landscape.csv'
|
||||
});
|
||||
response.end(output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let filePath = path.join(process.env.PROJECT_PATH, 'dist', request.url.split('?')[0]);
|
||||
if (fs.existsSync(path.resolve(filePath, 'index.html'))) {
|
||||
filePath = path.resolve(filePath, 'index.html');
|
||||
} else if (fs.existsSync(filePath + '.html')) {
|
||||
filePath = filePath + '.html'
|
||||
}
|
||||
|
||||
const extname = path.extname(filePath);
|
||||
let encoding = 'utf-8';
|
||||
var contentType = 'text/html; charset=utf-8';
|
||||
switch (extname) {
|
||||
case '.js':
|
||||
contentType = 'text/javascript; charset=utf-8';
|
||||
break;
|
||||
case '.css':
|
||||
contentType = 'text/css; charset=utf-8';
|
||||
break;
|
||||
case '.json':
|
||||
contentType = 'application/json; charset=utf-8';
|
||||
break;
|
||||
case '.svg':
|
||||
contentType = 'image/svg+xml; charset=utf-8';
|
||||
break;
|
||||
case '.jpg':
|
||||
contentType = 'image/jpg';
|
||||
encoding = undefined;
|
||||
break;
|
||||
case '.png':
|
||||
contentType = 'image/png';
|
||||
encoding = undefined;
|
||||
break;
|
||||
case '.pdf':
|
||||
contentType = 'application/pdf';
|
||||
encoding = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
fs.readFile(filePath, encoding, function(error, content) {
|
||||
if (error) {
|
||||
const extraPath = filePath + '.html';
|
||||
fs.readFile(extraPath, encoding, function(error, content) {
|
||||
if (error) {
|
||||
if(error.code == 'ENOENT') {
|
||||
response.writeHead(200, { 'Content-Type': contentType });
|
||||
response.end('404. Not found. ', 'utf-8');
|
||||
} else {
|
||||
response.writeHead(500);
|
||||
response.end('Sorry, check with the site admin for error: '+error.code+' ..\n');
|
||||
response.end();
|
||||
}
|
||||
} else {
|
||||
response.writeHead(200, { 'Content-Type': contentType });
|
||||
response.end(content, 'utf-8');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
response.writeHead(200, { 'Content-Type': contentType });
|
||||
response.end(content, 'utf-8');
|
||||
}
|
||||
});
|
||||
}).listen(process.env.PORT || 8001);
|
||||
console.log(`Development server running at http://127.0.0.1:${process.env.PORT || 8001}/`);
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
globals: {
|
||||
jest: true,
|
||||
test: true,
|
||||
it: true,
|
||||
describe: true,
|
||||
expect: true
|
||||
}
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
import puppeteer from "puppeteer";
|
||||
const { AxePuppeteer } = require('axe-puppeteer');
|
||||
|
||||
const appUrl = `http://localhost:${(process.env.PORT || '4000')}`;
|
||||
|
||||
const analyzePage = async (url) => {
|
||||
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'], defaultViewport: { width: 1600, height: 1200 }});
|
||||
const page = await browser.newPage();
|
||||
await page.goto(appUrl);
|
||||
await page.waitForSelector('.cards-section')
|
||||
const results = await new AxePuppeteer(page).withTags('wcag2a').analyze()
|
||||
await browser.close()
|
||||
|
||||
if (results.violations.length > 0) {
|
||||
throw(`Page has accessibility issues!: \n\n${JSON.stringify(results.violations, null, 4)}`)
|
||||
}
|
||||
}
|
||||
|
||||
describe("Accessibility", () => {
|
||||
test("Main Landscape", async () => {
|
||||
await analyzePage(appUrl)
|
||||
}, 60 * 1000);
|
||||
|
||||
test("Card Mode", async () => {
|
||||
await analyzePage(`${appUrl}/format=card-mode`)
|
||||
}, 60 * 1000);
|
||||
});
|
|
@ -1,17 +1,14 @@
|
|||
|
||||
import puppeteer from "puppeteer";
|
||||
const puppeteer = require("puppeteer");
|
||||
require('expect-puppeteer');
|
||||
import { devicesMap } from 'puppeteer/DeviceDescriptors';
|
||||
import { paramCase } from 'change-case';
|
||||
import { settings } from '../tools/settings';
|
||||
import { projects } from '../tools/loadData';
|
||||
import { landscapeSettingsList } from "../src/utils/landscapeSettings";
|
||||
const port = process.env.PORT || '4000';
|
||||
const appUrl = `http://localhost:${port}`;
|
||||
const { paramCase } = require('change-case');
|
||||
const { settings } = require('../tools/settings');
|
||||
const { projects } = require('../tools/loadData');
|
||||
const { landscapeSettingsList } = require("../src/utils/landscapeSettings");
|
||||
const { appUrl, pathPrefix } = require('../tools/distSettings');
|
||||
|
||||
const width = 1920;
|
||||
const height = 1080;
|
||||
|
||||
let setup;
|
||||
let browser;
|
||||
let page;
|
||||
let close = () => test('Closing a browser', async () => await browser.close());
|
||||
|
@ -27,145 +24,142 @@ expect.extend({
|
|||
return { pass, message };
|
||||
},
|
||||
})
|
||||
|
||||
jest.setTimeout(process.env.SHOW_BROWSER ? 30000 : 20000);
|
||||
jest.setTimeout(process.env.SHOW_BROWSER ? 30000 : 30000);
|
||||
|
||||
async function makePage(initialUrl) {
|
||||
try {
|
||||
browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'], headless: !process.env.SHOW_BROWSER});
|
||||
const page = await browser.newPage();
|
||||
await setup(page);
|
||||
await page.goto(initialUrl);
|
||||
await page.setViewport({ width, height });
|
||||
return page;
|
||||
} catch(ex) {
|
||||
try {
|
||||
console.info('retrying...', ex);
|
||||
browser.close();
|
||||
} catch(ex2) {
|
||||
|
||||
console.info('failed to close browser', ex2);
|
||||
}
|
||||
return await makePage(initialUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function embedTest() {
|
||||
describe("Embed test", () => {
|
||||
describe("I visit an example embed page", () => {
|
||||
let frame;
|
||||
test('page is open and has a frame', async function(){
|
||||
page = await makePage(appUrl + '/embed.html');
|
||||
frame = await page.frames()[1];
|
||||
await frame.waitForXPath(`//h1[contains(text(), 'full interactive landscape')]`);
|
||||
});
|
||||
|
||||
test('Do not see a content from a main mode', async function() {
|
||||
await expect(frame).not.toHaveElement(`//h1[text() = '${settings.test.header}']`);
|
||||
});
|
||||
|
||||
// ensure that it is clickable
|
||||
test('I can click on a tile in a frame and I get a modal after that', async function() {
|
||||
await expect(frame).toHaveElement(`.mosaic img`);
|
||||
await frame.click(`.mosaic img`);
|
||||
await frame.waitForSelector(".modal-content");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
});
|
||||
async function waitForSelector(page, selector) {
|
||||
await page.waitForFunction(`document.querySelector('${selector}') && document.querySelector('${selector}').clientHeight != 0`);
|
||||
}
|
||||
|
||||
function mainTest() {
|
||||
describe("Main test", () => {
|
||||
describe("I visit a main page and have all required elements", () => {
|
||||
test('I can open a page', async function() {
|
||||
page = await makePage(appUrl + '/format=card-mode');
|
||||
await page.waitFor('.cards-section');
|
||||
});
|
||||
|
||||
//header
|
||||
test('A proper header is present', async function() {
|
||||
await expect(page).toHaveElement(`//h1[text() = '${settings.test.header}']`);
|
||||
});
|
||||
test('Group headers are ok', async function() {
|
||||
await expect(page).toHaveElement(`//a[contains(text(), '${settings.test.section}')]`);
|
||||
});
|
||||
test('I see a You are viewing text', async function() {
|
||||
await expect(page).toHaveElement(`//*[contains(text(), 'You are viewing ')]`);
|
||||
});
|
||||
test(`A proper card is present`, async function() {
|
||||
await expect(page).toHaveElement(`.mosaic img[src='logos/${settings.test.logo}']`);
|
||||
});
|
||||
test(`If I click on a card, I see a modal dialog`, async function() {
|
||||
await page.click(`.mosaic img[src='logos/${settings.test.logo}']`);
|
||||
await page.waitForSelector(".modal-content");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
});
|
||||
async function waitForSummaryText(page, text) {
|
||||
await page.waitForFunction(`document.querySelector('.summary') && document.querySelector('.summary').innerText.includes('${text}')`);
|
||||
}
|
||||
|
||||
function landscapeTest() {
|
||||
describe("Big Picture Test", () => {
|
||||
describe("I visit a main landscape page and have all required elements", () => {
|
||||
test('I open a landscape page and wait for it to load', async function() {
|
||||
page = await makePage(appUrl);
|
||||
await page.waitForSelector('.big-picture-section');
|
||||
});
|
||||
test('When I click on an item the modal is open', async function() {
|
||||
await page.click('.big-picture-section img[src]');
|
||||
await page.waitForSelector(".modal-content");
|
||||
});
|
||||
async function waitForHeaderText(page, text) {
|
||||
await page.waitForFunction(`[...document.querySelectorAll('.sh_wrapper')].find( (x) => x.innerText.includes('${text}'))`);
|
||||
}
|
||||
|
||||
// and check that without redirect it works too
|
||||
test('If I would straight open the url with a selected id, a modal appears', async function() {
|
||||
await page.goto(appUrl);
|
||||
await page.waitForSelector('.big-picture-section');
|
||||
await page.click('.big-picture-section img[src]');
|
||||
await page.waitForSelector(".modal-content");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
});
|
||||
landscapeSettingsList.slice(1).forEach(({ name, url }) => {
|
||||
test(`I visit ${name} landscape page and have all required elements, elemens are clickable`, async () => {
|
||||
const page = await makePage(`${appUrl}/format=${url}`);
|
||||
await page.waitForSelector('.big-picture-section');
|
||||
await page.click('.big-picture-section img[src]');
|
||||
await page.waitForSelector(".modal-content");
|
||||
// describe("Embed test", () => {
|
||||
// describe("I visit an example embed page", () => {
|
||||
// let frame;
|
||||
// test('page is open and has a frame', async function(){
|
||||
// page = await makePage(appUrl + '/embed');
|
||||
// frame = await page.frames()[1];
|
||||
// await frame.waitForSelector('.cards-section .mosaic');
|
||||
// await waitForSelector(frame, '#embedded-footer');
|
||||
// });
|
||||
|
||||
// test('Do not see a content from a main mode', async function() {
|
||||
// const title = await frame.$('h1', { text: settings.test.header })
|
||||
// expect(await title.boundingBox()).toBe(null)
|
||||
// });
|
||||
|
||||
// // ensure that it is clickable
|
||||
// test('I can click on a tile in a frame and I get a modal after that', async function() {
|
||||
// await waitForSelector(frame, ".cards-section .mosaic img");
|
||||
// await frame.click(`.mosaic img`);
|
||||
// });
|
||||
// close();
|
||||
// }, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
// });
|
||||
|
||||
describe("Main test", () => {
|
||||
describe("I visit a main page and have all required elements", () => {
|
||||
test('I can open a page', async function() {
|
||||
page = await makePage(appUrl + '/card-mode');
|
||||
await page.waitForSelector('.cards-section .mosaic');
|
||||
});
|
||||
|
||||
//header
|
||||
test('A proper header is present', async function() {
|
||||
await expect(page).toHaveElement(`//h1[text() = '${settings.test.header}']`);
|
||||
});
|
||||
|
||||
test('Group headers are ok', async function() {
|
||||
await waitForHeaderText(page, settings.test.section);
|
||||
});
|
||||
|
||||
test('I see a You are viewing text', async function() {
|
||||
await waitForSummaryText(page, 'You are viewing ');
|
||||
});
|
||||
|
||||
test(`A proper card is present`, async function() {
|
||||
await expect(page).toHaveElement(`.mosaic img[src='${pathPrefix}/logos/${settings.test.logo}']`);
|
||||
});
|
||||
|
||||
test(`If I click on a card, I see a modal dialog`, async function() {
|
||||
await page.click(`.mosaic img[src='${pathPrefix}/logos/${settings.test.logo}']`);
|
||||
await waitForSelector(page, ".modal-content .product-logo");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
});
|
||||
|
||||
describe("Landscape Test", () => {
|
||||
describe("I visit a main landscape page and have all required elements", () => {
|
||||
test('I open a landscape page and wait for it to load', async function() {
|
||||
page = await makePage(appUrl);
|
||||
await page.waitForSelector('.cards-section [data-mode=main]');
|
||||
});
|
||||
test('When I click on an item the modal is open', async function() {
|
||||
await waitForSelector(page, '.cards-section [data-mode=main] [data-id]');
|
||||
await page.click('.cards-section [data-mode=main] [data-id]');
|
||||
await waitForSelector(page, ".modal-content .product-logo");
|
||||
});
|
||||
|
||||
test('If I would straight open the url with a selected id, a modal appears', async function() {
|
||||
await page.goto(appUrl);
|
||||
await waitForSelector(page, '.cards-section [data-mode=main] [data-id]');
|
||||
await page.click('.cards-section [data-mode=main] [data-id]');
|
||||
await waitForSelector(page, ".modal-content .product-logo");
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
landscapeSettingsList.slice(1).forEach(({ name, basePath, url }) => {
|
||||
test(`I visit ${name} landscape page and have all required elements, elements are clickable`, async () => {
|
||||
const page = await makePage(`${appUrl}/${basePath}`);
|
||||
await waitForSelector(page, `.cards-section [data-mode=${url}] [data-id]`);
|
||||
await page.click(`.cards-section [data-mode=${url}] [data-id]`);
|
||||
await waitForSelector(page, ".modal-content .product-logo");
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
close();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
describe("Normal browser", function() {
|
||||
beforeAll(async function() {
|
||||
setup = async (page) => await page.setViewport({ width, height });
|
||||
})
|
||||
mainTest();
|
||||
landscapeTest();
|
||||
embedTest();
|
||||
|
||||
describe("Filtering by organization", () => {
|
||||
const project = projects[0];
|
||||
const organizationSlug = paramCase(project.organization);
|
||||
const otherProject = projects.find(({ organization }) => organization !== project.organization);
|
||||
describe("Filtering by organization", () => {
|
||||
const project = projects[0];
|
||||
const organizationSlug = paramCase(project.organization);
|
||||
const otherProject = projects.find(({ organization }) => organization.toLowerCase() !== project.organization.toLowerCase());
|
||||
if (otherProject) {
|
||||
const otherOrganizationSlug = paramCase(otherProject.organization);
|
||||
|
||||
test(`Checking we see ${project.name} when filtering by organization ${project.organization}`, async function() {
|
||||
page = await makePage(`${appUrl}/organization=${organizationSlug}&format=card-mode`);
|
||||
page = await makePage(`${appUrl}/card-mode?organization=${organizationSlug}`);
|
||||
await page.waitForSelector('.cards-section .mosaic');
|
||||
await expect(page).toHaveElement(`//div[contains(@class, 'mosaic')]//*[text()='${project.name}']`);
|
||||
});
|
||||
test(`Checking we don't see ${project.name} when filtering by organization ${otherProject.organization}`, async function() {
|
||||
await page.goto(`${appUrl}/organization=${otherOrganizationSlug}&format=card-mode`);
|
||||
await page.goto(`${appUrl}/card-mode?organization=${otherOrganizationSlug}`);
|
||||
await page.waitForSelector('.cards-section .mosaic');
|
||||
await expect(page).not.toHaveElement(`//div[contains(@class, 'mosaic')]//*[text()='${project.name}']`);
|
||||
});
|
||||
close();
|
||||
}, 6 * 60 * 1000);
|
||||
});
|
||||
|
||||
describe("iPhone simulator", function() {
|
||||
beforeAll(async function() {
|
||||
setup = async (page) => await page.emulate(devicesMap['iPhone X'])
|
||||
})
|
||||
mainTest();
|
||||
landscapeTest();
|
||||
});
|
||||
}
|
||||
close();
|
||||
}, 6 * 60 * 1000);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import actualTwitter from '../../tools/actualTwitter';
|
||||
const { actualTwitter } = require('../../tools/actualTwitter');
|
||||
|
||||
describe('Twitter URL', () => {
|
||||
describe('when crunchbase data not set', () => {
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
const { flattenItems } = require('../utils/itemsCalculator');
|
||||
const { getGroupedItems, expandSecondPathItems } = require('../utils/itemsCalculator');
|
||||
const { parseParams } = require('../utils/routing');
|
||||
const Parser = require('json2csv/lib/JSON2CSVParser');
|
||||
const { readJsonFromDist } = require('../utils/readJson');
|
||||
|
||||
const allItems = readJsonFromDist('data/items-export');
|
||||
const projects = readJsonFromDist('data/items');
|
||||
|
||||
const processRequest = module.exports.processRequest = query => {
|
||||
const params = parseParams(query);
|
||||
const p = new URLSearchParams(query);
|
||||
params.format = p.get('format');
|
||||
|
||||
let items = projects;
|
||||
if (params.grouping === 'landscape' || params.format !== 'card') {
|
||||
items = expandSecondPathItems(items);
|
||||
}
|
||||
|
||||
// extract alias - if grouping = category
|
||||
// extract alias - if params != card-mode (big_picture - always show)
|
||||
// i.e. make a copy to items here - to get a list of ids
|
||||
|
||||
const selectedItems = flattenItems(getGroupedItems({data: items, skipDuplicates: params.format === 'card', ...params}))
|
||||
.reduce((acc, item) => ({ ...acc, [item.id]: true }), {})
|
||||
|
||||
const fields = allItems[0].map(([label]) => label !== 'id' && label).filter(_ => _);
|
||||
const itemsForExport = allItems
|
||||
.map(item => item.reduce((acc, [label, value]) => ({ ...acc, [label]: value }), {}))
|
||||
.filter(item => selectedItems[item.id]);
|
||||
|
||||
const json2csvParser = new Parser({ fields });
|
||||
const csv = json2csvParser.parse(itemsForExport, { fields });
|
||||
return csv;
|
||||
}
|
||||
|
||||
// Netlify function
|
||||
module.exports.handler = async function(event) {
|
||||
const body = processRequest(event.queryStringParameters)
|
||||
const headers = {
|
||||
'Content-Type': 'text/css',
|
||||
'Content-Disposition': 'attachment; filename=interactive-landscape.csv'
|
||||
};
|
||||
return { statusCode: 200, body: body, headers }
|
||||
}
|
||||
if (__filename === process.argv[1]) {
|
||||
console.info(processRequest(process.argv[2]), null, 4);
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
const { expandSecondPathItems } = require('../utils/itemsCalculator');
|
||||
const { getGroupedItems } = require('../utils/itemsCalculator');
|
||||
const { getSummary, getSummaryText } = require('../utils/summaryCalculator');
|
||||
const { parseParams } = require('../utils/routing');
|
||||
const { readJsonFromDist } = require('../utils/readJson');
|
||||
|
||||
const projects = readJsonFromDist('data/items');
|
||||
|
||||
const processRequest = module.exports.processRequest = query => {
|
||||
const params = parseParams(query);
|
||||
const p = new URLSearchParams(query);
|
||||
params.format = p.get('format');
|
||||
|
||||
let items = projects;
|
||||
if (params.grouping === 'landscape' || params.format !== 'card') {
|
||||
items = expandSecondPathItems(items);
|
||||
}
|
||||
|
||||
const summary = getSummary({data: items, ...params});
|
||||
const groupedItems = getGroupedItems({data: items, skipDuplicates: params.format === 'card', ...params })
|
||||
.map(group => {
|
||||
const items = group.items.map(({ id }) => ({ id } ))
|
||||
return { ...group, items }
|
||||
})
|
||||
|
||||
return {
|
||||
summaryText: getSummaryText(summary),
|
||||
items: groupedItems
|
||||
}
|
||||
}
|
||||
|
||||
// Netlify function
|
||||
module.exports.handler = async function(event) {
|
||||
const body = processRequest(event.queryStringParameters)
|
||||
const headers = { 'Content-Type': 'application/json' }
|
||||
return { statusCode: 200, body: JSON.stringify(body), headers }
|
||||
}
|
||||
|
||||
if (__filename === process.argv[1]) {
|
||||
console.info(JSON.stringify(processRequest(process.argv[2]), null, 4));
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
const { flattenItems, expandSecondPathItems } = require('../utils/itemsCalculator');
|
||||
const { getGroupedItems } = require('../utils/itemsCalculator');
|
||||
const { parseParams } = require('../utils/routing');
|
||||
const { readJsonFromDist } = require('../utils/readJson');
|
||||
|
||||
const projects = readJsonFromDist('data/items');
|
||||
const settings = readJsonFromDist('settings');
|
||||
|
||||
const processRequest = module.exports.processRequest = query => {
|
||||
const params = parseParams({ mainContentMode: 'card-mode', ...query })
|
||||
// extract alias - if grouping = category
|
||||
// extract alias - if params != card-mode (big_picture - always show)
|
||||
// i.e. make a copy to items here - to get a list of ids
|
||||
let items = projects;
|
||||
if (params.grouping === 'landscape') {
|
||||
items = expandSecondPathItems(items);
|
||||
}
|
||||
|
||||
const groupedItems = getGroupedItems({data: items, ...params, skipDuplicates: true})
|
||||
.map(group => {
|
||||
const items = group.items.map(({ id, name, href }) => ({ id, name, logo: `${settings.global.website}/${href}` }))
|
||||
return { ...group, items }
|
||||
})
|
||||
return params.grouping === 'no' ? flattenItems(groupedItems) : groupedItems
|
||||
}
|
||||
// Netlify function
|
||||
module.exports.handler = async function(event) {
|
||||
const body = processRequest(event.queryStringParameters)
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Credentials': true
|
||||
}
|
||||
return { statusCode: 200, body: JSON.stringify(body), headers }
|
||||
}
|
|
@ -1,251 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import Container from '@material-ui/core/Container'
|
||||
import Table from '@material-ui/core/Table'
|
||||
import TableBody from '@material-ui/core/TableBody'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableContainer from '@material-ui/core/TableContainer'
|
||||
import TableHead from '@material-ui/core/TableHead'
|
||||
import TablePagination from '@material-ui/core/TablePagination'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import Typography from "@material-ui/core/Typography"
|
||||
import Box from "@material-ui/core/Box"
|
||||
import Toolbar from "@material-ui/core/Toolbar"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import FormControl from "@material-ui/core/FormControl"
|
||||
import InputLabel from "@material-ui/core/InputLabel"
|
||||
import Select from "@material-ui/core/Select"
|
||||
import MenuItem from "@material-ui/core/MenuItem"
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Dialog from "@material-ui/core/Dialog"
|
||||
import DialogTitle from "@material-ui/core/DialogTitle"
|
||||
import DialogContent from "@material-ui/core/DialogContent"
|
||||
import DialogActions from "@material-ui/core/DialogActions"
|
||||
import FilterListIcon from "@material-ui/icons/FilterList"
|
||||
import Autocomplete from "@material-ui/lab/Autocomplete"
|
||||
import { DatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'
|
||||
import DateFnsUtils from '@date-io/date-fns'
|
||||
import millify from 'millify'
|
||||
import OutboundLink from './OutboundLink'
|
||||
import { filtersToUrl } from '../utils/syncToUrl'
|
||||
|
||||
export default ({ acquisitions, members, acquirers, acquirees }) => {
|
||||
const linkToOrg = (name) => {
|
||||
if (!members.has(name)) {
|
||||
return name
|
||||
}
|
||||
return <OutboundLink to={filtersToUrl({mainContentMode: 'landscape', filters: { organization: name}})}>{name}</OutboundLink>
|
||||
}
|
||||
|
||||
const rowKey = ({ acquirer, acquiree, date }) => {
|
||||
return `${acquirer}-${acquiree}-${date.toString()}`
|
||||
}
|
||||
|
||||
const rowsPerPage = 20
|
||||
const [page, setPage] = useState(0)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const closeDialog = () => setDialogOpen(false)
|
||||
const defaultFilters = {
|
||||
acquirers: [],
|
||||
acquirees: [],
|
||||
min_date: null,
|
||||
max_date: null,
|
||||
min_price: '',
|
||||
max_price: ''
|
||||
}
|
||||
const priceOptions = [
|
||||
{ label: '', value: '' },
|
||||
{ label: '$100M', value: 100000000 },
|
||||
{ label: '$250M', value: 250000000 },
|
||||
{ label: '$500M', value: 500000000 },
|
||||
{ label: '$1B', value: 1000000000 },
|
||||
{ label: '$5B', value: 5000000000 },
|
||||
{ label: '$10B', value: 10000000000 },
|
||||
{ label: '$20B', value: 20000000000 },
|
||||
{ label: '$50B', value: 50000000000 },
|
||||
{ label: '$100B', value: 100000000000 }
|
||||
]
|
||||
const [filters, setFilters] = useState(defaultFilters)
|
||||
const filterFn = (acquisition) => {
|
||||
if (filters.acquirers.length > 0 && !filters.acquirers.includes(acquisition.acquirer)) {
|
||||
return false
|
||||
}
|
||||
if (filters.acquirees.length > 0 && !filters.acquirees.includes(acquisition.acquiree)) {
|
||||
return false
|
||||
}
|
||||
if (filters.min_date && filters.min_date > acquisition.date) {
|
||||
return false
|
||||
}
|
||||
if (filters.max_date && filters.max_date < acquisition.date) {
|
||||
return false
|
||||
}
|
||||
if (filters.min_price && (!acquisition.price || filters.min_price > acquisition.price)) {
|
||||
return false
|
||||
}
|
||||
if (filters.max_price && (!acquisition.price || filters.max_price < acquisition.price)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
setPage(0)
|
||||
setFilters(defaultFilters)
|
||||
}
|
||||
const setFilter = (name, value) => {
|
||||
setPage(0)
|
||||
setFilters({ ...filters, [name]: value })
|
||||
}
|
||||
|
||||
const hasFilters = () => {
|
||||
const { acquirers, acquirees, min_date, max_date, min_price, max_price } = filters
|
||||
return acquirers.length > 0 || acquirees.length > 0 || min_date || max_date || min_price || max_price
|
||||
}
|
||||
|
||||
const filteredAcquisitions = acquisitions.filter(filterFn)
|
||||
|
||||
const data = filteredAcquisitions.slice(page * rowsPerPage, (page + 1) * rowsPerPage)
|
||||
|
||||
const filterDialog = () => {
|
||||
return <Dialog onClose={closeDialog} open={dialogOpen} maxWidth='sm' fullWidth={true}>
|
||||
<DialogTitle>Filter Acquisitions</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box mb={3}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={acquirers}
|
||||
value={filters.acquirers}
|
||||
renderInput={params => <TextField {...params} label="Acquirer"/>}
|
||||
onChange={(_, value) => setFilter('acquirers', value)}
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={3}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={acquirees}
|
||||
value={filters.acquirees}
|
||||
renderInput={params => <TextField {...params} label="Acquiree" />}
|
||||
onChange={(_, value) => setFilter('acquirees', value)}
|
||||
/>
|
||||
</Box>
|
||||
<MuiPickersUtilsProvider utils={DateFnsUtils}>
|
||||
<Box mb={3} flexDirection="row" display={"flex"}>
|
||||
<Box mr={2} flexGrow="1">
|
||||
<DatePicker
|
||||
value={filters.min_date}
|
||||
onChange={value => setFilter('min_date', value)}
|
||||
label="From Date"
|
||||
autoOk
|
||||
fullWidth
|
||||
disableFuture
|
||||
labelFunc={(date) => date ? date.toLocaleDateString() : ''}
|
||||
clearable={true}
|
||||
/>
|
||||
</Box>
|
||||
<Box ml={2} flexGrow="1">
|
||||
<DatePicker
|
||||
value={filters.max_date}
|
||||
onChange={value => setFilter('max_date', value)}
|
||||
label="To Date"
|
||||
autoOk
|
||||
fullWidth
|
||||
disableFuture
|
||||
labelFunc={(date) => date ? date.toLocaleDateString() : ''}
|
||||
clearable={true}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</MuiPickersUtilsProvider>
|
||||
<Box mb={5} flexDirection="row" display={"flex"}>
|
||||
<Box mr={2} flexGrow="1">
|
||||
<FormControl fullWidth={true}>
|
||||
<InputLabel>Min Price</InputLabel>
|
||||
<Select
|
||||
value={filters.min_price}
|
||||
onChange={e => setFilter('min_price', e.target.value)}
|
||||
>
|
||||
{priceOptions.map(({ value, label }) => <MenuItem key={value} value={value}>{label}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box ml={2} flexGrow="1">
|
||||
<FormControl fullWidth={true}>
|
||||
<InputLabel>Max Price</InputLabel>
|
||||
<Select
|
||||
value={filters.max_price}
|
||||
onChange={e => setFilter('max_price', e.target.value)}
|
||||
>
|
||||
{priceOptions.map(({ value, label }) => <MenuItem key={value} value={value}>{label}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={resetFilters} color="primary">
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={closeDialog}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
}
|
||||
|
||||
return <Container maxWidth="lg" disableGutters={true}>
|
||||
<Paper>
|
||||
<Toolbar variant="dense">
|
||||
<Box flexGrow="1">
|
||||
<Typography variant="h6">
|
||||
Acquisitions
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{ hasFilters() ?
|
||||
<Button color="primary" onClick={resetFilters}>Reset</Button>
|
||||
: null
|
||||
}
|
||||
|
||||
<Button
|
||||
aria-label="filter list"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
endIcon={<FilterListIcon>filter</FilterListIcon>}
|
||||
>
|
||||
Filter
|
||||
</Button>
|
||||
|
||||
{filterDialog()}
|
||||
</Toolbar>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Acquirer</TableCell>
|
||||
<TableCell>Acquiree</TableCell>
|
||||
<TableCell align="right">Price</TableCell>
|
||||
<TableCell align="right">Date</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map(acquisition => {
|
||||
return <TableRow key={rowKey(acquisition)}>
|
||||
<TableCell>{linkToOrg(acquisition.acquirer)}</TableCell>
|
||||
<TableCell>{acquisition.acquiree && linkToOrg(acquisition.acquiree)}</TableCell>
|
||||
<TableCell align="right">{acquisition.price && `$${millify(acquisition.price)}`}</TableCell>
|
||||
<TableCell align="right">{acquisition.date.toLocaleDateString()}</TableCell>
|
||||
</TableRow>
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={filteredAcquisitions.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
rowsPerPageOptions={[]}
|
||||
onChangePage={(_, page) => setPage(page)}
|
||||
/>
|
||||
</Paper>
|
||||
</Container>
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { uniq, uniqBy, sortBy } from 'lodash'
|
||||
import Acquisitions from './Acquisitions'
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return { data: state.main.data || [] }
|
||||
};
|
||||
|
||||
const makeOptions = (arr) => sortBy(uniq(arr), name => name.toLowerCase())
|
||||
|
||||
const AcquisitionsContainer = ({ data }) => {
|
||||
const organizations = uniqBy(data, 'crunchbase')
|
||||
|
||||
const acquisitions = organizations
|
||||
.filter(({ crunchbaseData }) => crunchbaseData && crunchbaseData.acquisitions)
|
||||
.map(({ crunchbaseData }) => {
|
||||
return crunchbaseData.acquisitions.map(({ date, ...data }) => {
|
||||
const acquirer = crunchbaseData.name
|
||||
return {
|
||||
date: new Date(date),
|
||||
acquirer,
|
||||
...data
|
||||
}
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
.sort((a, b) => b.date - a.date)
|
||||
|
||||
const members = new Set(organizations.map(({ crunchbaseData }) => crunchbaseData.name))
|
||||
const acquirers = makeOptions(acquisitions.map(a => a.acquirer))
|
||||
const acquirees = makeOptions(acquisitions.map(a => a.acquiree).filter(a => a))
|
||||
|
||||
return <Acquisitions acquisitions={acquisitions}
|
||||
members={members}
|
||||
acquirers={acquirers}
|
||||
acquirees={acquirees}
|
||||
/>
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AcquisitionsContainer);
|
|
@ -1,25 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import settings from 'project/settings.yml';
|
||||
|
||||
const Ad = () => {
|
||||
|
||||
const entries = settings.ads;
|
||||
const normalizeUrl = function(url) {
|
||||
if (url.indexOf('/') === 0) {
|
||||
return url.substring(1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
return <div id="kubecon">
|
||||
{ entries.map( (entry) => (
|
||||
<OutboundLink className="sidebar-event" key={entry.image} to={entry.url} title={entry.title}>
|
||||
<img src={normalizeUrl(entry.image)} alt={entry.title} />
|
||||
</OutboundLink>
|
||||
)) }
|
||||
</div>
|
||||
}
|
||||
export default pure(Ad);
|
|
@ -1,75 +0,0 @@
|
|||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
|
||||
import { FullscreenLandscapeContainer } from "./BigPicture";
|
||||
import HomePageContainer from './HomePageContainer';
|
||||
import AcquisitionsContainer from './AcquisitionsContainer';
|
||||
import NotFoundPage from './NotFoundPage';
|
||||
import { isZoomedIn } from "../utils/browserZoom";
|
||||
import { landscapeSettingsList } from "../utils/landscapeSettings";
|
||||
|
||||
// detect an initial prefix, like /cncf/ or /lfai/ , but it can be just /
|
||||
const possiblePrefix = window.possiblePrefix || '';
|
||||
const prefix = (possiblePrefix && location.pathname.indexOf(possiblePrefix) === 1) ? (possiblePrefix + '/') : '';
|
||||
window.prefix = prefix;
|
||||
|
||||
const theme = createMuiTheme({
|
||||
typography: { "fontFamily": '"Helvetica Neue", "Helvetica", "Arial", sans-serif' }
|
||||
});
|
||||
|
||||
// This is a class-based component because the current
|
||||
// version of hot reloading won't hot reload a stateless
|
||||
// component at the top-level.
|
||||
class App extends React.Component {
|
||||
state = {
|
||||
isZoomed: false
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.checkZoomedIn();
|
||||
window.addEventListener("touchend", this.checkZoomedIn);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener("touchend", this.checkZoomedIn);
|
||||
}
|
||||
|
||||
checkZoomedIn = () => {
|
||||
this.setState({ isZoomed: isZoomedIn() })
|
||||
}
|
||||
|
||||
fullscreenLandscapes = () => {
|
||||
return landscapeSettingsList.map((landscapeSettings) => {
|
||||
const url = landscapeSettings.url
|
||||
const renderer = (props) => <FullscreenLandscapeContainer {...props} landscapeSettings={landscapeSettings} />
|
||||
return <Route exact path={`/${prefix}${url}`} render={renderer} key={url} />
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<div className={this.state.isZoomed ? "zoomed-in" : ""}>
|
||||
<CssBaseline />
|
||||
<Switch>
|
||||
<Route exact path={`/${prefix}`} component={HomePageContainer} />
|
||||
{ this.fullscreenLandscapes() }
|
||||
<Route path={`/${prefix}acquisitions`} component={AcquisitionsContainer} />
|
||||
<Route path={`/${prefix}`} component={HomePageContainer} />
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
children: PropTypes.element
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -1,25 +0,0 @@
|
|||
// locate zoom buttons
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { pure } from 'recompose';
|
||||
import FullscreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
import FullscreenIcon from '@material-ui/icons/Fullscreen';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const FullscreenButton = function({isVisible, isFullscreen, enableFullscreen, disableFullscreen}) {
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
return <div className="fullscreen-button">
|
||||
{ isFullscreen ?
|
||||
<IconButton onClick={disableFullscreen} title="Exit fullscreen">
|
||||
<FullscreenExitIcon />
|
||||
</IconButton>
|
||||
:
|
||||
<IconButton onClick={enableFullscreen} title="Enter fullscreen">
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
export default pure(FullscreenButton);
|
|
@ -1,16 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FullscreenButton from './FullscreenButton';
|
||||
|
||||
import { makeFullscreenEnabled, makeFullscreenDisabled} from '../../reducers/mainReducer';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isFullscreen: state.main.isFullscreen,
|
||||
isVisible: state.main.mainContentMode !== 'card'
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
enableFullscreen: makeFullscreenEnabled,
|
||||
disableFullscreen: makeFullscreenDisabled
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FullscreenButton);
|
|
@ -1,133 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import LandscapeContent from './LandscapeContent';
|
||||
import { calculateSize, outerPadding, headerHeight } from "../../utils/landscapeCalculations";
|
||||
import isDesktop from "../../utils/isDesktop";
|
||||
|
||||
const calculateZoom = (width, height, zoomedIn) => {
|
||||
const boxHeight = height + headerHeight + 2 * outerPadding
|
||||
const boxWidth = width + 2 * outerPadding
|
||||
const isFirefox = navigator.userAgent.indexOf('Firefox') > -1
|
||||
|
||||
const aspectRatio = innerWidth / innerHeight
|
||||
const adjustedWidth = outerWidth
|
||||
const adjustedHeight = adjustedWidth / aspectRatio
|
||||
let baseZoom = Math.min(adjustedHeight / boxHeight, adjustedWidth / boxWidth).toPrecision(4)
|
||||
let wrapperWidth, wrapperHeight
|
||||
|
||||
if (baseZoom <= 0.95 || !isDesktop || isFirefox || location.search.indexOf('scale=false') > -1) {
|
||||
wrapperWidth = Math.max(boxWidth, innerWidth)
|
||||
wrapperHeight = Math.max(boxHeight, innerHeight)
|
||||
baseZoom = 1
|
||||
} else {
|
||||
wrapperWidth = adjustedWidth / baseZoom
|
||||
wrapperHeight = adjustedHeight / baseZoom
|
||||
}
|
||||
|
||||
return { zoom: Math.min(baseZoom * (zoomedIn ? 3 : 1), 3), wrapperWidth, wrapperHeight }
|
||||
}
|
||||
|
||||
const Fullscreen = ({ready, groupedItems, landscapeSettings, version}) => {
|
||||
if (ready !== true) {
|
||||
return <div></div>
|
||||
}
|
||||
|
||||
const [_, setWindowSize] = useState(1)
|
||||
const [zoomedIn, setZoomedIn] = useState(false)
|
||||
const [zoomedAt, setZoomedAt] = useState({})
|
||||
const onZoom = (e) => {
|
||||
setZoomedAt({ x: e.pageX / zoom, y: e.pageY / zoom })
|
||||
setZoomedIn(!zoomedIn)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const calculateWindowSize = () => setWindowSize(`${innerWidth}x${innerHeight}`)
|
||||
window.addEventListener("resize", calculateWindowSize)
|
||||
return () => window.removeEventListener("resize", calculateWindowSize)
|
||||
}, [true]);
|
||||
|
||||
useEffect(() => {
|
||||
zoomedIn ? window.scrollTo((zoomedAt.x * zoom - innerWidth / 2), (zoomedAt.y * zoom - innerHeight / 2)) : null
|
||||
}, [zoomedAt, zoomedIn])
|
||||
|
||||
const { width, height } = calculateSize(landscapeSettings)
|
||||
const { zoom, wrapperWidth, wrapperHeight } = calculateZoom(width, height, zoomedIn)
|
||||
|
||||
return (
|
||||
<div className="gradient-bg" style={{
|
||||
fontFamily: 'roboto',
|
||||
width: wrapperWidth * zoom,
|
||||
height: wrapperHeight * zoom,
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
transform: `scale(${zoom})`,
|
||||
width: wrapperWidth,
|
||||
height: wrapperHeight,
|
||||
transformOrigin: '0 0',
|
||||
paddingTop: headerHeight,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div
|
||||
onClick={isDesktop && onZoom}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
cursor: zoomedIn ? 'zoom-out' : 'zoom-in',
|
||||
zIndex: 100000
|
||||
}}>
|
||||
</div>
|
||||
<LandscapeContent groupedItems={groupedItems} landscapeSettings={landscapeSettings} padding={0} />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
fontSize: 18,
|
||||
background: 'rgb(64,89,163)',
|
||||
color: 'white',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
paddingTop: 3,
|
||||
paddingBottom: 3,
|
||||
borderRadius: 5
|
||||
}}>{landscapeSettings.fullscreen_header}</div>
|
||||
{ !landscapeSettings.fullscreen_hide_grey_logos && <div style={{
|
||||
position: 'absolute',
|
||||
top: 15,
|
||||
right: 12,
|
||||
fontSize: 11,
|
||||
background: '#eee',
|
||||
color: 'rgb(100,100,100)',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
paddingTop: 3,
|
||||
paddingBottom: 3,
|
||||
borderRadius: 5
|
||||
}}>Greyed logos are not open source</div> }
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 15,
|
||||
fontSize: 14,
|
||||
color: 'white',
|
||||
}}>{landscapeSettings.title} </div>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 30,
|
||||
left: 15,
|
||||
fontSize: 12,
|
||||
color: '#eee',
|
||||
}}>{version}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default pure(Fullscreen);
|
|
@ -1,14 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import qs from 'query-string';
|
||||
import FullscreenLandscape from './FullscreenLandscape';
|
||||
import { getGroupedItemsForBigPicture } from '../../utils/itemsCalculator';
|
||||
|
||||
const mapStateToProps = (state, { landscapeSettings }) => ({
|
||||
ready: state.main.ready,
|
||||
groupedItems: state.main.ready && getGroupedItemsForBigPicture(state, landscapeSettings),
|
||||
version: qs.parse(location.search).version
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FullscreenLandscape);
|
|
@ -1,119 +0,0 @@
|
|||
import React, { Fragment } from "react";
|
||||
import { getContrastRatio } from "@material-ui/core/styles";
|
||||
import Item from "./Item";
|
||||
import InternalLink from "../InternalLink";
|
||||
import {
|
||||
calculateHorizontalCategory,
|
||||
categoryBorder,
|
||||
categoryTitleHeight,
|
||||
dividerWidth,
|
||||
itemMargin,
|
||||
smallItemWidth,
|
||||
smallItemHeight,
|
||||
subcategoryMargin,
|
||||
subcategoryTitleHeight
|
||||
} from "../../utils/landscapeCalculations";
|
||||
|
||||
const Divider = (({ color }) => {
|
||||
const width = dividerWidth
|
||||
const marginTop = 2 * subcategoryMargin
|
||||
const height = `calc(100% - ${2 * marginTop}px)`
|
||||
|
||||
return <div style={{ width, marginTop, height, borderLeft: `${width}px solid ${color}` }}/>
|
||||
})
|
||||
|
||||
const HorizontalCategory = (({ header, subcategories, width, height, top, left, color, href, onSelectItem, fitWidth }) => {
|
||||
const subcategoriesWithCalculations = calculateHorizontalCategory({ height, width, subcategories, fitWidth })
|
||||
|
||||
return (
|
||||
<div style={{ width, left, height, top, position: 'absolute' }} className="big-picture-section">
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
background: color,
|
||||
top: subcategoryTitleHeight,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
|
||||
padding: categoryBorder
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
top: 5,
|
||||
bottom: 5,
|
||||
left: 0,
|
||||
width: categoryTitleHeight,
|
||||
position: 'absolute',
|
||||
writingMode: 'vertical-rl',
|
||||
transform: 'rotate(180deg)',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<InternalLink to={href} style={{
|
||||
color: getContrastRatio('#ffffff', color) < 4.5 ? '#282828' : '#ffffff',
|
||||
fontSize: 12,
|
||||
lineHeight: '13px'
|
||||
}}>
|
||||
{header}
|
||||
</InternalLink>
|
||||
</div>
|
||||
<div style={{
|
||||
marginLeft: 30,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-evenly',
|
||||
background: 'white'
|
||||
}}>
|
||||
{subcategoriesWithCalculations.map((subcategory, index) => {
|
||||
const lastSubcategory = index !== subcategories.length - 1
|
||||
const { allItems, columns, width, name, href } = subcategory
|
||||
const padding = fitWidth ? 0 : `${subcategoryMargin}px 0`
|
||||
const style = {
|
||||
display: 'grid',
|
||||
height: '100%',
|
||||
gridTemplateColumns: `repeat(${columns}, ${smallItemWidth}px)`,
|
||||
gridAutoRows: `${smallItemHeight}px`
|
||||
}
|
||||
const extraStyle = fitWidth ? { justifyContent: 'space-evenly', alignContent: 'space-evenly' } : { gridGap: itemMargin }
|
||||
|
||||
return <Fragment key={name}>
|
||||
<div style={{
|
||||
width,
|
||||
position: 'relative',
|
||||
fontSize: 10,
|
||||
overflow: 'visible',
|
||||
padding,
|
||||
boxSizing: 'border-box'
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: -1 * categoryTitleHeight,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: categoryTitleHeight,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<InternalLink to={href} style={{ color: 'white', fontSize: 11 }}>{name}</InternalLink>
|
||||
</div>
|
||||
<div style={{...style, ...extraStyle}}>
|
||||
{
|
||||
allItems.map(item => <Item item={item} onSelectItem={onSelectItem} key={item.name}/>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{lastSubcategory && <Divider color={color}/>}
|
||||
</Fragment>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
});
|
||||
|
||||
export default HorizontalCategory
|
|
@ -1,77 +0,0 @@
|
|||
import React from "react";
|
||||
import Fade from "@material-ui/core/Fade";
|
||||
import settings from 'project/settings.yml'
|
||||
import fields from "../../types/fields";
|
||||
import {
|
||||
largeItemHeight,
|
||||
largeItemWidth,
|
||||
smallItemHeight,
|
||||
smallItemWidth
|
||||
} from "../../utils/landscapeCalculations";
|
||||
|
||||
const LargeItem = (({ item, onSelectItem, isMember }) => {
|
||||
const relationInfo = fields.relation.values.find(({ id }) => id === item.relation);
|
||||
const color = relationInfo.big_picture_color;
|
||||
const label = relationInfo.big_picture_label;
|
||||
const textHeight = isMember ? 0 : 10
|
||||
const padding = 2
|
||||
|
||||
return <div style={{
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
background: color,
|
||||
visibility: item.isVisible ? 'visible' : 'hidden',
|
||||
width: largeItemWidth,
|
||||
height: largeItemHeight }}
|
||||
onClick={ () => onSelectItem(item.id)}
|
||||
>
|
||||
<img loading="lazy" src={item.href} style={{
|
||||
width: `calc(100% - ${2 * padding}px)`,
|
||||
height: `calc(100% - ${2 * padding + textHeight}px)`,
|
||||
padding: 5,
|
||||
margin: `${padding}px ${padding}px 0 ${padding}px`,
|
||||
}} data-href={item.id} alt={item.name} />
|
||||
<div style={{position: 'absolute', bottom: 0, width: '100%', height: textHeight + padding, textAlign: 'center', verticalAlign: 'middle', background: color, color: 'white', fontSize: 6.7, lineHeight: '13px'}}>
|
||||
{label}
|
||||
</div>
|
||||
</div>;
|
||||
})
|
||||
|
||||
const SmallItem = (({ item, onSelectItem }) => {
|
||||
const isMember = item.category === settings.global.membership;
|
||||
return <img style={{
|
||||
cursor: 'pointer',
|
||||
width: smallItemWidth,
|
||||
height: smallItemHeight,
|
||||
border: `1px solid ${isMember ? 'white' : 'grey'}`,
|
||||
borderRadius: 2,
|
||||
padding: 1,
|
||||
visibility: item.isVisible ? 'visible' : 'hidden'
|
||||
}}
|
||||
data-href={item.id}
|
||||
loading="lazy"
|
||||
src={item.href}
|
||||
onClick={() => onSelectItem(item.id)}
|
||||
alt={item.name}
|
||||
/>
|
||||
|
||||
})
|
||||
|
||||
export default props => {
|
||||
const { isLarge, isVisible, category, oss } = props.item
|
||||
const isMember = category === settings.global.membership;
|
||||
|
||||
const style = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gridColumnEnd: `span ${isLarge ? 2 : 1}`,
|
||||
gridRowEnd: `span ${isLarge ? 2 : 1}`
|
||||
}
|
||||
|
||||
return <Fade timeout={1000} in={isVisible}>
|
||||
<div className={isMember || oss ? 'oss' : 'nonoss'} style={style}>
|
||||
{isLarge ? <LargeItem {...props} isMember={isMember} /> : <SmallItem {...props} />}
|
||||
</div>
|
||||
</Fade>
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import _ from 'lodash';
|
||||
import HorizontalCategory from './HorizontalCategory'
|
||||
import VerticalCategory from './VerticalCategory'
|
||||
import LandscapeInfo from './LandscapeInfo';
|
||||
import OtherLandscapeLink from './OtherLandscapeLink';
|
||||
import { calculateSize } from "../../utils/landscapeCalculations";
|
||||
|
||||
const extractKeys = (obj, keys) => {
|
||||
const attributes = _.pick(obj, keys)
|
||||
|
||||
return _.mapKeys(attributes, (value, key) => _.camelCase(key))
|
||||
}
|
||||
|
||||
const LandscapeContent = ({groupedItems, onSelectItem, zoom, switchToLandscape, landscapeSettings, padding = 10 }) => {
|
||||
const elements = landscapeSettings.elements.map(function(element) {
|
||||
if (element.type === 'HorizontalCategory') {
|
||||
const cat = _.find(groupedItems, {key: element.category});
|
||||
const attributes = extractKeys(element, ['width', 'height', 'top', 'left', 'color', 'fit_width'])
|
||||
return <HorizontalCategory {...cat} {...attributes} onSelectItem={onSelectItem}/>
|
||||
}
|
||||
if (element.type === 'VerticalCategory') {
|
||||
const cat = _.find(groupedItems, {key: element.category});
|
||||
const attributes = extractKeys(element, ['width', 'height', 'top', 'left', 'color', 'fit_width'])
|
||||
return <VerticalCategory {...cat} {...attributes} onSelectItem={onSelectItem}/>
|
||||
}
|
||||
if (element.type === 'LandscapeLink') {
|
||||
return <OtherLandscapeLink {..._.pick(element, ['width','height','top','left','color', 'layout', 'title', 'url']) }
|
||||
onClick={() => switchToLandscape(element.url)}
|
||||
key={element.url}
|
||||
/>
|
||||
}
|
||||
if (element.type === 'LandscapeInfo') {
|
||||
return <LandscapeInfo {..._.pick(element, ['width', 'height', 'top', 'left']) } childrenInfo={element.children}
|
||||
key='landscape-info'
|
||||
/>
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { width, height } = calculateSize(landscapeSettings)
|
||||
|
||||
const style = {
|
||||
padding,
|
||||
width: width + 2 * padding,
|
||||
height: height + 2 * padding,
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: '0 0'
|
||||
}
|
||||
|
||||
return <div style={style}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{elements}
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default pure(LandscapeContent);
|
|
@ -1,21 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import LandscapeContent from './LandscapeContent';
|
||||
import { changeSelectedItemId, changeMainContentMode } from '../../reducers/mainReducer';
|
||||
import { getGroupedItemsForBigPicture } from '../../utils/itemsCalculator';
|
||||
import { findLandscapeSettings } from "../../utils/landscapeSettings";
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const landscapeSettings = findLandscapeSettings(state.main.mainContentMode);
|
||||
|
||||
return {
|
||||
groupedItems: getGroupedItemsForBigPicture(state, landscapeSettings),
|
||||
zoom: state.main.zoom,
|
||||
landscapeSettings: landscapeSettings
|
||||
}
|
||||
};
|
||||
const mapDispatchToProps = {
|
||||
onSelectItem: changeSelectedItemId,
|
||||
switchToLandscape: changeMainContentMode,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LandscapeContent);
|
|
@ -1,73 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import _ from 'lodash';
|
||||
|
||||
const LandscapeInfo = ({width, height, top, left, childrenInfo}) => {
|
||||
const children = childrenInfo.map(function(info) {
|
||||
const positionProps = {
|
||||
position: 'absolute',
|
||||
top: _.isUndefined(info.top) ? null : info.top,
|
||||
left: _.isUndefined(info.left) ? null : info.left,
|
||||
right: _.isUndefined(info.right) ? null : info.right,
|
||||
bottom: _.isUndefined(info.bottom) ? null : info.bottom,
|
||||
width: _.isUndefined(info.width) ? null : info.width,
|
||||
height: _.isUndefined(info.height) ? null : info.height
|
||||
};
|
||||
if (info.type === 'text') {
|
||||
// pdf requires a normal version without a zoom trick
|
||||
if (window.location.href.indexOf('&pdf') !== -1) {
|
||||
return <div key='text' style={{
|
||||
...positionProps,
|
||||
fontSize: info.font_size,
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'justify',
|
||||
zIndex: 1
|
||||
}}>{info.text}</div>
|
||||
// while in a browser we use a special version which renders fonts
|
||||
// properly on a small zoom
|
||||
} else {
|
||||
return <div key='text' style={{
|
||||
...positionProps,
|
||||
fontSize: info.font_size * 4,
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'justify',
|
||||
zIndex: 1
|
||||
}}><div style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '400%',
|
||||
height: '100%',
|
||||
transform: 'scale(0.25)',
|
||||
transformOrigin: 'left'
|
||||
}}> {info.text} </div></div>
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
if (info.type === 'title') {
|
||||
return <div key='title' style= {{
|
||||
...positionProps,
|
||||
fontSize: info.font_size,
|
||||
color: '#666'
|
||||
}}>{info.title}</div>
|
||||
}
|
||||
if (info.type === 'image') {
|
||||
return <img src={`images/${info.image}`} style={{...positionProps}} key={info.image} alt={info.title || info.image} />
|
||||
}
|
||||
});
|
||||
|
||||
return <div style={{
|
||||
position: 'absolute',
|
||||
width: width,
|
||||
height: height - 20,
|
||||
top: top,
|
||||
left: left,
|
||||
border: '1px solid black',
|
||||
background: 'white',
|
||||
borderRadius: 10,
|
||||
marginTop: 20,
|
||||
boxShadow: `0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)`
|
||||
}}>{children}</div>
|
||||
}
|
||||
export default pure(LandscapeInfo);
|
|
@ -1,29 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
|
||||
const OtherLandscapeLink = function({top, left, height, width, color, onClick, title, url, layout}) {
|
||||
if (layout === 'category') {
|
||||
return <div style={{
|
||||
position: 'absolute', top, left, height, width, background: color,
|
||||
cursor: 'pointer',
|
||||
boxShadow: `0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: 1
|
||||
}} onClick={onClick} >
|
||||
<div style={{ width, height: 30, lineHeight: '25px', textAlign: 'center', color: 'white', fontSize: 12}}>{title}</div>
|
||||
<div style={{ flex: 1, background: 'white'}}>
|
||||
<img loading="lazy" src={`images/${url}_preview.png`} style={{ width: width - 10, height: height - 40, margin: 5,
|
||||
objectFit: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }} alt={title} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (layout === 'subcategory') {
|
||||
return <div style={{position: 'absolute', top, left, height, width, cursor: 'pointer' }} onClick={onClick}>
|
||||
<div style={{ width, top: 0, height: 20, lineHeight: '20px', textAlign: 'center', color: 'white', fontSize: 11}}>{title}</div>
|
||||
<img loading="lazy" src={`images/${url}_preview.png`} alt={title}
|
||||
style={{ width: width, height: height - 20, objectFit: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
export default pure(OtherLandscapeLink);
|
|
@ -1,30 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure, withProps, toClass } from 'recompose';
|
||||
import Tabs from '@material-ui/core/Tabs';
|
||||
import Tab from '@material-ui/core/Tab';
|
||||
import InternalLink from '../InternalLink';
|
||||
import isEmbed from '../../utils/isEmbed';
|
||||
|
||||
const SwitchButton = function({mainContentMode, changeMainContentMode, cards}) {
|
||||
if (isEmbed) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
<Tabs
|
||||
className="big-picture-switch big-picture-switch-normal"
|
||||
value={mainContentMode}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
onChange={(_event, value) => changeMainContentMode(value)}
|
||||
key='tabs'
|
||||
>
|
||||
{ cards.map(({ mode, title, url}) => {
|
||||
const link = toClass(withProps(props => { return { to: url } })(InternalLink));
|
||||
return <Tab key={mode} label={title} component={link} value={mode} />
|
||||
})}
|
||||
</Tabs>
|
||||
]
|
||||
|
||||
|
||||
}
|
||||
export default pure(SwitchButton);
|
|
@ -1,27 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import SwitchButton from './SwitchButton';
|
||||
import { changeMainContentMode } from '../../reducers/mainReducer.js';
|
||||
import { filtersToUrl } from '../../utils/syncToUrl';
|
||||
import settings from 'project/settings.yml';
|
||||
import _ from 'lodash';
|
||||
|
||||
const mainCard = [{shortTitle: 'Card', title: 'Card Mode', mode: 'card', tabIndex: 0}];
|
||||
const landscapes = _.map(settings.big_picture, function(section) {
|
||||
return {
|
||||
title: section.name,
|
||||
shortTitle: section.short_name,
|
||||
mode: section.url,
|
||||
tabIndex: section.tab_index
|
||||
}
|
||||
});
|
||||
const cards = _.orderBy(mainCard.concat(landscapes), 'tabIndex').map( item => _.pick(item, ['title', 'mode', 'shortTitle']));
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
mainContentMode: state.main.mainContentMode,
|
||||
cards: cards.map( (card) => ({ ...card, url: filtersToUrl({filters: state.main.filters, grouping: state.main.grouping, sortField: state.main.sortField, mainContentMode: card.mode})})),
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
changeMainContentMode: changeMainContentMode
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SwitchButton);
|
|
@ -1,50 +0,0 @@
|
|||
import React from "react";
|
||||
import Item from "./Item";
|
||||
import InternalLink from "../InternalLink";
|
||||
import {
|
||||
calculateVerticalCategory,
|
||||
categoryBorder,
|
||||
categoryTitleHeight,
|
||||
itemMargin, smallItemWidth,
|
||||
subcategoryMargin
|
||||
} from "../../utils/landscapeCalculations";
|
||||
|
||||
const VerticalCategory = ({header, subcategories, top, left, width, height, color, href, onSelectItem, fitWidth}) => {
|
||||
const subcategoriesWithCalculations = calculateVerticalCategory({ subcategories, fitWidth, width })
|
||||
return <div>
|
||||
<div style={{
|
||||
position: 'absolute', top, left, height, width, background: color,
|
||||
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
|
||||
padding: categoryBorder,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}} className="big-picture-section">
|
||||
<div style={{ width: '100%', height: categoryTitleHeight, lineHeight: '25px', textAlign: 'center'}}>
|
||||
<InternalLink to={href} style={{ color: 'white', fontSize: 12 }}>
|
||||
{header}
|
||||
</InternalLink>
|
||||
</div>
|
||||
<div style={{ width: '100%', position: 'relative', flex: 1, padding: `${subcategoryMargin}px 0`, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', background: 'white' }}>
|
||||
{subcategoriesWithCalculations.map(subcategory => {
|
||||
const { width, columns } = subcategory
|
||||
const style = { display: 'grid', gridTemplateColumns: `repeat(${columns}, ${smallItemWidth}px)` }
|
||||
const extraStyle = fitWidth ? { justifyContent: 'space-evenly' } : { gridGap: itemMargin }
|
||||
|
||||
return <div key={subcategory.name} style={{position: 'relative'}}>
|
||||
<div style={{ lineHeight: '15px', textAlign: 'center'}}>
|
||||
<InternalLink to={subcategory.href} style={{ color: '#282828', fontSize: 11 }}>
|
||||
{subcategory.name}
|
||||
</InternalLink>
|
||||
</div>
|
||||
|
||||
<div style={{width, overflow: 'hidden', margin: '0 auto', ...style, ...extraStyle}}>
|
||||
{subcategory.allItems.map(item => <Item item={item} onSelectItem={onSelectItem} key={item.name} fitWidth={fitWidth} />)}
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default VerticalCategory
|
|
@ -1,11 +0,0 @@
|
|||
// locate zoom buttons
|
||||
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
|
||||
const Zoom = function({zoom, children}) {
|
||||
return <div style={{position:'relative', zoom: zoom}}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
export default pure(Zoom);
|
|
@ -1,21 +0,0 @@
|
|||
// locate zoom buttons
|
||||
import { pure } from 'recompose';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import RemoveCircleIcon from '@material-ui/icons/RemoveCircle';
|
||||
import AddCircleIcon from '@material-ui/icons/AddCircle';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const ZoomButtons = function({canZoomIn, canZoomOut, zoomText, onZoomIn, onZoomOut, onZoomReset}) {
|
||||
return <div className="zoom-buttons">
|
||||
<IconButton disabled={!canZoomOut} onClick={onZoomOut} className='zoom-change' title="Zoom out">
|
||||
<RemoveCircleIcon />
|
||||
</IconButton>
|
||||
<Button onClick={onZoomReset} className='zoom-reset' title="Reset zoom">{zoomText}</Button>
|
||||
<IconButton disabled={!canZoomIn} onClick={onZoomIn} className='zoom-change' title="Zoom in">
|
||||
<AddCircleIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
export default pure(ZoomButtons);
|
|
@ -1,19 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ZoomButtons from './ZoomButtons';
|
||||
import { makeZoomIn, makeZoomOut, makeZoomReset} from '../../reducers/mainReducer';
|
||||
import { zoomLevels } from '../../utils/zoom';
|
||||
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
canZoomOut: state.main.zoom !== zoomLevels[0],
|
||||
canZoomIn: state.main.zoom !== zoomLevels.slice(-1)[0],
|
||||
zoomText: Math.round(state.main.zoom * 100) + '%'
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onZoomIn: makeZoomIn,
|
||||
onZoomOut: makeZoomOut,
|
||||
onZoomReset: makeZoomReset
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ZoomButtons);
|
|
@ -1,10 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Zoom from './Zoom';
|
||||
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
zoom: state.main.zoom
|
||||
});
|
||||
const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Zoom);
|
|
@ -1,13 +0,0 @@
|
|||
import LandscapeContentContainer from './LandscapeContentContainer';
|
||||
import SwitchButtonContainer from './SwitchButtonContainer';
|
||||
import ZoomButtonsContainer from './ZoomButtonsContainer';
|
||||
import FullscreenLandscapeContainer from './FullscreenLandscapeContainer';
|
||||
import FullscreenButtonContainer from './FullscreenButtonContainer';
|
||||
|
||||
export {
|
||||
LandscapeContentContainer,
|
||||
SwitchButtonContainer,
|
||||
ZoomButtonsContainer,
|
||||
FullscreenLandscapeContainer,
|
||||
FullscreenButtonContainer
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
const _ = require('lodash');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
const { millify, h } = require('../utils/format');
|
||||
const { fields } = require('../types/fields');
|
||||
|
||||
function getRelationStyle(relation) {
|
||||
const relationInfo = fields.relation.valuesMap[relation]
|
||||
if (relationInfo && relationInfo.color) {
|
||||
return `border: 4px solid ${relationInfo.color};`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.renderDefaultCard = function renderDefaultCard({item}) {
|
||||
return `
|
||||
<div data-id="${h(item.id)}" class="mosaic-wrap">
|
||||
<div class="mosaic ${item.oss ? '' : 'nonoss' }" style="${getRelationStyle(item.relation)}">
|
||||
<div class="logo_wrapper">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" class="logo" max-height="100%" max-width="100%" alt="${h(item.name)}" />
|
||||
</div>
|
||||
<div class="mosaic-info">
|
||||
<div class="mosaic-title">
|
||||
<h5>${h(item.name)}</h5>
|
||||
${h(item.organization)}
|
||||
</div>
|
||||
<div class="mosaic-stars">
|
||||
${_.isNumber(item.stars) && item.stars ?
|
||||
`<div>
|
||||
<span>★</span>
|
||||
<span>${h(item.starsAsText)}</span>
|
||||
</div>` : ''
|
||||
}
|
||||
${Number.isInteger(item.amount) ?
|
||||
`<div class="mosaic-funding">${item.amountKind === 'funding' ? 'Funding: ': 'MCap: '} ${'$'+ h(millify(item.amount))}</div>` : ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
module.exports.renderFlatCard = function renderFlatCard({item}) {
|
||||
return `
|
||||
<div data-id="${item.id}" class="mosaic-wrap">
|
||||
<div class="mosaic">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" class="logo" alt="${h(item.name)}" />
|
||||
<div class="separator"></div>
|
||||
<h5>${h(item.flatName)}</h5>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
module.exports.renderBorderlessCard = function renderBorderlessCard({item}) {
|
||||
return `
|
||||
<div data-id="${item.id}" class="mosaic-wrap">
|
||||
<div class="mosaic">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" class="logo" alt="${h(item.name)}" />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
const getContrastRatio = require('get-contrast-ratio').default;
|
||||
|
||||
const { h } = require('../utils/format');
|
||||
const { renderGuideLink } = require('./GuideLink');
|
||||
const { categoryTitleHeight } = require('../utils/landscapeCalculations');
|
||||
|
||||
module.exports.renderCategoryHeader = function renderCategoryHeader({ href, label, guideAnchor, background, rotate = false }) {
|
||||
const lowContrast = getContrastRatio('#ffffff', background) < 4.5
|
||||
const color = lowContrast ? '#282828' : '#ffffff'
|
||||
const backgroundColor = lowContrast ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'
|
||||
|
||||
return `
|
||||
<a data-type="internal"
|
||||
href="${href}"
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: ${color};
|
||||
background: none
|
||||
"
|
||||
>${h(label)}</a>
|
||||
${ guideAnchor ? renderGuideLink({label, anchor: guideAnchor, style: `
|
||||
width: ${categoryTitleHeight - 4}px;
|
||||
height: ${categoryTitleHeight - 4}px;
|
||||
margin: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
color: ${color};
|
||||
background: ${backgroundColor};
|
||||
transform: ${rotate ? 'rotate(180deg)' : 'none' };
|
||||
`}) : '' }
|
||||
`;
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
const CheckboxSelector = ({value, options, onChange}) => {
|
||||
const valueOf = function(checkbox) {
|
||||
return value.indexOf(checkbox) !== -1;
|
||||
};
|
||||
const handleCheckboxChange = function(checkbox, checked) {
|
||||
if (checked) {
|
||||
onChange(value.concat([checkbox]));
|
||||
} else {
|
||||
onChange(value.filter(function(x) { return x !== checkbox; }))
|
||||
}
|
||||
};
|
||||
|
||||
return <FormGroup>
|
||||
{ options.map( (el) => (
|
||||
<FormControlLabel key={el.id} control={
|
||||
<Checkbox onClick={function() {
|
||||
handleCheckboxChange(el.id, !valueOf(el.id));
|
||||
}}
|
||||
checked={valueOf(el.id)}
|
||||
/>
|
||||
} label={el.label}
|
||||
/>
|
||||
)) }
|
||||
</FormGroup>
|
||||
};
|
||||
export default pure(CheckboxSelector);
|
|
@ -1,40 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
|
||||
|
||||
const idToValue = (id) => id !== null ? id : 'any';
|
||||
const valueToId = (value) => value === 'any' ? null : value;
|
||||
|
||||
const ComboboxSelector = ({value, options, onChange}) => {
|
||||
const renderValue = function(selected) {
|
||||
if (selected.length === 0) {
|
||||
return 'Any';
|
||||
}
|
||||
return selected.join(', ');
|
||||
}
|
||||
|
||||
return <Select
|
||||
multiple
|
||||
style={{width:175, fontSize:'0.8em'}}
|
||||
value={idToValue(value)}
|
||||
onChange={(e) => onChange(valueToId(e.target.value))}
|
||||
renderValue={renderValue }
|
||||
displayEmpty
|
||||
>
|
||||
{ options.map( (el) => (
|
||||
<MenuItem key={idToValue(el.id)}
|
||||
value={idToValue(el.id)}
|
||||
style={{height:5}}
|
||||
>
|
||||
<Checkbox color="primary" disableRipple checked={value.indexOf(el.id) !== -1} />
|
||||
|
||||
<ListItemText disableTypography style={{fontSize:'0.8em'}} primary={el.label}/>
|
||||
</MenuItem>
|
||||
)) }
|
||||
</Select>
|
||||
};
|
||||
export default pure(ComboboxSelector);
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
|
||||
|
||||
const idToValue = (id) => id !== null ? id : 'any';
|
||||
const valueToId = (value) => value === 'any' ? null : value;
|
||||
|
||||
const ComboboxSelector = ({value, options, onChange}) => {
|
||||
|
||||
return <Select
|
||||
style={{width:175, fontSize:'0.8em'}}
|
||||
value={idToValue(value)}
|
||||
onChange={(e) => onChange(valueToId(e.target.value))}
|
||||
>
|
||||
{ options.map( (el) => (
|
||||
<MenuItem key={idToValue(el.id)}
|
||||
value={idToValue(el.id)}
|
||||
style={{height:5, fontSize:'0.8em'}}>{el.label}</MenuItem>
|
||||
)) }
|
||||
</Select>
|
||||
};
|
||||
export default pure(ComboboxSelector);
|
|
@ -1,96 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { connect } from "react-redux";
|
||||
import { isZoomedIn } from "../utils/browserZoom";
|
||||
|
||||
class AutoSizer extends React.PureComponent {
|
||||
state = {
|
||||
height: this.props.defaultHeight || 0
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (
|
||||
this._autoSizer &&
|
||||
this._autoSizer.parentNode &&
|
||||
this._autoSizer.parentNode.ownerDocument &&
|
||||
this._autoSizer.parentNode.ownerDocument.defaultView &&
|
||||
this._autoSizer.parentNode instanceof
|
||||
this._autoSizer.parentNode.ownerDocument.defaultView.HTMLElement
|
||||
) {
|
||||
// Delay access of parentNode until mount.
|
||||
// This handles edge-cases where the component has already been unmounted before its ref has been set,
|
||||
// As well as libraries like react-lite which have a slightly different lifecycle.
|
||||
this._parentNode = this._autoSizer.parentNode;
|
||||
|
||||
// Defer requiring resize handler in order to support server-side rendering.
|
||||
// See issue #41
|
||||
this._onResize();
|
||||
window.addEventListener("resize", this._onResize);
|
||||
window.addEventListener("touchend", this.checkedZoomedIn);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("resize", this._onResize);
|
||||
window.removeEventListener("touchend", this.checkedZoomedIn);
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.location !== prevProps.location) {
|
||||
this._onResize();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { height } = this.state;
|
||||
|
||||
// Outer div should not force width/height since that may prevent containers from shrinking.
|
||||
// Inner component should overflow and use calculated width/height.
|
||||
// See issue #68 for more information.
|
||||
const childParams = { height };
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this._setRef}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
width: '100%',
|
||||
}}>
|
||||
{children(childParams)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
checkedZoomedIn = (e) => {
|
||||
const windowHeight = window.innerHeight;
|
||||
this._onResize();
|
||||
|
||||
for (let i = 1; i < 11; i++) {
|
||||
const timeout = i * 50;
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.innerHeight !== windowHeight) {
|
||||
this._onResize();
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
_onResize = () => {
|
||||
if (this._parentNode) {
|
||||
const height = this._autoSizer.clientHeight + window.innerHeight - document.body.offsetHeight;
|
||||
const zoomedIn = isZoomedIn();
|
||||
this.setState({ height: zoomedIn ? 'auto' : height, zoomedIn });
|
||||
}
|
||||
};
|
||||
|
||||
_setRef = (autoSizer) => {
|
||||
this._autoSizer = autoSizer;
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ router }) => ({
|
||||
location: router.location
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(AutoSizer);
|
|
@ -1,35 +0,0 @@
|
|||
import { Component } from 'react';
|
||||
|
||||
export default class DelayRender extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { ready: true };
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const delay = this.props.delay || 1000;
|
||||
const d = parseInt(delay, 10);
|
||||
if (d && d > 0) {
|
||||
this.setState({ ready: false });
|
||||
this.timeout = setTimeout(() => {
|
||||
this.setState({ ready: true });
|
||||
}, delay);
|
||||
} else {
|
||||
this.setState({ ready: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.ready) {
|
||||
const content = this.props.content();
|
||||
return content;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
const _ = require('lodash');
|
||||
// Render only for an export
|
||||
const { saneName } = require('../utils/saneName');
|
||||
const { h } = require('../utils/format');
|
||||
const { getGroupedItems, expandSecondPathItems } = require('../utils/itemsCalculator');
|
||||
const { parseParams } = require('../utils/routing');
|
||||
const { renderDefaultCard, renderBorderlessCard, renderFlatCard } = require('./CardRenderer');
|
||||
const icons = require('../utils/icons');
|
||||
|
||||
module.exports.render = function({items, exportUrl}) {
|
||||
const params = parseParams(exportUrl.split('?').slice(-1)[0]);
|
||||
if (params.grouping === 'landscape') {
|
||||
items = expandSecondPathItems(items);
|
||||
}
|
||||
const groupedItems = getGroupedItems({data: items, ...params})
|
||||
const cardStyle = params.cardStyle || 'default';
|
||||
const cardFn = cardStyle === 'borderless' ? renderBorderlessCard : cardStyle === 'flat' ? renderFlatCard : renderDefaultCard;
|
||||
const linkUrl = exportUrl.replace('&embed=yes', '').replace('embed=yes', '')
|
||||
|
||||
const result = `
|
||||
<div class="modal" style="display: none;">
|
||||
<div class="modal-shadow"></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-body">
|
||||
<div class="modal-buttons">
|
||||
<a class="modal-close">x</a>
|
||||
<span class="modal-prev">${icons.prev}</span>
|
||||
<span class="modal-next">${icons.next}</span>
|
||||
</div>
|
||||
<div class="modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="home" class="app ${cardStyle}-mode">
|
||||
<div class="app-overlay"></div>
|
||||
<div class="main-parent">
|
||||
<div class="app-overlay"></div>
|
||||
<div class="main">
|
||||
<div class="cards-section">
|
||||
<div class="column-content" >
|
||||
${ groupedItems.map( (groupedItem) => {
|
||||
const uniqItems = _.uniqBy(groupedItem.items, (x) => x.name + x.logo);
|
||||
const cardElements = uniqItems.map( (item) => cardFn({item}));
|
||||
const header = items.length > 0 ? `
|
||||
<div class="sh_wrapper" data-wrapper-id="${h(saneName(groupedItem.header))}">
|
||||
<div style="font-size: 24px; padding-left: 16px; line-height: 48px; font-weight: 500;">
|
||||
<span>${h(groupedItem.header)}</span>
|
||||
<span class="items-cont"> (${uniqItems.length})</span>
|
||||
</div>
|
||||
</div>` : '';
|
||||
return [ header, `<div data-section-id="${h(saneName(groupedItem.header))}">${cardElements.join('')}</div>`].join('');
|
||||
}) }
|
||||
</div>
|
||||
</div>
|
||||
<div id="embedded-footer">
|
||||
<h1 style="margin-top: 20px; width: 100%; text-align: center;">
|
||||
<a data-type="external" target="_blank" href="${linkUrl}">View</a> the full interactive landscape
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return result;
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import OutboundLink from './OutboundLink';
|
||||
|
||||
const EmbeddedFooter = () => {
|
||||
const originalLink = window.location.pathname.replace('&embed=yes', '').replace('&embed=true', '');
|
||||
return <h1 style={{ marginTop: 20, width: '100%', textAlign: 'center' }}>
|
||||
<OutboundLink to={originalLink}>View</OutboundLink> the full interactive landscape
|
||||
</h1>
|
||||
}
|
||||
export default pure(EmbeddedFooter);
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import SystemUpdateIcon from '@material-ui/icons/SystemUpdate';
|
||||
|
||||
const ExportCsv = ({onExport}) => {
|
||||
return (
|
||||
<div className="filters-action" onClick={()=>onExport()} aria-label="Download as CSV">
|
||||
<SystemUpdateIcon /><span>Download as CSV</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default pure(ExportCsv);
|
|
@ -1,12 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ExportCsv from './ExportCsv';
|
||||
import { exportCsv } from '../reducers/mainReducer.js';
|
||||
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
onExport: exportCsv
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ExportCsv);
|
|
@ -1,54 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
|
||||
import ProjectFilterContainer from './ProjectFilterContainer';
|
||||
import LicenseFilterContainer from './LicenseFilterContainer';
|
||||
import OrganizationFilterContainer from './OrganizationFilterContainer';
|
||||
import HeadquartersFilterContainer from './HeadquartersFilterContainer';
|
||||
import LandscapeFilterContainer from './LandscapeFilterContainer';
|
||||
import fields from '../types/fields';
|
||||
const Filters = () => {
|
||||
return <div>
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.landscape.label}</FormLabel>
|
||||
<LandscapeFilterContainer/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.relation.label}</FormLabel>
|
||||
<ProjectFilterContainer/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.license.label}</FormLabel>
|
||||
<LicenseFilterContainer />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.organization.label}</FormLabel>
|
||||
<OrganizationFilterContainer />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{fields.headquarters.label}</FormLabel>
|
||||
<HeadquartersFilterContainer />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
|
||||
</div>;
|
||||
}
|
||||
export default pure(Filters);
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import settings from 'project/settings.yml'
|
||||
|
||||
const Footer = () => {
|
||||
return <div style={{ marginTop: 10, fontSize:'9pt', width: '100%', textAlign: 'center' }}>
|
||||
{settings.home.footer} For more information, please see the
|
||||
<OutboundLink eventLabel="crunchbase-terms" to={`https://github.com/${settings.global.repo}/blob/master/README.md#license`}>
|
||||
license
|
||||
</OutboundLink> info.
|
||||
</div>
|
||||
}
|
||||
export default pure(Footer);
|
|
@ -0,0 +1,61 @@
|
|||
const { calculateSize } = require("../utils/landscapeCalculations");
|
||||
const headerHeight = 40;
|
||||
module.exports.render = function({landscapeSettings, landscapeContent, version}) {
|
||||
const { fullscreenWidth, fullscreenHeight } = calculateSize(landscapeSettings);
|
||||
return `
|
||||
<div class="gradient-bg" style="
|
||||
width: ${fullscreenWidth}px;
|
||||
height: ${fullscreenHeight}px;
|
||||
overflow: hidden;
|
||||
"><div class="inner-landscape" style="
|
||||
width: ${fullscreenWidth}px;
|
||||
height: ${fullscreenHeight}px;
|
||||
padding-top: ${headerHeight + 20}px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
">
|
||||
${landscapeContent }
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 18px,
|
||||
background: rgb(64,89,163);
|
||||
color: white;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-radius: 5px;
|
||||
">${landscapeSettings.fullscreen_header}</div>
|
||||
${ !landscapeSettings.fullscreen_hide_grey_logos ? `<div style="
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 12px;
|
||||
font-size: 11px;
|
||||
background: #eee;
|
||||
color: rgb(100,100,100);
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
border-radius: 5px;
|
||||
">Greyed logos are not open source</div>` : '' }
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 15px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
">${landscapeSettings.title} </div>
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 15px;
|
||||
font-size: 12px;
|
||||
color: #eee;
|
||||
">${version}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
import GroupingTypeContainer from './GroupingTypeContainer';
|
||||
const Grouping = () => {
|
||||
return <FormGroup row>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Grouping</FormLabel>
|
||||
<GroupingTypeContainer />
|
||||
</FormControl>
|
||||
</FormGroup>;
|
||||
};
|
||||
export default pure(Grouping);
|
|
@ -1,25 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import ComboboxSelector from './ComboboxSelector';
|
||||
|
||||
|
||||
const GroupingSelector = ({isBigPicture, value, options, onChange}) => {
|
||||
if (!isBigPicture) {
|
||||
return <ComboboxSelector value={value} options={options} onChange={onChange} />;
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
disabled
|
||||
value="empty"
|
||||
style={{width:175 ,fontSize:'0.8em'}}
|
||||
>
|
||||
<MenuItem value="empty">
|
||||
<em>N/A</em>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default pure(GroupingSelector);
|
|
@ -1,30 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import GroupingSelector from './GroupingSelector';
|
||||
import { changeGrouping } from '../reducers/mainReducer.js';
|
||||
import fields from '../types/fields';
|
||||
|
||||
const groupingFields = ['landscape', 'relation', 'license', 'organization', 'headquarters'];
|
||||
const options = [{
|
||||
id: 'no',
|
||||
label: 'No Grouping',
|
||||
url: 'no'
|
||||
}].concat(groupingFields.map(function(x) {
|
||||
return {
|
||||
id: x,
|
||||
label: fields[x].groupingLabel
|
||||
};
|
||||
}));
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isBigPicture: state.main.mainContentMode !== 'card',
|
||||
value: state.main.grouping,
|
||||
options: options
|
||||
});
|
||||
const onChange = function(newValue) {
|
||||
return changeGrouping(newValue);
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
onChange: onChange
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GroupingSelector);
|
|
@ -0,0 +1,10 @@
|
|||
const { h } = require('../utils/format');
|
||||
const { guideLink } = require('../utils/icons');
|
||||
module.exports.renderGuideLink = function({anchor, label, style }) {
|
||||
const ariaLabel = `Read more about ${label} on the guide`
|
||||
const to = h(`guide#${anchor}`);
|
||||
|
||||
return `<a data-type="external" target="_blank" style="${style}" aria-label="${h(ariaLabel)}" href="${to}">
|
||||
${guideLink}
|
||||
</a>`;
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
const { sizeFn } = require('../utils/landscapeCalculations');
|
||||
const { renderItem } = require('./Item.js');
|
||||
const { h } = require('../utils/format');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
const icons = require('../utils/icons');
|
||||
|
||||
|
||||
|
||||
// guide is a guide index
|
||||
module.exports.render = function({settings, items, guide}) {
|
||||
const currentBranch = require('child_process').execSync(`git rev-parse --abbrev-ref HEAD`, {
|
||||
cwd: require('../../tools/settings').projectPath
|
||||
}).toString().trim();
|
||||
|
||||
|
||||
const title = `<h1 className="title" style="margin-top: -5px;">${h(settings.global.short_name)} Landscape Guide</h1>`;
|
||||
const renderSubcategoryMetadata = ({ node, entries }) => {
|
||||
const orderedEntries = _.orderBy(entries, (x) => -x.size);
|
||||
const projectEntries = entries.filter(entry => entry.project)
|
||||
return `
|
||||
${ (node.buzzwords.length > 0 || projectEntries.length > 0) ? `<div class="metadata">
|
||||
<div class="header">
|
||||
<div>Buzzwords</div>
|
||||
<div>${h(settings.global.short_name)} Projects</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div>
|
||||
<ul>
|
||||
${ node.buzzwords.map(str => `<li>${h(str)}</li>`).join('') }
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
${ projectEntries.map(entry => `<li>${h(entry.name)} (${h(entry.project)})</li>`).join('') }
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div> ` : '' }
|
||||
|
||||
<div class="items">
|
||||
${ orderedEntries.map(entry => renderItem(entry)).join('') }
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderNavigation = ({ nodes }) => {
|
||||
const links = nodes.filter(node => node.anchor)
|
||||
const parents = links
|
||||
.map(n => n.anchor.split('--')[0])
|
||||
.reduce((acc, n) => ({ ...acc, [n]: (acc[n] || 0) + 1}), {})
|
||||
|
||||
return links
|
||||
.filter(({ title }) => {
|
||||
return title
|
||||
})
|
||||
.map(node => {
|
||||
const hasChildren = (parents[node.anchor] || 0) > 1
|
||||
return `
|
||||
<a href="#${node.anchor}" data-level="${node.level}" class="sidebar-link expandable" style="padding-left: ${10 + node.level * 10}px;">
|
||||
${h(node.title)} ${hasChildren ? icons.expand : ''}
|
||||
</a>
|
||||
${hasChildren ? `
|
||||
<a href="#${node.anchor}" data-level=${node.level + 1} class="sidebar-link" style="padding-left: 30px;"> Overview </a>
|
||||
` : ''}
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
const renderLandscapeLink = ({ landscapeKey, title }) => {
|
||||
const href = `card-mode?category=${landscapeKey}`
|
||||
return `<a href="${href}" target="_blank" class="permalink">${icons.guide} ${h(title)} </a>`;
|
||||
}
|
||||
|
||||
const renderContent = ({ nodes, enhancedEntries }) => {
|
||||
return nodes.map((node) => {
|
||||
const subcategoryEntries = node.subcategory && enhancedEntries.filter(entry => entry.path.split(' / ')[1].trim() === node.title) || [];
|
||||
return `<div>
|
||||
${ node.title ? `<div class="section-title" id="${h(node.anchor)}">
|
||||
<h2 data-variant="${node.level + 1}">
|
||||
${ node.landscapeKey
|
||||
? renderLandscapeLink({landscapeKey: node.landscapeKey, title: node.title})
|
||||
: h(node.title)
|
||||
}
|
||||
</h2>
|
||||
</div>
|
||||
` : ''}
|
||||
${ node.content ? `<div class="guide-content">${node.content}</div>` : ''}
|
||||
${ node.subcategory ? renderSubcategoryMetadata({entries: subcategoryEntries,node:node}) : '' }
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const enhancedEntries = items.map( (entry) => {
|
||||
let subcategory = entry.path.split(' / ')[1];
|
||||
let categoryAttrs = null;
|
||||
for (let key in settings.big_picture) {
|
||||
let page = settings.big_picture[key];
|
||||
for (let element of page.elements) {
|
||||
if (!page.category && element.category === entry.category) {
|
||||
categoryAttrs = element;
|
||||
}
|
||||
if (page.category === entry.category && element.category === subcategory) {
|
||||
categoryAttrs = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!categoryAttrs) {
|
||||
return null;
|
||||
}
|
||||
const enhanced = { ...entry, categoryAttrs }
|
||||
return { ...enhanced, size: sizeFn(enhanced) }
|
||||
}).filter( (x) => !!x);
|
||||
|
||||
return `
|
||||
<div class="links">
|
||||
<div>
|
||||
<a href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}/edit/${currentBranch}/guide.md" target="_blank">
|
||||
${icons.edit}
|
||||
Edit this page</a>
|
||||
</div>
|
||||
<div style="height: 5px;"></div>
|
||||
<div>
|
||||
<a href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}/issues/new?title=Guide Issue" target="_blank">
|
||||
${icons.github}
|
||||
Report issue</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-content">
|
||||
<span class="landscape-logo">
|
||||
<a aria-label="reset filters" class="nav-link" href="/">
|
||||
<img alt="landscape logo" src="${assetPath("images/left-logo.svg")} ">
|
||||
</a>
|
||||
</span>
|
||||
<div class="guide-sidebar">
|
||||
<div class="sidebar-collapse">+</div>
|
||||
<div class="guide-toggle">
|
||||
<span class="toggle-item "><a href="./">Landscape</a></span>
|
||||
<span class="toggle-item active">Guide</span>
|
||||
</div>
|
||||
${renderNavigation({nodes: guide})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-header">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<button class="sidebar-show" role="none" aria-label="show sidebar">${icons.sidebar}</button>
|
||||
<span class="landscape-logo">
|
||||
<a aria-label="reset filters" class="nav-link" href="/">
|
||||
<img alt="landscape logo" src="${assetPath("/images/left-logo.svg")}">
|
||||
</a>
|
||||
</span>
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
<a rel="noopener noreferrer noopener noreferrer"
|
||||
class="landscapeapp-logo"
|
||||
title="${h(settings.global.short_name)}"
|
||||
target="_blank"
|
||||
href="${settings.global.company_url}">
|
||||
<img src="${assetPath("images/right-logo.svg")}" title="${settings.global.short_name}">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
${renderContent({nodes: guide,enhancedEntries: enhancedEntries})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { withRouter } from "react-router";
|
||||
import queryString from 'query-string';
|
||||
import { landscapeSettingsList } from "../utils/landscapeSettings";
|
||||
|
||||
const additionalLandscapes = landscapeSettingsList.slice(1).map(({ url }) => url);
|
||||
const allowedFormats = ["card-mode", ...additionalLandscapes];
|
||||
|
||||
const isCanonical = (pathname) => {
|
||||
const params = queryString.parse(pathname.split("/").pop());
|
||||
const paramsCount = Object.keys(params).length;
|
||||
|
||||
return paramsCount === 0 ||
|
||||
(paramsCount === 1 && allowedFormats.includes(params.format)) ||
|
||||
(paramsCount === 1 && "selected" in params) ||
|
||||
(paramsCount === 2 && "selected" in params && allowedFormats.includes(params.format));
|
||||
}
|
||||
|
||||
const Head = ({ location }) => {
|
||||
const indexTag = isCanonical(location.pathname) ?
|
||||
<link rel="canonical" href={window.location.href}/> :
|
||||
<meta name="robots" content="noindex"/>;
|
||||
|
||||
return ReactDOM.createPortal(indexTag, document.head);
|
||||
};
|
||||
|
||||
export default withRouter(Head);
|
|
@ -1,21 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import settings from 'project/settings.yml';
|
||||
|
||||
const Header = ({ reset }) => {
|
||||
const { short_name, company_url, name } = settings.global;
|
||||
return (
|
||||
<div className="header_container">
|
||||
<div className="header">
|
||||
<span className="landscape-logo"><img onClick={reset} src="./images/left-logo.svg" alt={name}/></span>
|
||||
<OutboundLink eventLabel={short_name} to={company_url} className="landscapeapp-logo" title={`${short_name} Home`}>
|
||||
<img src="./images/right-logo.svg" title={`${short_name} Logo`}/>
|
||||
</OutboundLink>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default pure(Header);
|
|
@ -1,13 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Header from './Header';
|
||||
import { resetParameters } from '../reducers/mainReducer.js';
|
||||
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
test: 1
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
reset: resetParameters
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Header);
|
|
@ -1,17 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import TreeSelector from './TreeSelector';
|
||||
import { changeFilter } from '../reducers/mainReducer.js';
|
||||
import { options } from '../types/fields';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
value: state.main.filters.headquarters,
|
||||
options: options('headquarters')
|
||||
});
|
||||
const onChange = function(newValue) {
|
||||
return changeFilter('headquarters', newValue);
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
onChange: onChange
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TreeSelector);
|
|
@ -1,214 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import classNames from 'classnames'
|
||||
import Filters from './Filters';
|
||||
import Grouping from './Grouping';
|
||||
import Sorting from './Sorting';
|
||||
import PresetsContainer from './PresetsContainer';
|
||||
import Ad from './Ad';
|
||||
import AutoSizer from './CustomAutoSizer';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import {
|
||||
LandscapeContentContainer,
|
||||
SwitchButtonContainer,
|
||||
ZoomButtonsContainer,
|
||||
FullscreenButtonContainer
|
||||
} from './BigPicture';
|
||||
import TweetButton from './TweetButton';
|
||||
import MainContentContainer from './MainContentContainer';
|
||||
import HomePageUrlContainer from './HomePageUrlContainer';
|
||||
import HomePageScrollerContainer from './HomePageScrollerContainer';
|
||||
import ResetFiltersContainer from './ResetFiltersContainer';
|
||||
import ItemDialogContainer from './ItemDialogContainer';
|
||||
import HeaderContainer from './HeaderContainer';
|
||||
import SummaryContainer from './SummaryContainer';
|
||||
import ExportCsvContainer from './ExportCsvContainer';
|
||||
import Footer from './Footer';
|
||||
import EmbeddedFooter from './EmbeddedFooter';
|
||||
|
||||
import isIphone from '../utils/isIphone';
|
||||
import isGoogle from '../utils/isGoogle';
|
||||
import bus from '../reducers/bus';
|
||||
import settings from 'project/settings.yml'
|
||||
|
||||
const state = {
|
||||
lastScrollPosition: 0
|
||||
};
|
||||
|
||||
bus.on('scrollToTop', function() {
|
||||
(document.scrollingElement || document.body).scrollTop = 0;
|
||||
});
|
||||
|
||||
function preventDefault(e){
|
||||
const modal = e.srcElement.closest('.modal-body');
|
||||
if (!modal) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function disableScroll(){
|
||||
const shadow = document.querySelector('.MuiBackdrop-root');
|
||||
if (shadow) {
|
||||
shadow.addEventListener('touchmove', preventDefault, { passive: false });
|
||||
}
|
||||
document.body.addEventListener('touchmove', preventDefault, { passive: false });
|
||||
}
|
||||
|
||||
function enableScroll(){
|
||||
const shadow = document.querySelector('.MuiBackdrop-root');
|
||||
if (shadow) {
|
||||
shadow.removeEventListener('touchmove', preventDefault);
|
||||
}
|
||||
document.body.removeEventListener('touchmove', preventDefault);
|
||||
}
|
||||
|
||||
const HomePage = ({isEmbed, mainContentMode, ready, hasSelectedItem, filtersVisible, hideFilters, showFilters, onClose, title, isFullscreen}) => {
|
||||
const isBigPicture = mainContentMode !== 'card';
|
||||
if (!ready) {
|
||||
return (
|
||||
<div>
|
||||
<HomePageUrlContainer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
document.title = title;
|
||||
if (isGoogle && hasSelectedItem) {
|
||||
return <ItemDialogContainer />;
|
||||
}
|
||||
|
||||
if (isBigPicture) {
|
||||
document.querySelector('html').classList.add('big-picture');
|
||||
} else {
|
||||
document.querySelector('html').classList.remove('big-picture');
|
||||
}
|
||||
|
||||
if (isFullscreen) {
|
||||
document.querySelector('html').classList.add('fullscreen');
|
||||
} else {
|
||||
document.querySelector('html').classList.remove('fullscreen');
|
||||
}
|
||||
|
||||
if (isIphone) {
|
||||
if (hasSelectedItem) {
|
||||
if (!document.querySelector('.iphone-scroller')) {
|
||||
state.lastScrollPosition = (document.scrollingElement || document.body).scrollTop;
|
||||
}
|
||||
document.querySelector('html').classList.add('has-selected-item');
|
||||
(document.scrollingElement || document.body).scrollTop = 0;
|
||||
disableScroll();
|
||||
} else {
|
||||
document.querySelector('html').classList.remove('has-selected-item');
|
||||
if (document.querySelector('.iphone-scroller')) {
|
||||
(document.scrollingElement || document.body).scrollTop = state.lastScrollPosition;
|
||||
}
|
||||
enableScroll();
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmbed) {
|
||||
if (window.parentIFrame) {
|
||||
if (hasSelectedItem) {
|
||||
window.parentIFrame.sendMessage({type: 'showModal'})
|
||||
} else {
|
||||
window.parentIFrame.sendMessage({type: 'hideModal'})
|
||||
}
|
||||
if (hasSelectedItem) {
|
||||
window.parentIFrame.getPageInfo(function(info) {
|
||||
var offset = info.scrollTop - info.offsetTop;
|
||||
var height = info.iframeHeight - info.clientHeight;
|
||||
var maxHeight = info.clientHeight * 0.9;
|
||||
if (maxHeight > 480) {
|
||||
maxHeight = 480;
|
||||
}
|
||||
var t = function(x1, y1, x2, y2, x3) {
|
||||
if (x3 < x1 - 50) {
|
||||
x3 = x1 - 50;
|
||||
}
|
||||
if (x3 > x2 + 50) {
|
||||
x3 = x2 + 50;
|
||||
}
|
||||
return y1 + (x3 - x1) / (x2 - x1) * (y2 - y1);
|
||||
}
|
||||
var top = t(0, -height, height, height, offset);
|
||||
if (top < 0 && info.iframeHeight <= 600) {
|
||||
top = 10;
|
||||
}
|
||||
setTimeout(function() {
|
||||
const modal = document.querySelector('.modal-body');
|
||||
if (modal) {
|
||||
modal.style.top = top + 'px';
|
||||
modal.style.maxHeight = maxHeight + 'px';
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
}
|
||||
document.querySelector('body').classList.add('embed');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HomePageScrollerContainer/>
|
||||
<ItemDialogContainer/>
|
||||
<div className={classNames('app',{'filters-opened' : filtersVisible})}>
|
||||
<div />
|
||||
<div style={{marginTop: (isIphone && hasSelectedItem) ? -state.lastScrollPosition : 0}} className={classNames({"iphone-scroller": isIphone && hasSelectedItem}, 'main-parent')} >
|
||||
{ !isEmbed && !isFullscreen && <HeaderContainer/> }
|
||||
{ !isEmbed && !isFullscreen && <IconButton className="sidebar-show" title="Show sidebar" onClick={showFilters}><MenuIcon /></IconButton> }
|
||||
{ !isEmbed && !isFullscreen && <div className="sidebar">
|
||||
<div className="sidebar-scroll">
|
||||
<IconButton className="sidebar-collapse" title="Hide sidebar" onClick={hideFilters}><CloseIcon /></IconButton>
|
||||
<ResetFiltersContainer />
|
||||
<Grouping/>
|
||||
<Sorting/>
|
||||
<Filters />
|
||||
<PresetsContainer />
|
||||
<ExportCsvContainer />
|
||||
<Ad />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="app-overlay" onClick={hideFilters}></div>
|
||||
|
||||
<HomePageUrlContainer />
|
||||
|
||||
<div className={classNames('main', {'embed': isEmbed})}>
|
||||
{ !isEmbed && <div className="disclaimer">
|
||||
<span dangerouslySetInnerHTML={{__html: settings.home.header}} />
|
||||
Please <OutboundLink to={`https://github.com/${settings.global.repo}`}>open</OutboundLink> a pull request to
|
||||
correct any issues. Greyed logos are not open source. Last Updated: {window.lastUpdated}
|
||||
</div> }
|
||||
{ !isEmbed && <SummaryContainer /> }
|
||||
|
||||
<div className="cards-section">
|
||||
<SwitchButtonContainer />
|
||||
<div className="right-buttons">
|
||||
<ZoomButtonsContainer/>
|
||||
<FullscreenButtonContainer/>
|
||||
<TweetButton cls="tweet-button-main"/>
|
||||
</div>
|
||||
{ isBigPicture &&
|
||||
<AutoSizer>
|
||||
{({ height }) => (
|
||||
<div className="landscape-wrapper" style={{height: height}}>
|
||||
<LandscapeContentContainer />
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
}
|
||||
{ !isBigPicture && <MainContentContainer/> }
|
||||
</div>
|
||||
{ !isEmbed && !isBigPicture && <Footer/> }
|
||||
{ isEmbed && <EmbeddedFooter/> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default pure(HomePage);
|
|
@ -1,38 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import createSelector from '../utils/createSelector';
|
||||
import HomePage from './HomePage';
|
||||
import {showFilters, hideFilters, closeDialog } from '../reducers/mainReducer';
|
||||
import isEmbed from '../utils/isEmbed';
|
||||
import getGroupedItems, {getGroupedItemsForBigPicture } from '../utils/itemsCalculator';
|
||||
import selectedItemCalculator from '../utils/selectedItemCalculator';
|
||||
import settings from 'project/settings.yml'
|
||||
|
||||
const defaultTitle = settings.global.meta.title;
|
||||
const getTitle = createSelector([state => state], function(state) {
|
||||
if (!state.main.ready) {
|
||||
return defaultTitle;
|
||||
}
|
||||
const groupedItems = getGroupedItems(state);
|
||||
const groupedItemsForBigPicture = getGroupedItemsForBigPicture(state);
|
||||
const selectedItemId = state.main.selectedItemId;
|
||||
const isBigPicture = state.main.mainContentMode !== 'card';
|
||||
const selectedItemInfo = selectedItemCalculator(groupedItems, groupedItemsForBigPicture, selectedItemId, isBigPicture);
|
||||
return selectedItemInfo.hasSelectedItem ? `${selectedItemInfo.itemInfo.name} - ${defaultTitle}` : defaultTitle;
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
ready: state.main.ready,
|
||||
filtersVisible: state.main.filtersVisible && !isEmbed,
|
||||
isEmbed: isEmbed,
|
||||
isFullscreen: state.main.isFullscreen && state.main.mainContentMode !== 'card',
|
||||
mainContentMode: state.main.mainContentMode,
|
||||
hasSelectedItem: !!state.main.selectedItemId,
|
||||
title: getTitle(state)
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
showFilters: showFilters,
|
||||
hideFilters: hideFilters,
|
||||
onClose: closeDialog
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
|
|
@ -0,0 +1,237 @@
|
|||
const _ = require('lodash');
|
||||
const { h } = require('../utils/format');
|
||||
const { fields, sortOptions, options } = require('../types/fields');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
const icons = require('../utils/icons');
|
||||
|
||||
const renderSingleSelect = ({name, options, title}) => (
|
||||
`
|
||||
<div class="select" data-type="single" data-name="${name}" data-options="${h(JSON.stringify(options))}">
|
||||
<select class="select-text" required>
|
||||
<option value="1" selected>Value</option>
|
||||
</select>
|
||||
<span class="select-highlight"></span>
|
||||
<span class="select-bar"></span>
|
||||
<label class="select-label">${h(title)}</label>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
const renderMultiSelect = ({name, options, title}) => (
|
||||
`
|
||||
<div class="select" data-type="multi" data-name="${name}" data-options="${h(JSON.stringify(options))}">
|
||||
<select class="select-text" required>
|
||||
<option value="1" selected>Value</option>
|
||||
</select>
|
||||
<span class="select-highlight"></span>
|
||||
<span class="select-bar"></span>
|
||||
<label class="select-label">${h(title)}</label>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
|
||||
const renderGroupingSelect = function() {
|
||||
const groupingFields = ['landscape', 'relation', 'license', 'organization', 'headquarters'];
|
||||
const options = [{
|
||||
id: 'no',
|
||||
label: 'No Grouping',
|
||||
}].concat(groupingFields.map(id => ({ id: fields[id].url, label: (fields[id].groupingLabel) })))
|
||||
return renderSingleSelect({name: "grouping", options, title: "Grouping" });
|
||||
}
|
||||
|
||||
const renderSortBySelect = function() {
|
||||
const options = sortOptions.filter( (x) => !x.disabled).map( (x) => ({
|
||||
id: (fields[x.id] || { url: x.id}).url || x.id, label: x.label
|
||||
}))
|
||||
return renderSingleSelect({name: "sort", options, title: "Sort By" });
|
||||
}
|
||||
|
||||
const renderFilterCategory = function() {
|
||||
return renderMultiSelect({name:"category", options: options('landscape'), title: 'Category'});
|
||||
}
|
||||
|
||||
const renderFilterProject = function() {
|
||||
return renderMultiSelect({name:"project", options: options('relation'), title: 'Project'});
|
||||
}
|
||||
|
||||
const renderFilterLicense = function() {
|
||||
return renderMultiSelect({name:"license", options: options('license'), title: "License"});
|
||||
}
|
||||
|
||||
const renderFilterOrganization = function() {
|
||||
return renderMultiSelect({name: "organization", options: options('organization'), title: "Organization"});
|
||||
}
|
||||
|
||||
const renderFilterHeadquarters = function() {
|
||||
return renderMultiSelect({name: "headquarters", options: options('headquarters'), title: "Headquarters"});
|
||||
}
|
||||
|
||||
const renderFilterCompanyType = function() {
|
||||
return renderMultiSelect({name: "company-type", options: options('companyType'), title: "Company Type"});
|
||||
}
|
||||
|
||||
const renderFilterIndustries = function() {
|
||||
return renderMultiSelect({name: "industries", options: options('industries'), title: "Industry"});
|
||||
}
|
||||
|
||||
module.exports.render = function({settings, guidePayload, hasGuide, bigPictureKey}) {
|
||||
const mainCard = [{shortTitle: 'Card', title: 'Card Mode', mode: 'card', url: 'card-mode', tabIndex: 0}]
|
||||
const landscapes = Object.values(settings.big_picture).map(function(section) {
|
||||
return {
|
||||
url: section.url,
|
||||
title: section.name,
|
||||
shortTitle: section.short_name,
|
||||
mode: section.url === settings.big_picture.main.url ? 'main' : section.url,
|
||||
tabIndex: section.tab_index
|
||||
}
|
||||
})
|
||||
const tabs = _.orderBy(mainCard.concat(landscapes), 'tabIndex').map( item => _.pick(item, ['title', 'mode', 'shortTitle', 'url']))
|
||||
|
||||
|
||||
return `
|
||||
<div class="select-popup" style="display: none;">
|
||||
<div class="select-popup-body" ></div>
|
||||
</div>
|
||||
<div class="modal" style="display: none;">
|
||||
<div class="modal-shadow" ></div>
|
||||
<div class="modal-container">
|
||||
<div class="modal-body">
|
||||
<div class="modal-buttons">
|
||||
<a class="modal-close">x</a>
|
||||
<span class="modal-prev">${icons.prev}</span>
|
||||
<span class="modal-next">${icons.next}</span>
|
||||
</div>
|
||||
<div class="modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="guide-page" style="display: ${guidePayload ? "" : "none"};" data-loaded="${guidePayload ? "true" : ""}">
|
||||
${ !guidePayload ? `<div class="side-content">
|
||||
<span class="landscape-logo">
|
||||
<a aria-label="reset filters" class="nav-link" href="/">
|
||||
<img alt="landscape logo" src="${assetPath("images/left-logo.svg")}" />
|
||||
</a>
|
||||
</span>
|
||||
<div class="guide-sidebar">
|
||||
<div class="sidebar-collapse">X</div>
|
||||
<div class="guide-toggle">
|
||||
<span class="toggle-item "><a href="./">Landscape</a></span>
|
||||
<span class="toggle-item active">Guide</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>` : ''
|
||||
}
|
||||
${ guidePayload ? "$$guide$$" : ''}
|
||||
</div>
|
||||
<div id="home" style="display: ${guidePayload ? "none" : ""}" class="app">
|
||||
<div class="app-overlay"></div>
|
||||
<div class="main-parent">
|
||||
<button class="sidebar-show" role="none" aria-label="show sidebar">${icons.sidebar}</button>
|
||||
<div class="header_container">
|
||||
<div class="header">
|
||||
<span class="landscape-logo">
|
||||
<a aria-label="reset filters" class="nav-link" href="/">
|
||||
<img alt="landscape logo" src="${assetPath("images/left-logo.svg")}" />
|
||||
</a>
|
||||
</span>
|
||||
<a rel="noopener noreferrer noopener noreferrer"
|
||||
class="landscapeapp-logo"
|
||||
title="${h(settings.global.short_name)}"
|
||||
target="_blank"
|
||||
href="${settings.global.company_url}">
|
||||
<img src="${assetPath("/images/right-logo.svg")}" title="${h(settings.global.short_name)}" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-scroll">
|
||||
<div class="sidebar-collapse">+</div>
|
||||
${ hasGuide ? `
|
||||
<div class="guide-toggle">
|
||||
<span class="toggle-item active">Landscape</span>
|
||||
<span class="toggle-item "><a href="/guide">Guide</a></span>
|
||||
</div> ` : ''
|
||||
}
|
||||
<a class="filters-action reset-filters">${icons.reset}<span>Reset Filters</span>
|
||||
</a>
|
||||
${renderGroupingSelect()}
|
||||
${renderSortBySelect()}
|
||||
${renderFilterCategory()}
|
||||
${renderFilterProject()}
|
||||
${renderFilterLicense()}
|
||||
${renderFilterOrganization()}
|
||||
${renderFilterHeadquarters()}
|
||||
${renderFilterCompanyType()}
|
||||
${renderFilterIndustries()}
|
||||
|
||||
<div class="sidebar-presets">
|
||||
<h4>Example filters</h4>
|
||||
${ (settings.presets || []).map(preset => `
|
||||
<a data-type="internal" class="preset" href="${preset.url}">
|
||||
${h(preset.label)}
|
||||
</a> `
|
||||
).join('')}
|
||||
</div>
|
||||
${ (settings.ads || []).map( (entry) => `
|
||||
<a data-type="external" target="_blank" class="sidebar-event" href="${entry.url}" title="${h(entry.title)}">
|
||||
<img src="${assetPath(entry.image)}" alt="${entry.title}" />
|
||||
</a>
|
||||
`).join('') }
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-overlay"></div>
|
||||
|
||||
<div class="main">
|
||||
<div class="disclaimer">
|
||||
<span> ${settings.home.header} </span>
|
||||
Please <a data-type="external" target="_blank" href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}">open</a> a pull request to
|
||||
correct any issues. Greyed logos are not open source. Last Updated: ${process.env.lastUpdated}
|
||||
</div>
|
||||
<h4 class="summary"></h4>
|
||||
<div class="cards-section">
|
||||
<div class="big-picture-switch big-picture-switch-normal">
|
||||
${ tabs.map( (tab) => `
|
||||
<a href="${tab.url}" data-mode="${tab.mode}"><div>${h(tab.title)}</div></a>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="right-buttons">
|
||||
<div class="fullscreen-exit">${icons.fullscreenExit}</div>
|
||||
<div class="fullscreen-enter">${icons.fullscreenEnter}</div>
|
||||
<div class="zoom-out">${icons.zoomOut}</div>
|
||||
<div class="zoom-reset"></div>
|
||||
<div class="zoom-in">${icons.zoomIn}</div>
|
||||
</div>
|
||||
|
||||
${ tabs.filter( (x) => x.mode !== 'card').map( (tab) => `
|
||||
<div data-mode="${tab.mode}" class="landscape-flex">
|
||||
<div class="landscape-wrapper">
|
||||
<div class="inner-landscape" style="padding: 10px; display: none;">
|
||||
${ bigPictureKey === tab.mode ? '$$' + bigPictureKey + '$$' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
|
||||
<div class="column-content"></div>
|
||||
</div>
|
||||
<div id="footer" style="
|
||||
margin-top: 10px;
|
||||
font-size: 9pt;
|
||||
width: 100%;
|
||||
text-align: center;">
|
||||
${h(settings.home.footer)} For more information, please see the
|
||||
<a data-type="external" target="_blank" eventLabel="crunchbase-terms" href="${(settings.global.self_hosted_repo || false) ? "" : "https://github.com/"}${settings.global.repo}/blob/HEAD/README.md#license">
|
||||
license
|
||||
</a> info.
|
||||
</div>
|
||||
<div id="embedded-footer">
|
||||
<h1 style="margin-top: 20px; width: 100%; text-align: center;">
|
||||
<a data-type="external" target="_blank" href="url">View</a> the full interactive landscape
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { pure } from 'recompose';
|
||||
import { connect } from 'react-redux';
|
||||
import getGroupedItems from '../utils/itemsCalculator';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
groupedItems: getGroupedItems(state)
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
const Component = function({groupedItems}) {
|
||||
setTimeout(function() {
|
||||
(document.scrollingElement || document.body).scrollTop = 0;
|
||||
}, 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(pure(Component));
|
|
@ -1,32 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import createSelector from '../utils/createSelector';
|
||||
import { parseUrl } from '../utils/syncToUrl';
|
||||
import { changeParameters} from '../reducers/mainReducer';
|
||||
|
||||
const getParameters = createSelector(
|
||||
(state) => state.router.location.pathname.split('/').slice(-1)[0],
|
||||
function(part) {
|
||||
return parseUrl(part);
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
info: getParameters(state),
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
changeParameters: changeParameters
|
||||
};
|
||||
|
||||
|
||||
const render = ({info, changeParameters}) => {
|
||||
// if we are here - url has changed
|
||||
// otherwise everything is cached
|
||||
window.setTimeout(() => changeParameters(info), 1);
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(pure(render));
|
|
@ -0,0 +1,120 @@
|
|||
const { renderItem } = require("./Item");
|
||||
const { h } = require('../utils/format');
|
||||
const {
|
||||
calculateHorizontalCategory,
|
||||
categoryBorder,
|
||||
categoryTitleHeight,
|
||||
dividerWidth,
|
||||
itemMargin,
|
||||
smallItemWidth,
|
||||
smallItemHeight,
|
||||
subcategoryMargin,
|
||||
subcategoryTitleHeight
|
||||
} = require("../utils/landscapeCalculations");
|
||||
const { renderSubcategoryInfo } = require('./SubcategoryInfo');
|
||||
const { renderCategoryHeader } = require('./CategoryHeader');
|
||||
|
||||
const renderDivider = (color) => {
|
||||
const width = dividerWidth;
|
||||
const marginTop = 2 * subcategoryMargin;
|
||||
const height = `calc(100% - ${2 * marginTop}px)`;
|
||||
|
||||
return `<div style="
|
||||
width: ${width}px;
|
||||
margin-top: ${marginTop}px;
|
||||
height: ${height};
|
||||
border-left: ${width}px solid ${color}
|
||||
"></div>`;
|
||||
}
|
||||
|
||||
module.exports.renderHorizontalCategory = function({ header, guideInfo, subcategories, width, height, top, left, color, href, fitWidth }) {
|
||||
const addInfoIcon = !!guideInfo;
|
||||
const subcategoriesWithCalculations = calculateHorizontalCategory({ height, width, subcategories, fitWidth, addInfoIcon })
|
||||
const totalRows = Math.max(...subcategoriesWithCalculations.map(({ rows }) => rows))
|
||||
|
||||
return `
|
||||
<div style="
|
||||
width: ${width}px;
|
||||
left: ${left}px;
|
||||
height: ${height}px;
|
||||
top: ${top}px;
|
||||
position: absolute;
|
||||
" class="big-picture-section">
|
||||
<div
|
||||
style="
|
||||
position: absolute;
|
||||
background: ${color};
|
||||
top: ${subcategoryTitleHeight}px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: ${categoryBorder}px;
|
||||
"
|
||||
>
|
||||
<div style="
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: ${categoryTitleHeight}px;
|
||||
position: absolute;
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
">
|
||||
${renderCategoryHeader({href, label: header, guideAnchor: guideInfo, background: color,rotate: true})}
|
||||
</div>
|
||||
<div style="
|
||||
margin-left: 30px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
background: white;
|
||||
">
|
||||
${subcategoriesWithCalculations.map((subcategory, index) => {
|
||||
const lastSubcategory = index !== subcategories.length - 1
|
||||
const { allItems, guideInfo, columns, width, name, href } = subcategory
|
||||
const padding = fitWidth ? 0 : `${subcategoryMargin}px 0`;
|
||||
const style = `
|
||||
display: grid;
|
||||
height: 100%;
|
||||
grid-template-columns: repeat(${columns}, ${smallItemWidth}px);
|
||||
grid-auto-rows: ${smallItemHeight}px;
|
||||
`;
|
||||
const extraStyle = fitWidth ? `justify-content: space-evenly; align-content: space-evenly;` : `grid-gap: ${itemMargin}px;`;
|
||||
return `
|
||||
<div style="
|
||||
width: ${width}px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
padding: ${padding};
|
||||
box-sizing: border-box;
|
||||
">
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: ${-1 * categoryTitleHeight}px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: ${categoryTitleHeight}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
">
|
||||
<a data-type="internal" href="${href}" class="white-link">${h(name)}</a>
|
||||
</div>
|
||||
<div style="${style} ${extraStyle}">
|
||||
${allItems.map(renderItem).join('')}
|
||||
${guideInfo ? renderSubcategoryInfo({label: name, anchor: guideInfo,column: columns, row:totalRows}) : ''}
|
||||
</div>
|
||||
</div>
|
||||
${lastSubcategory ? renderDivider(color) : ''}
|
||||
`
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import isEmbed from '../utils/isEmbed';
|
||||
import isGoogle from '../utils/isGoogle';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
const skipDefaultHandler = (e) => e.preventDefault();
|
||||
const InternalLink = ({to, children, onClick, className, ...other}) => {
|
||||
if (onClick) {
|
||||
other.onClick = function(e) {
|
||||
skipDefaultHandler(e);
|
||||
onClick();
|
||||
};
|
||||
}
|
||||
if (isEmbed || isGoogle || !to) {
|
||||
return <span className={`${className}`} {...other}>{children}</span>;
|
||||
} else {
|
||||
return <NavLink className={`${className} nav-link`} {...other} to={to}>{children}</NavLink>
|
||||
}
|
||||
}
|
||||
export default pure(InternalLink);
|
||||
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
const { assetPath } = require('../utils/assetPath');
|
||||
const { fields } = require("../types/fields");
|
||||
const { h } = require('../utils/format');
|
||||
|
||||
const { readJsonFromDist } = require('../utils/readJson');
|
||||
const settings = readJsonFromDist('settings');
|
||||
|
||||
const largeItem = function(item) {
|
||||
const isMember = item.category === settings.global.membership;
|
||||
const relationInfo = fields.relation.valuesMap[item.relation]
|
||||
if (!relationInfo) {
|
||||
console.error(`no map for ${item.relation} on ${item.name}`);
|
||||
}
|
||||
const color = relationInfo.big_picture_color;
|
||||
const label = relationInfo.big_picture_label;
|
||||
const textHeight = label ? 10 : 0
|
||||
const padding = 2
|
||||
|
||||
const isMultiline = h(label).length > 20;
|
||||
const formattedLabel = isMultiline ? h(label).replace(' - ', '<br>') : h(label);
|
||||
|
||||
if (isMember) {
|
||||
return `
|
||||
<div data-id="${item.id}" class="large-item large-item-${item.size} item">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" alt="${item.name}" style="
|
||||
width: calc(100% - ${2 * padding}px);
|
||||
height: calc(100% - ${2 * padding + textHeight}px);
|
||||
padding: 5px;
|
||||
margin: ${padding}px ${padding}px 0 ${padding}px;
|
||||
"/>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div data-id="${item.id}" class="large-item large-item-${item.size} item" style="background: ${color}">
|
||||
<img loading="lazy" src="${assetPath(item.href)}" alt="${item.name}" style="
|
||||
width: calc(100% - ${2 * padding}px);
|
||||
height: calc(100% - ${2 * padding + textHeight}px);
|
||||
padding: 5px;
|
||||
margin: ${padding}px ${padding}px 0 ${padding}px;
|
||||
"/>
|
||||
<div class="label" style="
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: ${textHeight + padding + (isMultiline ? 6 : 0) }px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background: ${color};
|
||||
color: white;
|
||||
font-size: 6.7px;
|
||||
line-height: ${isMultiline ? 9 : 13 }px;
|
||||
">${ formattedLabel }</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const smallItem = function(item) {
|
||||
const isMember = item.category === settings.global.membership;
|
||||
return `
|
||||
<img data-id="${item.id}"
|
||||
loading="lazy"
|
||||
class="item small-item"
|
||||
src="${assetPath(item.href)}"
|
||||
alt="${h(item.name)}"
|
||||
style="border-color: ${isMember ? 'white' : ''};"
|
||||
/>`
|
||||
}
|
||||
|
||||
module.exports.renderItem = function (item) {
|
||||
const {size, category, oss, categoryAttrs } = item;
|
||||
const isMember = category === settings.global.membership;
|
||||
const ossClass = isMember || oss || (categoryAttrs.isLarge && !settings.global.flags?.gray_large_items) ? 'oss' : 'nonoss';
|
||||
const isLargeClass = size > 1 ? `wrapper-large-${size}` : '';
|
||||
|
||||
return `<div class="${isLargeClass + ' item-wrapper ' + ossClass}">
|
||||
${size > 1 ? largeItem({isMember, ...item}) : smallItem({...item})}
|
||||
</div>`;
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import classNames from 'classnames'
|
||||
import _ from 'lodash';
|
||||
import ItemDialogContent from './ItemDialogContent';
|
||||
import ItemDialogButtonsContainer from './ItemDialogButtonsContainer';
|
||||
|
||||
import '../styles/itemModal.scss';
|
||||
import fields from '../types/fields';
|
||||
|
||||
let lastItemInfo;
|
||||
function getRelationStyle(relation) {
|
||||
const relationInfo = _.find(fields.relation.values, {id: relation});
|
||||
if (relationInfo && relationInfo.color) {
|
||||
return {
|
||||
border: '4px solid ' + relationInfo.color
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const ItemDialog = ({onClose, itemInfo}) => {
|
||||
const recentItemInfo = itemInfo || lastItemInfo || {};
|
||||
if (itemInfo) {
|
||||
lastItemInfo = itemInfo;
|
||||
}
|
||||
return (
|
||||
<Dialog open={!!itemInfo} onClose={() => onClose() } transitionDuration={400}
|
||||
classes={{paper:'modal-body'}}
|
||||
className={classNames('modal', 'product', {nonoss : recentItemInfo.oss === false})}>
|
||||
{ itemInfo && <ItemDialogButtonsContainer/> }
|
||||
{ (itemInfo || lastItemInfo) && <ItemDialogContent itemInfo={itemInfo || lastItemInfo}/> }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default pure(ItemDialog);
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
|
||||
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
|
||||
import KeyHandler from 'react-key-handler';
|
||||
|
||||
import '../styles/itemModal.scss';
|
||||
|
||||
const ItemDialogButtons = ({hasSelectedItem, onClose, previousItemId, nextItemId, onSelectItem }) => {
|
||||
if (!hasSelectedItem) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className='modal-buttons'>
|
||||
{ nextItemId && <KeyHandler keyValue="ArrowRight" onKeyHandle={() => onSelectItem(nextItemId)} /> }
|
||||
{ previousItemId && <KeyHandler keyValue="ArrowLeft" onKeyHandle={() => onSelectItem(previousItemId)} /> }
|
||||
<a className="modal-close" onClick={() => onClose()}>×</a>
|
||||
<span className="modal-prev" disabled={!previousItemId} onClick={(e) => {e.stopPropagation(); onSelectItem(previousItemId)}}>
|
||||
<ChevronLeftIcon style={{ fontSize:'1.2em'}} />
|
||||
</span>
|
||||
<span className="modal-next" disabled={!nextItemId} onClick={(e) => {e.stopPropagation(); onSelectItem(nextItemId)}}>
|
||||
<ChevronRightIcon style={{ fontSize:'1.2em'}} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default pure(ItemDialogButtons);
|
|
@ -1,32 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import createSelector from '../utils/createSelector';
|
||||
import ItemDialogButtons from './ItemDialogButtons';
|
||||
|
||||
import { changeSelectedItemId, closeDialog } from '../reducers/mainReducer';
|
||||
import getGroupedItems, {getGroupedItemsForBigPicture } from '../utils/itemsCalculator';
|
||||
import selectedItemCalculator from '../utils/selectedItemCalculator';
|
||||
|
||||
const getSelectedItem = createSelector(
|
||||
[ getGroupedItems,
|
||||
getGroupedItemsForBigPicture,
|
||||
(state) => state.main.selectedItemId,
|
||||
(state) => state.main.mainContentMode !== 'card'
|
||||
],
|
||||
function(groupedItems,groupedItemsForBigPicture, selectedItemId, isBigPicture) {
|
||||
const selectedItemInfo = selectedItemCalculator(groupedItems, groupedItemsForBigPicture, selectedItemId, isBigPicture);
|
||||
return {
|
||||
hasSelectedItem: selectedItemInfo.hasSelectedItem,
|
||||
nextItemId: selectedItemInfo.nextItemId,
|
||||
previousItemId: selectedItemInfo.previousItemId
|
||||
};
|
||||
}
|
||||
)
|
||||
const mapStateToProps = (state) => ({
|
||||
... getSelectedItem(state)
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
onClose: closeDialog,
|
||||
onSelectItem: changeSelectedItemId
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ItemDialogButtons);
|
|
@ -1,31 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import createSelector from '../utils/createSelector';
|
||||
import ItemDialog from './ItemDialog';
|
||||
|
||||
import { changeSelectedItemId, closeDialog } from '../reducers/mainReducer';
|
||||
import getGroupedItems, {getGroupedItemsForBigPicture } from '../utils/itemsCalculator';
|
||||
import selectedItemCalculator from '../utils/selectedItemCalculator';
|
||||
|
||||
const getSelectedItem = createSelector(
|
||||
[ getGroupedItems,
|
||||
getGroupedItemsForBigPicture,
|
||||
(state) => state.main.selectedItemId,
|
||||
(state) => state.main.mainContentMode !== 'card'
|
||||
],
|
||||
function(groupedItems,groupedItemsForBigPicture, selectedItemId, isBigPicture) {
|
||||
const selectedItemInfo = selectedItemCalculator(groupedItems, groupedItemsForBigPicture, selectedItemId, isBigPicture);
|
||||
|
||||
return {
|
||||
itemInfo: selectedItemInfo.itemInfo,
|
||||
};
|
||||
}
|
||||
)
|
||||
const mapStateToProps = (state) => ({
|
||||
... getSelectedItem(state)
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
onClose: closeDialog,
|
||||
onSelectItem: changeSelectedItemId
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ItemDialog);
|
|
@ -1,631 +0,0 @@
|
|||
import React, { Fragment, useState } from 'react';
|
||||
import SvgIcon from '@material-ui/core/SvgIcon';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import KeyHandler from 'react-key-handler';
|
||||
import _ from 'lodash';
|
||||
import OutboundLink from './OutboundLink';
|
||||
import millify from 'millify';
|
||||
import relativeDate from 'relative-date';
|
||||
import { filtersToUrl } from '../utils/syncToUrl';
|
||||
import formatNumber from '../utils/formatNumber';
|
||||
import isParent from '../utils/isParent';
|
||||
import InternalLink from './InternalLink';
|
||||
import '../styles/itemModal.scss';
|
||||
import fields from '../types/fields';
|
||||
import isGoogle from '../utils/isGoogle';
|
||||
import isEmbed from '../utils/isEmbed';
|
||||
import settings from 'project/settings.yml';
|
||||
import TweetButton from './TweetButton';
|
||||
import currentDevice from 'current-device';
|
||||
import TwitterTimeline from "./TwitterTimeline";
|
||||
import {Bar, Pie, defaults} from 'react-chartjs-2';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import useWindowSize from "@rooks/use-window-size"
|
||||
import classNames from 'classnames'
|
||||
import CreateWidthMeasurer from 'measure-text-width';
|
||||
|
||||
const measureWidth = CreateWidthMeasurer(window).setFont('0.6rem Roboto');
|
||||
|
||||
|
||||
let productScrollEl = null;
|
||||
const formatDate = function(x) {
|
||||
if (x.text) {
|
||||
return x.text;
|
||||
}
|
||||
return relativeDate(new Date(x));
|
||||
};
|
||||
const formatTwitter = function(x) {
|
||||
const name = x.split('/').slice(-1)[0];
|
||||
return '@' + name;
|
||||
}
|
||||
|
||||
function getRelationStyle(relation) {
|
||||
const relationInfo = _.find(fields.relation.values, {id: relation});
|
||||
if (relationInfo && relationInfo.color) {
|
||||
return {
|
||||
border: '4px solid ' + relationInfo.color
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const showTwitter = !isGoogle;
|
||||
|
||||
const iconGithub = <svg viewBox="0 0 24 24">
|
||||
<path d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58
|
||||
9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81
|
||||
5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18
|
||||
9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5
|
||||
6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84
|
||||
13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39
|
||||
18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68
|
||||
14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
|
||||
</svg>;
|
||||
|
||||
const linkTag = (label, { name, url = null, color = 'blue', multiline = false }) => {
|
||||
return (<InternalLink to={url || '/'} className={`tag tag-${color} ${multiline ? 'multiline' : ''}`}>
|
||||
{(name ? <span className="tag-name">{name}</span> : null)}
|
||||
<span className="tag-value">{label}</span>
|
||||
</InternalLink>)
|
||||
}
|
||||
|
||||
const parentTag = (project) => {
|
||||
const membership = Object.values(settings.membership).find(({ crunchbase_and_children }) => {
|
||||
return isParent(crunchbase_and_children, project)
|
||||
});
|
||||
|
||||
if (membership) {
|
||||
const { label, name, crunchbase_and_children } = membership;
|
||||
const slug = crunchbase_and_children.split("/").pop();
|
||||
return linkTag(label, {name, url: filtersToUrl({filters: {parents: slug}, grouping: 'organization'})});
|
||||
}
|
||||
}
|
||||
|
||||
const projectTag = function({relation, isSubsidiaryProject, project, ...item}) {
|
||||
if (relation === false) {
|
||||
return null;
|
||||
}
|
||||
const { prefix, tag } = _.find(fields.relation.values, {id: project}) || {};
|
||||
|
||||
if (prefix && tag) {
|
||||
return linkTag(tag, {name: prefix, url: filtersToUrl({filters:{relation: project}})})
|
||||
}
|
||||
|
||||
if (isSubsidiaryProject) {
|
||||
const url = filtersToUrl({filters: {format: 'card-mode', relation: 'member', organization: item.organization}});
|
||||
return linkTag("Subsidiary Project", { name: settings.global.short_name, url: url });
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const memberTag = function({relation, member, enduser}) {
|
||||
if (relation === 'member' || relation === 'company') {
|
||||
const info = settings.membership[member];
|
||||
const name = info.name;
|
||||
const label = enduser ? (info.end_user_label || info.label) : info.label ;
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
return linkTag(label, {name: name, url: filtersToUrl({filters: {relation: relation}})});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const openSourceTag = function(oss) {
|
||||
if (oss) {
|
||||
const url = filtersToUrl({grouping: 'license', filters: {license: 'Open Source'}});
|
||||
return linkTag("Open Source Software", { url, color: "orange" });
|
||||
}
|
||||
};
|
||||
|
||||
const licenseTag = function({relation, license, hideLicense}) {
|
||||
if (relation === 'company' || hideLicense) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { label } = _.find(fields.license.values, {id: license});
|
||||
const url = filtersToUrl({grouping: 'license', filters:{license: license}});
|
||||
const width = measureWidth(label);
|
||||
console.info({width: width});
|
||||
return linkTag(label, { name: "License", url, color: "purple", multiline: width > 90 });
|
||||
}
|
||||
const badgeTag = function(itemInfo) {
|
||||
if (!itemInfo.bestPracticeBadgeId) {
|
||||
if (itemInfo.oss) {
|
||||
const emptyUrl="https://bestpractices.coreinfrastructure.org/";
|
||||
return (<OutboundLink to={emptyUrl} className="tag tag-grass">
|
||||
<span className="tag-value">No CII Best Practices </span>
|
||||
</OutboundLink>);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const url = `https://bestpractices.coreinfrastructure.org/en/projects/${itemInfo.bestPracticeBadgeId}`;
|
||||
const label = itemInfo.bestPracticePercentage === 100 ? 'passing' : (itemInfo.bestPracticePercentage + '%');
|
||||
return (<OutboundLink to={url} className="tag tag-grass">
|
||||
<span className="tag-name">CII Best Practices</span>
|
||||
<span className="tag-value">{label}</span>
|
||||
</OutboundLink>);
|
||||
}
|
||||
|
||||
const chart = function(itemInfo) {
|
||||
if (isEmbed || !itemInfo.github_data || !itemInfo.github_data.languages) {
|
||||
return null;
|
||||
}
|
||||
const callbacks = defaults.global.tooltips.callbacks;
|
||||
function percents(v) {
|
||||
const p = Math.round(v / total * 100);
|
||||
if (p === 0) {
|
||||
return '<1%';
|
||||
} else {
|
||||
return p + '%';
|
||||
}
|
||||
}
|
||||
const newCallbacks = {...callbacks, label: function(tooltipItem, data) {
|
||||
const v = data.datasets[0].data[tooltipItem.index];
|
||||
const value = millify(v, {precision: 1});
|
||||
const language = languages[tooltipItem.index];
|
||||
return `${percents(language.value)} (${value})`;
|
||||
}};
|
||||
/*{
|
||||
label: function(tooltipItem, data) {
|
||||
debugger
|
||||
var label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += Math.round(tooltipItem.yLabel * 100) / 100;
|
||||
return label;
|
||||
}
|
||||
}; */
|
||||
const allLanguages = itemInfo.github_data.languages;
|
||||
const languages = (function() {
|
||||
const maxEntries = 7;
|
||||
if (allLanguages.length <= maxEntries) {
|
||||
return allLanguages
|
||||
} else {
|
||||
return allLanguages.slice(0, maxEntries).concat([{
|
||||
name: 'Other',
|
||||
value: _.sum( allLanguages.slice(maxEntries - 1).map( (x) => x.value)),
|
||||
color: 'Grey'
|
||||
}]);
|
||||
}
|
||||
})();
|
||||
const data = {
|
||||
labels: languages.map((x) => x.name),
|
||||
datasets: [{
|
||||
data: languages.map( (x) => x.value),
|
||||
backgroundColor: languages.map( (x) => x.color)
|
||||
}]
|
||||
};
|
||||
const total = _.sumBy(languages, 'value');
|
||||
|
||||
function getLegendText(language) {
|
||||
const millify = require('millify').default;
|
||||
const total = _.sumBy(languages, 'value');
|
||||
return `${language.name} ${percents(language.value)}`;
|
||||
}
|
||||
|
||||
function getPopupText(language) {
|
||||
const millify = require('millify').default;
|
||||
return `${language.name} ${millify(language.value, {precision: 1})}`;
|
||||
}
|
||||
|
||||
|
||||
const legend = <div style={{position: 'absolute', width: 170, left: 0, top: 0, marginTop: -5, marginBottom: 5, fontSize: '0.8em' }}>
|
||||
{languages.map(function(language) {
|
||||
const url = language.name === 'Other' ? null : filtersToUrl({grouping: 'no', filters: {language: language.name }});
|
||||
return <div style = {{
|
||||
position: 'relative',
|
||||
marginTop: 2,
|
||||
height: 12
|
||||
}} >
|
||||
<div style={{display: 'inline-block', position: 'absolute', height: 12, width: 12, background: language.color, top: 2, marginRight: 4}} />
|
||||
<div style={{display: 'inline-block', position: 'relative', width: 125, left: 16, whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden'}}>
|
||||
<InternalLink to={url}>{ getLegendText(language) } </InternalLink></div>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
|
||||
return <div style={{width: 220, height: 120, position: 'relative'}}>
|
||||
<div style={{marginLeft: 170, width: 100, height: 100}}>
|
||||
<Pie height={100} width={100} data={data} legend={{display: false}} options={{tooltips: {callbacks: newCallbacks}}} />
|
||||
</div>
|
||||
{ legend }
|
||||
</div>
|
||||
}
|
||||
|
||||
const participation = function(itemInfo) {
|
||||
const { innerWidth } = useWindowSize();
|
||||
if (isEmbed || !itemInfo.github_data || !itemInfo.github_data.contributions) {
|
||||
return null;
|
||||
}
|
||||
let lastMonth = null;
|
||||
let lastWeek = null;
|
||||
const data = {
|
||||
labels: _.range(0, 51).map(function(week) {
|
||||
const firstWeek = new Date(itemInfo.github_data.firstWeek.replace('Z', 'T00:00:00Z'));
|
||||
firstWeek.setDate(firstWeek.getDate() + week * 7);
|
||||
const m = firstWeek.getMonth();
|
||||
if (lastMonth === null) {
|
||||
lastMonth = m;
|
||||
lastWeek = week;
|
||||
}
|
||||
else if (m % 12 === (lastMonth + 2) % 12) {
|
||||
if (week > lastWeek + 6) {
|
||||
lastMonth = m;
|
||||
lastWeek = week;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
const result = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ')[m];
|
||||
return result;
|
||||
}),
|
||||
datasets: [{
|
||||
backgroundColor: 'darkblue',
|
||||
labels: [],
|
||||
data: itemInfo.github_data.contributions.split(';').map( (x)=> +x).slice(-51)
|
||||
}]
|
||||
};
|
||||
const callbacks = defaults.global.tooltips.callbacks;
|
||||
const newCallbacks = {...callbacks, title: function(data) {
|
||||
const firstWeek = new Date(itemInfo.github_data.firstWeek.replace('Z', 'T00:00:00Z'));
|
||||
const week = data[0].index;
|
||||
firstWeek.setDate(firstWeek.getDate() + week * 7);
|
||||
const s = firstWeek.toISOString().substring(0, 10);
|
||||
return s;
|
||||
}};
|
||||
const options = {
|
||||
tooltips: {callbacks: newCallbacks},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: false,
|
||||
ticks: {
|
||||
backdropPaddingY: 15,
|
||||
autoSkip: false,
|
||||
minRotation: 0,
|
||||
maxRotation: 0
|
||||
},
|
||||
scaleLabel: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
callback: function (value) { if (Number.isInteger(value)) { return value; } }
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
const width = Math.min(innerWidth - 110, 300);
|
||||
return <div style={{width: width, height: 150, position: 'relative'}}>
|
||||
<Bar height={150} width={width} data={data} legend={{display: false}} options={options} />
|
||||
<div style={{
|
||||
transform: 'rotate(-90deg)',
|
||||
position: 'absolute',
|
||||
left: -24,
|
||||
fontSize: 10,
|
||||
top: 59
|
||||
}}>Commits</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function handleUp() {
|
||||
productScrollEl.scrollBy({top: -200, behavior: 'smooth'});
|
||||
}
|
||||
function handleDown() {
|
||||
productScrollEl.scrollBy({top: 200, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
let timeoutId;
|
||||
const ItemDialogContent = ({ itemInfo }) => {
|
||||
const setIsLandscape = useState(currentDevice.landscape())[1]
|
||||
const [showAllRepos, setShowAllRepos] = useState(false)
|
||||
if (!timeoutId) {
|
||||
timeoutId = setInterval(function() {
|
||||
setIsLandscape(currentDevice.landscape());
|
||||
}, 1000);
|
||||
}
|
||||
const { innerWidth, innerHeight } = useWindowSize();
|
||||
|
||||
const linkToOrganization = filtersToUrl({grouping: 'organization', filters: {organization: itemInfo.organization}});
|
||||
const itemCategory = function(path) {
|
||||
var separator = <span className="product-category-separator" key="product-category-separator">•</span>;
|
||||
var subcategory = _.find(fields.landscape.values,{id: path});
|
||||
var category = _.find(fields.landscape.values, {id: subcategory.parentId});
|
||||
var categoryMarkup = (
|
||||
<InternalLink key="category" to={filtersToUrl({grouping: 'landscape', filters: {landscape: category.id}})}>{category.label}</InternalLink>
|
||||
)
|
||||
var subcategoryMarkup = (
|
||||
<InternalLink key="subcategory" to={filtersToUrl({grouping: 'landscape', filters: {landscape: path}})}>{subcategory.label}</InternalLink>
|
||||
)
|
||||
return (<span>{[categoryMarkup, separator, subcategoryMarkup]}</span>);
|
||||
}
|
||||
const twitterElement = itemInfo.twitter &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">Twitter</div>
|
||||
<div className="product-property-value col col-60">
|
||||
<OutboundLink to={itemInfo.twitter}>{formatTwitter(itemInfo.twitter)}</OutboundLink>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const latestTweetDateElement = itemInfo.twitter && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-50">Latest Tweet</div>
|
||||
<div className="product-property-value col col-50">
|
||||
{ itemInfo.latestTweetDate && (
|
||||
<OutboundLink to={itemInfo.twitter}>{formatDate(itemInfo.latestTweetDate)}</OutboundLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const firstCommitDateElement = itemInfo.firstCommitDate && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">First Commit</div>
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={itemInfo.firstCommitLink} >{formatDate(itemInfo.firstCommitDate)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const contributorsCountElement = itemInfo.contributorsCount ? (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">Contributors</div>
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={itemInfo.contributorsLink}>{itemInfo.contributorsCount}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const headquartersElement = itemInfo.headquarters && itemInfo.headquarters !== 'N/A' && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">Headquarters</div>
|
||||
<div className="product-property-value tight-col col-60"><InternalLink to={filtersToUrl({grouping: 'headquarters', filters:{headquarters:itemInfo.headquarters}})}>{itemInfo.headquarters}</InternalLink></div>
|
||||
</div>
|
||||
);
|
||||
const amountElement = !settings.global.hide_funding_and_market_cap && Number.isInteger(itemInfo.amount) && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">{itemInfo.amountKind === 'funding' ? 'Funding' : 'Market Cap'}</div>
|
||||
{ itemInfo.amountKind === 'funding' &&
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={itemInfo.crunchbase + '#section-funding-rounds'}>
|
||||
{'$' + millify(itemInfo.amount)}
|
||||
</OutboundLink>
|
||||
</div>
|
||||
}
|
||||
{ itemInfo.amountKind !== 'funding' &&
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={'https://finance.yahoo.com/quote/' + itemInfo.yahoo_finance_data.effective_ticker}>
|
||||
{'$' + millify(itemInfo.amount)}
|
||||
</OutboundLink>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
const tickerElement = itemInfo.ticker && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-40">Ticker</div>
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink to={"https://finance.yahoo.com/quote/" + itemInfo.yahoo_finance_data.effective_ticker}>
|
||||
{itemInfo.yahoo_finance_data.effective_ticker}
|
||||
</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const latestCommitDateElement = itemInfo.latestCommitDate && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-50">Latest Commit</div>
|
||||
<div className="product-property-value col col-50">
|
||||
<OutboundLink to={itemInfo.latestCommitLink}>{formatDate(itemInfo.latestCommitDate)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const releaseDateElement = itemInfo.releaseDate && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-50">Latest Release</div>
|
||||
<div className="product-property-value col col-50">
|
||||
<OutboundLink to={itemInfo.releaseLink}>{formatDate(itemInfo.releaseDate)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const crunchbaseEmployeesElement = itemInfo.crunchbaseData && itemInfo.crunchbaseData.numEmployeesMin && (
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-50">Headcount</div>
|
||||
<div className="product-property-value col col-50">{formatNumber(itemInfo.crunchbaseData.numEmployeesMin)}-{formatNumber(itemInfo.crunchbaseData.numEmployeesMax)}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const scrollAllContent = innerWidth < 1000 || innerHeight < 630;
|
||||
const cellStyle = {
|
||||
width: 146,
|
||||
marginRight: 4,
|
||||
height: 26,
|
||||
display: 'inline-block',
|
||||
layout: 'relative',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
const productLogoAndTags = <Fragment>
|
||||
<div className="product-logo" style={getRelationStyle(itemInfo.relation)}>
|
||||
<img src={itemInfo.href} className='product-logo-img' alt={itemInfo.name}/>
|
||||
</div>
|
||||
<div className="product-tags">
|
||||
<div className="product-badges" style = {{width: Math.min(300, innerWidth - 110)}} >
|
||||
<div style={cellStyle}>{projectTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{parentTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{openSourceTag(itemInfo.oss)}</div>
|
||||
<div style={cellStyle}>{licenseTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{badgeTag(itemInfo)}</div>
|
||||
<div style={cellStyle}><TweetButton/></div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>;
|
||||
|
||||
const charts = <Fragment>
|
||||
{chart(itemInfo)}
|
||||
{participation(itemInfo)}
|
||||
</Fragment>
|
||||
|
||||
const productLogoAndTagsAndCharts = <Fragment>
|
||||
<div className="product-logo" style={getRelationStyle(itemInfo.relation)}>
|
||||
<img src={itemInfo.href} className='product-logo-img'/>
|
||||
</div>
|
||||
<div className="product-tags">
|
||||
<div className="product-badges" style = {{width: 300}} >
|
||||
<div style={cellStyle}>{projectTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{parentTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{openSourceTag(itemInfo.oss)}</div>
|
||||
<div style={cellStyle}>{licenseTag(itemInfo)}</div>
|
||||
<div style={cellStyle}>{badgeTag(itemInfo)}</div>
|
||||
<div style={cellStyle}><TweetButton/></div>
|
||||
{chart(itemInfo)}
|
||||
{participation(itemInfo)}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>;
|
||||
|
||||
const shortenUrl = (url) => url.replace(/http(s)?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
||||
|
||||
const productInfo = <Fragment>
|
||||
<div className="product-main">
|
||||
{ !isGoogle && <React.Fragment>
|
||||
<div className="product-name">{itemInfo.name}</div>
|
||||
<div className="product-parent"><InternalLink to={linkToOrganization}><span>{itemInfo.organization}</span>{memberTag(itemInfo)}</InternalLink></div>
|
||||
<div className="product-category">{itemCategory(itemInfo.landscape)}</div>
|
||||
<div className="product-description">{itemInfo.description}</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
{ isGoogle && <React.Fragment>
|
||||
<div className="product-name">{itemInfo.name}</div>
|
||||
<div className="product-description">{itemInfo.description}</div>
|
||||
<div className="product-parent"><InternalLink to={linkToOrganization}>{itemInfo.organization}</InternalLink></div>
|
||||
<div className="product-category">{itemCategory(itemInfo.landscape)}</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
<div className="product-properties">
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20">Website</div>
|
||||
<div className="product-property-value col col-80">
|
||||
<OutboundLink to={itemInfo.homepage_url}>{shortenUrl(itemInfo.homepage_url)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
{ itemInfo.repos && itemInfo.repos.map(({ url, stars }, idx) => {
|
||||
return <div className={`product-property row ${ idx < 3 || showAllRepos ? '' : 'hidden' }`}>
|
||||
<div className="product-property-name col col-20">
|
||||
{ idx === 0 && (itemInfo.repos.length > 1 ? 'Repositories' : 'Repository') }
|
||||
</div>
|
||||
<div className="product-property-value product-repo col col-80">
|
||||
<OutboundLink to={url}>{shortenUrl(url)}</OutboundLink>
|
||||
|
||||
{ idx === 0 && itemInfo.repos.length > 1 && <span className="primary-repo">(primary)</span> }
|
||||
|
||||
<span className="product-repo-stars">
|
||||
<SvgIcon style={{ color: '#7b7b7b' }}>{iconGithub}</SvgIcon>
|
||||
<StarIcon style={{ color: '#7b7b7b' }}/>{formatNumber(stars)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
{itemInfo.repos && itemInfo.repos.length > 1 &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20"></div>
|
||||
<div className="product-property-value product-repo col col-80">
|
||||
{itemInfo.repos && itemInfo.repos.length > 3 &&
|
||||
<span>
|
||||
<a href="#" onClick={() => setShowAllRepos(!showAllRepos)}>{ showAllRepos ? 'less...' : 'more...' }</a>
|
||||
</span>
|
||||
}
|
||||
<span className="product-repo-stars-label">
|
||||
total:
|
||||
</span>
|
||||
<span className="product-repo-stars">
|
||||
<SvgIcon style={{color: '#7b7b7b'}}>{iconGithub}</SvgIcon>
|
||||
<StarIcon style={{color: '#7b7b7b'}} />
|
||||
{itemInfo.starsAsText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{itemInfo.crunchbase &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20">Crunchbase</div>
|
||||
<div className="product-property-value col col-80">
|
||||
<OutboundLink to={itemInfo.crunchbase}>{shortenUrl(itemInfo.crunchbase)}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{itemInfo.crunchbaseData && itemInfo.crunchbaseData.linkedin &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20">LinkedIn</div>
|
||||
<div className="product-property-value col col-80">
|
||||
<OutboundLink to={itemInfo.crunchbaseData.linkedin}>
|
||||
{shortenUrl(itemInfo.crunchbaseData.linkedin)}
|
||||
</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="row">
|
||||
{ innerWidth <= 1000 && <div className="col col-50 single-column">
|
||||
{ twitterElement }
|
||||
{ latestTweetDateElement }
|
||||
{ firstCommitDateElement }
|
||||
{ latestCommitDateElement }
|
||||
{ contributorsCountElement }
|
||||
{ releaseDateElement }
|
||||
{ headquartersElement }
|
||||
{ crunchbaseEmployeesElement }
|
||||
{ amountElement }
|
||||
{ tickerElement }
|
||||
</div> }
|
||||
{ innerWidth > 1000 && <div className="col col-50">
|
||||
{ twitterElement }
|
||||
{ firstCommitDateElement }
|
||||
{ contributorsCountElement }
|
||||
{ headquartersElement }
|
||||
{ amountElement }
|
||||
{ tickerElement }
|
||||
</div>
|
||||
}
|
||||
{ innerWidth > 1000 && <div className="col col-50">
|
||||
{ latestTweetDateElement }
|
||||
{ latestCommitDateElement }
|
||||
{ releaseDateElement }
|
||||
{ crunchbaseEmployeesElement }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>;
|
||||
|
||||
return (
|
||||
<div className={classNames("modal-content", {'scroll-all-content': scrollAllContent})} >
|
||||
<KeyHandler keyEventName="keydown" keyValue="ArrowUp" onKeyHandle={handleUp} />
|
||||
<KeyHandler keyEventName="keydown" keyValue="ArrowDown" onKeyHandle={handleDown} />
|
||||
|
||||
{ !scrollAllContent && !isGoogle && productLogoAndTagsAndCharts }
|
||||
|
||||
<div className="product-scroll" ref={(x) => productScrollEl = x }>
|
||||
{ !scrollAllContent && productInfo }
|
||||
{ scrollAllContent && <div className="landscape-layout">
|
||||
{productLogoAndTags}
|
||||
<div className="right-column">{productInfo}</div>
|
||||
{charts}
|
||||
</div>
|
||||
}
|
||||
|
||||
{ showTwitter && itemInfo.twitter && <TwitterTimeline twitter={itemInfo.twitter} />}
|
||||
</div>
|
||||
{ !scrollAllContent && isGoogle && productLogoAndTags }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ItemDialogContent
|
|
@ -0,0 +1,820 @@
|
|||
const _ = require('lodash');
|
||||
const relativeDate = require('relative-date');
|
||||
|
||||
const { formatNumber } = require('../utils/formatNumber');
|
||||
const { isParent } = require('../utils/isParent');
|
||||
const { fields } = require('../types/fields');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
const { stringifyParams } = require('../utils/routing');
|
||||
const { millify, h } = require('../utils/format');
|
||||
const icons = require('../utils/icons');
|
||||
|
||||
module.exports.render = function({settings, tweetsCount, itemInfo}) {
|
||||
|
||||
const closeUrl = stringifyParams;
|
||||
|
||||
const formatDate = function(x) {
|
||||
if (x.text) {
|
||||
return x.text;
|
||||
}
|
||||
return relativeDate(new Date(x));
|
||||
};
|
||||
|
||||
function getLinkedIn(itemInfo) {
|
||||
if (itemInfo.extra && itemInfo.extra.override_linked_in) {
|
||||
return itemInfo.extra.override_linked_in;
|
||||
}
|
||||
if (itemInfo.crunchbaseData && itemInfo.crunchbaseData.linkedin) {
|
||||
return itemInfo.crunchbaseData.linkedin;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getRelationStyle(relation) {
|
||||
const relationInfo = fields.relation.valuesMap[relation]
|
||||
if (relationInfo && relationInfo.color) {
|
||||
return `border: 4px solid ${relationInfo.color};`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const formatTwitter = function(x) {
|
||||
const name = x.split('/').slice(-1)[0];
|
||||
return '@' + name;
|
||||
}
|
||||
|
||||
const tweetButton = (function() {
|
||||
// locate zoom buttons
|
||||
|
||||
if (!process.env.TWITTER_KEYS) {
|
||||
return ``
|
||||
}
|
||||
const twitterUrl = `https://twitter.com/intent/tweet`
|
||||
|
||||
return `<div class="tweet-button">
|
||||
<a data-tweet="true" href="${h(twitterUrl)}">${icons.bird}<span>Tweet</span></a>
|
||||
<div class="tweet-count-wrapper">
|
||||
<div class="tweet-count">${tweetsCount}</div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
})();
|
||||
|
||||
|
||||
const renderLinkTag = (label, { name, url = null, color = 'blue', multiline = false, twoLines = false }) => {
|
||||
return `<a data-type="internal" href="${url || '/'}" class="tag tag-${color} ${multiline ? 'multiline' : ''} ${twoLines ? 'twolines' : ''}">
|
||||
${(name ? `<span class="tag-name">${h(name)}</span>` : '')}
|
||||
<span class="tag-value">${h(label)}</span>
|
||||
</a>`
|
||||
}
|
||||
|
||||
const renderParentTag = (project) => {
|
||||
const membership = Object.values(settings.membership).find(({ crunchbase_and_children }) => {
|
||||
return isParent(crunchbase_and_children, project)
|
||||
});
|
||||
|
||||
if (membership) {
|
||||
const { label, name, crunchbase_and_children } = membership;
|
||||
const slug = crunchbase_and_children.split("/").pop();
|
||||
const url = closeUrl({ grouping: 'organization', filters: {parents: slug}})
|
||||
return renderLinkTag(label, {name, url});
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const renderProjectTag = function({relation, isSubsidiaryProject, project, ...item}) {
|
||||
if (relation === false) {
|
||||
return '';
|
||||
}
|
||||
const { prefix, tag } = fields.relation.valuesMap[project] || {};
|
||||
|
||||
if (prefix && tag) {
|
||||
const url = closeUrl({ filters: { relation: project }})
|
||||
return renderLinkTag(tag, {name: prefix, url, twoLines: tag.indexOf(' - ') !== -1 || tag.length > 20 || prefix.length > 20 })
|
||||
}
|
||||
|
||||
if (isSubsidiaryProject) {
|
||||
const url = closeUrl({ filters: { relation: 'member', organization: item.organization }})
|
||||
return renderLinkTag("Subsidiary Project", { name: settings.global.short_name, url });
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const renderMemberTag = function({relation, member, enduser}) {
|
||||
if (relation === 'member' || relation === 'company') {
|
||||
const info = settings.membership[member];
|
||||
if (!info) {
|
||||
return '';
|
||||
}
|
||||
const name = info.name;
|
||||
const label = enduser ? (info.end_user_label || info.label) : info.label ;
|
||||
if (!label) {
|
||||
return '';
|
||||
}
|
||||
const url = closeUrl({ filters: { relation }})
|
||||
return renderLinkTag(label, {name: name, url });
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const renderOpenSourceTag = function(oss) {
|
||||
if (oss) {
|
||||
const url = closeUrl({ grouping: 'license', filters: {license: 'Open Source'}})
|
||||
return renderLinkTag("Open Source Software", { url, color: "orange" });
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const renderLicenseTag = function({relation, license, hideLicense, extra}) {
|
||||
const { label } = _.find(fields.license.values, {id: license});
|
||||
|
||||
if (extra && extra.hide_license) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (relation === 'company' || hideLicense) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const url = closeUrl({ grouping: 'license', filters: { license }});
|
||||
return renderLinkTag(label, { name: "License", url, color: "purple", multiline: true});
|
||||
}
|
||||
|
||||
const renderBadgeTag = function() {
|
||||
if (settings.global.hide_best_practices) {
|
||||
return '';
|
||||
}
|
||||
if (!itemInfo.bestPracticeBadgeId) {
|
||||
if (settings.global.hide_no_best_practices) {
|
||||
return '';
|
||||
}
|
||||
if (itemInfo.oss) {
|
||||
const emptyUrl="https://bestpractices.coreinfrastructure.org/";
|
||||
return `<a data-type="external" target="_blank" href=${emptyUrl} class="tag tag-grass">
|
||||
<span class="tag-value">No OpenSSF Best Practices </span>
|
||||
</a>`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
const url = `https://bestpractices.coreinfrastructure.org/en/projects/${itemInfo.bestPracticeBadgeId}`;
|
||||
const label = itemInfo.bestPracticePercentage === 100 ? '✓' : (itemInfo.bestPracticePercentage + '%');
|
||||
return (`<a data-type="external" target="_blank" href="${url}" class="tag tag-grass">
|
||||
<span class="tag-name">OpenSSF Best Practices</span>
|
||||
<span class="tag-value">${label}</span>
|
||||
</a>`);
|
||||
}
|
||||
|
||||
const renderChart = function() {
|
||||
if (!itemInfo.github_data || !itemInfo.github_data.languages) {
|
||||
return '';
|
||||
}
|
||||
const allLanguages = itemInfo.github_data.languages;
|
||||
const languages = (function() {
|
||||
const maxEntries = 7;
|
||||
if (allLanguages.length <= maxEntries) {
|
||||
return allLanguages
|
||||
} else {
|
||||
return allLanguages.slice(0, maxEntries).concat([{
|
||||
name: 'Other',
|
||||
value: _.sum( allLanguages.slice(maxEntries - 1).map( (x) => x.value)),
|
||||
color: 'Grey'
|
||||
}]);
|
||||
}
|
||||
})();
|
||||
function getLegendText(language) {
|
||||
const total = _.sumBy(languages, 'value');
|
||||
function percents(v) {
|
||||
const p = Math.round(v / total * 100);
|
||||
if (p === 0) {
|
||||
return '<1%';
|
||||
} else {
|
||||
return p + '%';
|
||||
}
|
||||
}
|
||||
return `${language.name} ${percents(language.value)}`;
|
||||
}
|
||||
|
||||
const legend = `
|
||||
<div style="
|
||||
position: absolute;
|
||||
width: 170px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin-top: -5px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.8em;
|
||||
">
|
||||
${languages.map(function(language) {
|
||||
const url = language.name === 'Other' ? null : closeUrl({ grouping: 'no', filters: {language: language.name }});
|
||||
return `<div style="position: relative; margin-top: 2px; height: 12px;" >
|
||||
<div style="display: inline-block; position: absolute; height: 12px; width: 12px; background: ${language.color}; top: 2px; margin-right: 4px;" ></div>
|
||||
<div style="display: inline-block; position: relative; width: 125px; left: 16px; white-space: nowrap; text-overflow: 'ellipsis'; overflow: hidden;">
|
||||
<a data-type="internal" href="${url}">${h(getLegendText(language)) }</a></div>
|
||||
</div>`
|
||||
}).join('')}
|
||||
</div> `;
|
||||
|
||||
|
||||
// a quick 50 lines pie chart implementation is here
|
||||
const renderSector = ({
|
||||
path, fill
|
||||
}) => `
|
||||
<path
|
||||
d="${path}"
|
||||
fill="${fill}"
|
||||
stroke="#fff"
|
||||
strokeWidth="1"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
`;
|
||||
|
||||
const renderCircle = ({
|
||||
center, color, radius
|
||||
}) => `
|
||||
<ellipse cx=${center} cy=${center} fill=${color} rx=${radius} ry=${radius} stroke="#fff" strokeWidth="1" ></ellipse>
|
||||
`;
|
||||
|
||||
const renderSectors = ({
|
||||
center,
|
||||
data
|
||||
}) => {
|
||||
const total = data.reduce((prev, current) => current.value + prev, 0)
|
||||
let angleStart = -90;
|
||||
let angleEnd = -90;
|
||||
let angleMargin = 0;
|
||||
return total > 0 ? `
|
||||
<g>
|
||||
${data.map((d) => {
|
||||
const isLarge = d.value / total > 0.5;
|
||||
const angle = 360 * d.value / total;
|
||||
const radius = center - 1 / 2;
|
||||
|
||||
angleStart = angleEnd;
|
||||
angleMargin = angleMargin > angle ? angle : angleMargin;
|
||||
angleEnd = angleStart + angle - angleMargin;
|
||||
|
||||
const x1 = center + radius * Math.cos(Math.PI * angleStart / 180);
|
||||
const y1 = center + radius * Math.sin(Math.PI * angleStart / 180);
|
||||
const x2 = center + radius * Math.cos(Math.PI * angleEnd / 180);
|
||||
const y2 = center + radius * Math.sin(Math.PI * angleEnd / 180);
|
||||
const path = `
|
||||
M${center},${center}
|
||||
L${x1},${y1}
|
||||
A${radius},${radius}
|
||||
0 ${isLarge ? 1 : 0},1
|
||||
${x2},${y2}
|
||||
z
|
||||
`
|
||||
angleEnd += angleMargin;
|
||||
return renderSector({fill: d.color, path: path});
|
||||
}).join('')}
|
||||
</g>
|
||||
` : ''
|
||||
}
|
||||
|
||||
const renderPie = ({data}) => {
|
||||
const viewBoxSize = 100;
|
||||
const center = viewBoxSize / 2;
|
||||
if (!data || data.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return `<svg viewBox="0 0 ${viewBoxSize } ${viewBoxSize}">
|
||||
<g>
|
||||
${ data.length === 1
|
||||
? renderCircle({center: center, radius: center, ...data[0]})
|
||||
: renderSectors({center: center, data: data})
|
||||
}
|
||||
</g>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
return `<div style="width: 220px; height: 120px; position: relative">
|
||||
<div style="margin-left: 170px; width: 100px; height: 100px;">
|
||||
${renderPie({data: languages})}
|
||||
</div>
|
||||
${legend}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const renderParticipation = function() {
|
||||
if (!itemInfo.github_data || !itemInfo.github_data.contributions) {
|
||||
return '';
|
||||
}
|
||||
// build an Y scale axis
|
||||
// build an X scale axis
|
||||
const monthText = (function() {
|
||||
const firstWeek = new Date(itemInfo.github_data.firstWeek.replace('Z', 'T00:00:00Z'));
|
||||
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
|
||||
const result = [];
|
||||
const m = firstWeek.getMonth();
|
||||
for (let i = 0; i < 12; i += 2) {
|
||||
const monthName = months[(m + i) % 12];
|
||||
const separator = i === 12 ? '' : `<span style="width: 23px; display: inline-block;" ></span>`;
|
||||
result.push(`<span style="width: 30px; display: inline-block">${monthName}</span>`);
|
||||
result.push(separator);
|
||||
}
|
||||
return result.join('');
|
||||
})();
|
||||
|
||||
const barValues = itemInfo.github_data.contributions.split(';').map( (x)=> +x).slice(-51)
|
||||
const { maxValue, step } = ( () => {
|
||||
const max = _.max(barValues);
|
||||
let maxValue;
|
||||
let step;
|
||||
for (let pow = 0; pow < 10; pow++) {
|
||||
for (let v of [1, 2, 5]) {
|
||||
const value = v * Math.pow(10, pow);
|
||||
if (value >= max && !maxValue) {
|
||||
maxValue = value;
|
||||
if (pow === 0) {
|
||||
step = v;
|
||||
} else {
|
||||
step = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
step,
|
||||
maxValue
|
||||
}
|
||||
})();
|
||||
const xyLines = ( () => {
|
||||
const result = []
|
||||
for (let x = 0; x <= step; x += 1) {
|
||||
result.push(`<div style="
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
right: 0;
|
||||
top: ${(x / step) * 100}%;
|
||||
height: .5px;
|
||||
background: #777;"
|
||||
></div>
|
||||
`)
|
||||
result.push(`<span style="
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
left: 5px;
|
||||
right: 0px;
|
||||
top: ${(x / step) * 150 - 7}px;
|
||||
">${(step - x) / step * maxValue}</span>`);
|
||||
}
|
||||
result.push(`<div style="
|
||||
position: absolute;
|
||||
left: 25px;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
width: .5px;
|
||||
background: #777;
|
||||
"></div>`);
|
||||
return result.join('');
|
||||
})();
|
||||
const bars = barValues.map(function(value, index) {
|
||||
if (value === 0) {
|
||||
value = 1;
|
||||
}
|
||||
return `<div style="
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: ${(maxValue - value) / maxValue * 150}px;
|
||||
left: ${24 + index * 5.6}px;
|
||||
width: 4px;
|
||||
background: #00F;
|
||||
border: 1px solid #777;
|
||||
" ></div>`;
|
||||
}).join('');
|
||||
|
||||
|
||||
const width = 300;
|
||||
return `<div style="width: ${width}px; height: 150px; position: relative;">
|
||||
${xyLines}
|
||||
${bars}
|
||||
<div style="
|
||||
transform: rotate(-90deg);
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
font-size: 10px;
|
||||
top: 59px;
|
||||
">Commits</div>
|
||||
<div style="
|
||||
font-size: 10px;
|
||||
left: 20px;
|
||||
bottom: -16px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
">${monthText}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const linkToOrganization = closeUrl({ grouping: 'organization', filters: {organization: itemInfo.organization}});
|
||||
|
||||
const renderItemCategory = function({path, itemInfo}) {
|
||||
var separator = `<span class="product-category-separator" key="product-category-separator">•</span>`;
|
||||
var subcategory = _.find(fields.landscape.values,{id: path});
|
||||
if (!subcategory) {
|
||||
throw new Error(`Failed to render ${itemInfo.name}, can not find a subcategory: ${path}, available paths are below: \n${fields.landscape.values.map( (x) => x.id).join('\n')}`);
|
||||
}
|
||||
var category = _.find(fields.landscape.values, {id: subcategory.parentId});
|
||||
var categoryMarkup = `
|
||||
<a data-type="internal" href="${closeUrl({ grouping: 'landscape', filters: {landscape: category.id}})}">${h(category.label)}</a>
|
||||
`
|
||||
var subcategoryMarkup = `
|
||||
<a data-type="internal" href="${closeUrl({ grouping: 'landscape', filters: {landscape: path}})}">${h(subcategory.label)}</a>
|
||||
`
|
||||
return `<span>${categoryMarkup} ${separator} ${subcategoryMarkup}</span>`;
|
||||
}
|
||||
|
||||
const twitterElement = itemInfo.twitter ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">Twitter</div>
|
||||
<div class="product-property-value col col-60">
|
||||
<a data-type="external" target="_blank" href="${itemInfo.twitter}">${h(formatTwitter(itemInfo.twitter))}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const latestTweetDateElement = itemInfo.twitter ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">Latest Tweet</div>
|
||||
<div class="product-property-value col col-50">
|
||||
${ itemInfo.latestTweetDate ? `
|
||||
<a data-type="external" target="_blank" href="${h(itemInfo.twitter)}">${formatDate(itemInfo.latestTweetDate)}</a>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const firstCommitDateElement = itemInfo.firstCommitDate ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">First Commit</div>
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="${h(itemInfo.firstCommitLink)}">${formatDate(itemInfo.firstCommitDate)}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const contributorsCountElement = itemInfo.contributorsCount ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">Contributors</div>
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="${itemInfo.contributorsLink}">
|
||||
${itemInfo.contributorsCount > 500 ? '500+' : itemInfo.contributorsCount }
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const headquartersElement = itemInfo.headquarters && itemInfo.headquarters !== 'N/A' ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">Headquarters</div>
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="${closeUrl({ grouping: 'headquarters', filters:{headquarters:itemInfo.headquarters}})}">${h(itemInfo.headquarters)}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const amountElement = !settings.global.hide_funding_and_market_cap && Number.isInteger(itemInfo.amount) ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">${itemInfo.amountKind === 'funding' ? 'Funding' : 'Market Cap'}</div>
|
||||
${ itemInfo.amountKind === 'funding' ? `
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="${itemInfo.crunchbase + '#section-funding-rounds'}">
|
||||
${'$' + millify(itemInfo.amount)}
|
||||
</a>
|
||||
</div>` : ''
|
||||
}
|
||||
${ itemInfo.amountKind !== 'funding' ? `
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="https://finance.yahoo.com/quote/${itemInfo.yahoo_finance_data.effective_ticker}">
|
||||
${'$' + millify(itemInfo.amount)}
|
||||
</a>
|
||||
</div>` : ''
|
||||
}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const tickerElement = itemInfo.ticker ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">Ticker</div>
|
||||
<div class="product-property-value tight-col col-60">
|
||||
<a data-type="external" target=_blank href="https://finance.yahoo.com/quote/${itemInfo.yahoo_finance_data.effective_ticker}">
|
||||
${h(itemInfo.yahoo_finance_data.effective_ticker)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const latestCommitDateElement = itemInfo.latestCommitDate ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">Latest Commit</div>
|
||||
<div class="product-property-value col col-50">
|
||||
<a data-type="external" target=_blank href="${itemInfo.latestCommitLink}">${formatDate(itemInfo.latestCommitDate)}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const releaseDateElement = itemInfo.releaseDate ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">Latest Release</div>
|
||||
<div class="product-property-value col col-50">
|
||||
<a data-type="external" target=_blank href="${itemInfo.releaseLink}">${formatDate(itemInfo.releaseDate)}</a>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const crunchbaseEmployeesElement = itemInfo.crunchbaseData && itemInfo.crunchbaseData.numEmployeesMin ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">Headcount</div>
|
||||
<div class="product-property-value col col-50">${formatNumber(itemInfo.crunchbaseData.numEmployeesMin)}-${formatNumber(itemInfo.crunchbaseData.numEmployeesMax)}</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const specialDates = ( function() {
|
||||
let specialKeys = ['accepted', 'incubation', 'graduated', 'archived'];
|
||||
const names = {
|
||||
accepted: 'Accepted',
|
||||
incubation: 'Incubation',
|
||||
graduated: 'Graduated',
|
||||
archived: 'Archived'
|
||||
}
|
||||
let result = {};
|
||||
for (let key of specialKeys) {
|
||||
if (itemInfo.extra && itemInfo.extra[key]) {
|
||||
result[key] = itemInfo.extra[key];
|
||||
delete itemInfo.extra[key];
|
||||
}
|
||||
}
|
||||
|
||||
const keys = Object.keys(result);
|
||||
const values = Object.values(result);
|
||||
if (keys.length === 0) {
|
||||
return '';
|
||||
}
|
||||
if (keys.length === 1) {
|
||||
return `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">${names[keys[0]]}</div>
|
||||
<div class="product-property-name col col-80">${values[0]}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
if (keys.length === 2) {
|
||||
return `
|
||||
<div class="row">
|
||||
<div class="col col-50">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">${names[keys[0]]}</div>
|
||||
<div class="product-property-name col col-60">${values[0]}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-50">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">${names[keys[1]]}</div>
|
||||
<div class="product-property-name col col-50">${values[1]}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
if (keys.length === 3) {
|
||||
return `
|
||||
<div class="row">
|
||||
<div class="col col-50">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-40">${names[keys[0]]}</div>
|
||||
<div class="product-property-name col col-60">${values[0]}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-50">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-50">${names[keys[1]]}</div>
|
||||
<div class="product-property-name col col-50">${values[1]}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">${names[keys[2]]}</div>
|
||||
<div class="product-property-name col col-80">${values[2]}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
})();
|
||||
|
||||
const cloElement = ( function() {
|
||||
if (!itemInfo.extra) {
|
||||
return '';
|
||||
}
|
||||
if (!itemInfo.extra.clomonitor_svg) {
|
||||
return '';
|
||||
}
|
||||
return `
|
||||
<a href="https://clomonitor.io/projects/cncf/${itemInfo.extra.clomonitor_name}" target="_blank">
|
||||
${itemInfo.extra.clomonitor_svg}
|
||||
</a>
|
||||
`;
|
||||
})();
|
||||
|
||||
const extraElement = ( function() {
|
||||
if (!itemInfo.extra) {
|
||||
return '';
|
||||
}
|
||||
const items = Object.keys(itemInfo.extra).map( function(key) {
|
||||
if (key.indexOf('summary_') === 0) {
|
||||
return '';
|
||||
}
|
||||
if (key === 'clomonitor_name' || key === 'clomonitor_svg') {
|
||||
return '';
|
||||
}
|
||||
if (key === 'hide_license') {
|
||||
return '';
|
||||
}
|
||||
if (key === 'override_linked_in') {
|
||||
return '';
|
||||
}
|
||||
if (key === 'audits') {
|
||||
const value = itemInfo.extra[key];
|
||||
const lines = (value.map ? value : [value]).map( (auditInfo) => `
|
||||
<div>
|
||||
<a href="${h(auditInfo.url)}" target="_blank">${h(auditInfo.type)} at ${auditInfo.date}</a>
|
||||
</div>
|
||||
`).join('');
|
||||
return `<div class="product-property row">
|
||||
<div class="product-property-name tight-col col-20">Audits</div>
|
||||
<div class="product-proerty-value tight-col col-80">${lines}</div>
|
||||
</div>`;
|
||||
}
|
||||
const value = itemInfo.extra[key];
|
||||
const keyText = (function() {
|
||||
const step1 = key.replace(/_url/g, '');
|
||||
const step2 = step1.split('_').map( (x) => x.charAt(0).toUpperCase() + x.substring(1)).join(' ');
|
||||
return step2;
|
||||
})();
|
||||
const valueText = (function() {
|
||||
if (!!(new Date(value).getTime()) && typeof value === 'string') {
|
||||
return h(relativeDate(new Date(value)));
|
||||
}
|
||||
if (typeof value === 'string' && (value.indexOf('http://') === 0 || value.indexOf('https://') === 0)) {
|
||||
return `<a data-type="external" target=_blank href="${h(value)}">${h(value)}</a>`;
|
||||
}
|
||||
return h(value);
|
||||
})();
|
||||
return `<div class="product-property row">
|
||||
<div class="product-property-name tight-col col-20">${h(keyText)}</div>
|
||||
<div class="product-proerty-value tight-col col-80">${valueText}</div>
|
||||
</div>`;
|
||||
});
|
||||
return items.join('');
|
||||
})();
|
||||
|
||||
const cellStyle = `
|
||||
width: 146px;
|
||||
marginRight: 4px;
|
||||
height: 26px;
|
||||
display: inline-block;
|
||||
layout: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const productLogoAndTagsAndCharts = `
|
||||
<div class="product-logo" style="${getRelationStyle(itemInfo.relation)}">
|
||||
<img alt="product logo" src="${assetPath(itemInfo.href)}" class="product-logo-img">
|
||||
</div>
|
||||
<div class="product-tags">
|
||||
<div class="product-badges" style="width: 300px;" >
|
||||
<div style="${cellStyle}">${renderProjectTag(itemInfo)}</div>
|
||||
<div style="${cellStyle}">${renderParentTag(itemInfo)}</div>
|
||||
<div style="${cellStyle}">${renderOpenSourceTag(itemInfo.oss)}</div>
|
||||
<div style="${cellStyle}">${renderLicenseTag(itemInfo)}</div>
|
||||
<div style="${cellStyle}">${renderBadgeTag(itemInfo)}</div>
|
||||
<div style="${cellStyle}">${tweetButton}</div>
|
||||
<div class="charts-desktop">
|
||||
${renderChart(itemInfo)}
|
||||
${renderParticipation(itemInfo)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
const shortenUrl = (url) => url.replace(/http(s)?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
||||
|
||||
const productPaths1 = [itemInfo.landscape, itemInfo.second_path || [], itemInfo.allPaths || []].flat();
|
||||
const productPaths = _.uniq(productPaths1.filter( (x) => !!x));
|
||||
const productInfo = `
|
||||
<div class="product-main">
|
||||
<div class="product-name">${h(itemInfo.name)}</div>
|
||||
<div class="product-parent"><a data-type=internal href="${linkToOrganization}">
|
||||
<span>${h(itemInfo.organization)}</span>${renderMemberTag(itemInfo)}</a></div>
|
||||
${productPaths.map( (productPath) => `
|
||||
<div class="product-category">${renderItemCategory({path: productPath, itemInfo})}</div>
|
||||
`).join('')}
|
||||
<div class="product-description">${h(itemInfo.description)}</div>
|
||||
</div>
|
||||
<div class="product-properties">
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">Website</div>
|
||||
<div class="product-property-value col col-80">
|
||||
<a data-type=external target=_blank href="${itemInfo.homepage_url}">${shortenUrl(itemInfo.homepage_url)}</a>
|
||||
</div>
|
||||
</div>
|
||||
${ (itemInfo.repos || []).map(({ url, stars }, idx) => {
|
||||
return `<div class="product-property row">
|
||||
<div class="product-property-name col col-20">
|
||||
${ idx === 0 ? (itemInfo.repos.length > 1 ? 'Repositories' : 'Repository') : '' }
|
||||
</div>
|
||||
<div class="product-property-value product-repo col col-80">
|
||||
<a data-type=external target=_blank href="${url}">${shortenUrl(url)}</a>
|
||||
${ idx === 0 && itemInfo.repos.length > 1 ? `<span class="primary-repo">(primary)</span>` : '' }
|
||||
${ itemInfo.github_data ? `<span class="product-repo-stars">
|
||||
${icons.github}
|
||||
${icons.star}
|
||||
${formatNumber(stars)}
|
||||
</span> ` : ''
|
||||
}
|
||||
</div>
|
||||
</div>`
|
||||
}).join('')}
|
||||
${itemInfo.repos && (itemInfo.repos.length > 3 || (itemInfo.repos.length > 1 && itemInfo.github_data)) ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20"></div>
|
||||
<div class="product-property-value product-repo col col-80">
|
||||
${ itemInfo.github_data ? `
|
||||
<span class="product-repo-stars-label">
|
||||
total:
|
||||
</span>
|
||||
<span class="product-repo-stars">
|
||||
${icons.github}
|
||||
${icons.star}
|
||||
${formatNumber(itemInfo.github_data.stars)}
|
||||
</span> ` : ''
|
||||
}
|
||||
</div>
|
||||
</div> ` : ''
|
||||
}
|
||||
${itemInfo.crunchbase ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">Crunchbase</div>
|
||||
<div class="product-property-value col col-80">
|
||||
<a data-type=external target=_blank href="${itemInfo.crunchbase}">${shortenUrl(itemInfo.crunchbase)}</a>
|
||||
</div>
|
||||
</div> ` : ''
|
||||
}
|
||||
${getLinkedIn(itemInfo) ? `
|
||||
<div class="product-property row">
|
||||
<div class="product-property-name col col-20">LinkedIn</div>
|
||||
<div class="product-property-value col col-80">
|
||||
<a data-type=external target=_blank href="${getLinkedIn(itemInfo)}">
|
||||
${shortenUrl(getLinkedIn(itemInfo))}
|
||||
</a>
|
||||
</div>
|
||||
</div> ` : ''
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col col-50">
|
||||
${ twitterElement }
|
||||
${ firstCommitDateElement }
|
||||
${ contributorsCountElement }
|
||||
</div>
|
||||
<div class="col col-50">
|
||||
${ latestTweetDateElement }
|
||||
${ latestCommitDateElement }
|
||||
${ releaseDateElement }
|
||||
</div>
|
||||
</div>
|
||||
${specialDates}
|
||||
<div class="row">
|
||||
<div class="col col-50">
|
||||
${ headquartersElement }
|
||||
${ amountElement }
|
||||
${ tickerElement }
|
||||
</div>
|
||||
<div class="col col-50">
|
||||
${ crunchbaseEmployeesElement }
|
||||
</div>
|
||||
</div>
|
||||
${extraElement}
|
||||
${cloElement}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const result = `<div class="modal-content ${itemInfo.oss ? 'oss' : 'nonoss'}">
|
||||
${productLogoAndTagsAndCharts}
|
||||
<div class="product-scroll" >
|
||||
${productInfo}
|
||||
<div class="charts-mobile">
|
||||
${renderChart(itemInfo)}
|
||||
${renderParticipation(itemInfo)}
|
||||
</div>
|
||||
${ itemInfo.twitter ? `<div class="twitter-timeline">
|
||||
<a class="twitter-timeline" aria-hidden="true" data-tweet-limit="5" href="${itemInfo.twitter}"></a>
|
||||
</div>` : '' }
|
||||
</div>
|
||||
</div>`;
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
// Render all items here!
|
||||
|
||||
const { renderHorizontalCategory } = require('./HorizontalCategory');
|
||||
const { renderVerticalCategory } = require('./VerticalCategory');
|
||||
const { renderLandscapeInfo } = require('./LandscapeInfo');
|
||||
const { renderOtherLandscapeLink } = require('./OtherLandscapeLink');
|
||||
|
||||
const extractKeys = (obj, keys) => {
|
||||
const attributes = _.pick(obj, keys)
|
||||
|
||||
return _.mapKeys(attributes, (value, key) => _.camelCase(key))
|
||||
}
|
||||
|
||||
|
||||
module.exports.render = function({landscapeSettings, landscapeItems}) {
|
||||
const elements = landscapeSettings.elements.map(element => {
|
||||
if (element.type === 'LandscapeLink') {
|
||||
return renderOtherLandscapeLink(element)
|
||||
}
|
||||
if (element.type === 'LandscapeInfo') {
|
||||
return renderLandscapeInfo(element)
|
||||
}
|
||||
const category = landscapeItems.find(c => c.key === element.category);
|
||||
if (!category) {
|
||||
console.info(`Can not find the ${element.category}`);
|
||||
console.info(`Valid values: ${landscapeItems.map( (x) => x.key).join('; ')}`);
|
||||
}
|
||||
const attributes = extractKeys(element, ['width', 'height', 'top', 'left', 'color', 'fit_width', 'is_large'])
|
||||
const subcategories = category.subcategories.map(subcategory => {
|
||||
const allItems = subcategory.allItems.map(item => ({ ...item, categoryAttrs: attributes }))
|
||||
return { ...subcategory, allItems }
|
||||
})
|
||||
|
||||
if (element.type === 'HorizontalCategory') {
|
||||
return renderHorizontalCategory({...category, ...attributes, subcategories: subcategories});
|
||||
}
|
||||
if (element.type === 'VerticalCategory') {
|
||||
return renderVerticalCategory({...category, ...attributes, subcategories: subcategories});
|
||||
}
|
||||
}).join('');
|
||||
|
||||
return `<div style="position: relative;">${elements}</div>`;
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import LandscapeSelector from './LandscapeSelector';
|
||||
import { changeFilter } from '../reducers/mainReducer.js';
|
||||
import { options } from '../types/fields';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isBigPicture: state.main.mainContentMode !== 'card',
|
||||
value: state.main.filters.landscape,
|
||||
options: options('landscape')
|
||||
});
|
||||
const onChange = function(newValue) {
|
||||
return changeFilter('landscape', newValue);
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
onChange: onChange
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LandscapeSelector);
|
|
@ -0,0 +1,56 @@
|
|||
const { h } = require('../utils/format');
|
||||
const { assetPath } = require('../utils/assetPath');
|
||||
|
||||
module.exports.renderLandscapeInfo = function({width, height, top, left, children}) {
|
||||
children = children.map(function(info) {
|
||||
const positionStyle = `
|
||||
position: absolute;
|
||||
top: ${info.top}px;
|
||||
left: ${info.left}px;
|
||||
right: ${info.right}px;
|
||||
bottom: ${info.bottom}px;
|
||||
width: ${info.width}px;
|
||||
height: ${info.height}px;
|
||||
`;
|
||||
if (info.type === 'text') {
|
||||
return `<div key='text' style="
|
||||
${positionStyle}
|
||||
font-size: ${info.font_size * 4}px;
|
||||
font-style: italic;
|
||||
text-align: justify;
|
||||
z-index: 1;
|
||||
"><div style="
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 400%;
|
||||
height: 100%;
|
||||
transform: scale(0.25);
|
||||
transform-origin: left;
|
||||
"> ${h(info.text)} </div></div>`;
|
||||
}
|
||||
if (info.type === 'title') {
|
||||
return `<div key='title' style="
|
||||
${positionStyle}
|
||||
font-size: ${info.font_size}px;
|
||||
color: #666;
|
||||
">${h(info.title)}</div>`;
|
||||
}
|
||||
if (info.type === 'image') {
|
||||
return `<img src="${assetPath(`images/${info.image}`)}" style="${positionStyle}" alt="${info.title || info.image}" />`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
return `<div style="
|
||||
position: absolute;
|
||||
width: ${width}px;
|
||||
height: ${height - 20}px;
|
||||
top: ${top}px;
|
||||
left: ${left}px;
|
||||
border: 1px solid black;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
">${children}</div>`
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import TreeSelector from './TreeSelector';
|
||||
|
||||
|
||||
const LandscapeSelector = ({isBigPicture, value, options, onChange}) => {
|
||||
if (!isBigPicture) {
|
||||
return <TreeSelector value={value} options={options} onChange={onChange} />;
|
||||
} else {
|
||||
return (
|
||||
<Select
|
||||
disabled
|
||||
value="empty"
|
||||
style={{width:175 ,fontSize:'0.8em'}}
|
||||
>
|
||||
<MenuItem value="empty">
|
||||
<em>N/A</em>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
};
|
||||
export default pure(LandscapeSelector);
|
|
@ -1,17 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import TreeSelector from './TreeSelector';
|
||||
import { changeFilter } from '../reducers/mainReducer.js';
|
||||
import { options } from '../types/fields';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
value: state.main.filters.license,
|
||||
options: options('license')
|
||||
});
|
||||
const onChange = function(newValue) {
|
||||
return changeFilter('license', newValue);
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
onChange: onChange
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TreeSelector);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue