mirror of https://github.com/cncf/landscapeapp.git
Compare commits
1691 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 | |
|
e5ab6efd69 | |
|
28eb050557 | |
|
144b63b7a4 | |
|
0f66d6e640 | |
|
9a58d69c18 | |
|
d44d5a7d0d | |
|
631fcbfca6 | |
|
b67284cd2f | |
|
c9f33597f6 | |
|
47be574ef9 | |
|
4e98cfddc3 | |
|
fe29e8820e | |
|
1b370ba5c8 | |
|
013d5cddb3 | |
|
cd4c2c212e | |
|
fddbd126e5 | |
|
b73654b32d | |
|
2bde22b481 | |
|
5f3cf4b22e | |
|
3b474c69cf | |
|
2642219e0f | |
|
2bb09285c0 | |
|
9e23c85dbb | |
|
17ee06621b | |
|
d236e62764 | |
|
6052a61908 | |
|
15761cb93c | |
|
b8a6f5fba8 | |
|
e944ac0a33 | |
|
e5bb0b1785 | |
|
8cc7893384 | |
|
c7f5bc5964 | |
|
504e4bc191 | |
|
1d8d7bda9e | |
|
729a44880b | |
|
3536009e2e | |
|
83d6245ed0 | |
|
bfedb648b8 | |
|
a70a40d822 | |
|
68619fe0aa | |
|
51925ea0c1 | |
|
9daa4df629 | |
|
8d4f356abc | |
|
4f2ec7e824 | |
|
bfe4baf8df | |
|
72a80fa126 | |
|
bb6f65d17b | |
|
238d904851 | |
|
63834084d7 | |
|
cc0e1db6c3 | |
|
941cb479a3 | |
|
5639065ddf | |
|
272ae91a12 | |
|
1a558c8be8 | |
|
4f54468711 | |
|
0ea2609c69 | |
|
d68b369589 | |
|
14f8b9d352 | |
|
62d006f7c6 | |
|
c433796959 | |
|
d885def448 | |
|
acb38458cd | |
|
732be2b034 | |
|
5f49644435 | |
|
68fb9eabe4 | |
|
50fe0ecdd3 | |
|
adfac63435 | |
|
7c6bc21a8b | |
|
f018384720 | |
|
b591e6da62 | |
|
4912471754 | |
|
efe3640447 | |
|
88c2a79eba | |
|
82bb42b33d | |
|
d4e2655b98 | |
|
ff0435c7b1 | |
|
6a8dd50f19 | |
|
1ee1123c65 | |
|
d15428e79b | |
|
78a4d17f83 | |
|
84c810c2c7 | |
|
1e8b609490 | |
|
03a0dc747c | |
|
edd4e5e1da | |
|
b29fcca0f0 | |
|
c40106ce70 | |
|
0cd46d8e03 | |
|
24b18c6670 | |
|
5773dbe725 | |
|
360a7d67b7 | |
|
b50d2dbd22 | |
|
4be93d6d43 | |
|
585c2f8d9a | |
|
c1ea381ef5 | |
|
518b0b89a8 | |
|
3e02fc7ddc | |
|
035c2f44e1 | |
|
661404961e | |
|
616a1f97fd | |
|
f7d63087d6 | |
|
84723b13c2 | |
|
b3a09deeed | |
|
5caf7d1786 | |
|
f9e3fb9538 | |
|
43fe1f756b | |
|
302dcef106 | |
|
59acb07208 | |
|
68391ae848 | |
|
9ae818c6ac | |
|
6bdf00cc81 | |
|
c568fcaecf | |
|
ac6d7d12dc | |
|
10dc404bf6 | |
|
56a6fbb232 | |
|
9898b2845b | |
|
d3d9ef5fdc | |
|
1ff3d54298 | |
|
02cb3eb444 | |
|
4d749ade0a | |
|
f7f8dbb2c7 | |
|
34555b7cd7 | |
|
d7f780d771 | |
|
657fb59880 | |
|
f9685d8071 | |
|
b93c58af64 | |
|
34eecbfcce | |
|
5c2b123887 | |
|
0128fa3f70 | |
|
30ac70428e | |
|
1f0c6657a8 | |
|
f1cb07b159 | |
|
a226e9bc8a | |
|
5638df91d4 | |
|
000c8f4c96 | |
|
d56951be79 | |
|
dce953e0bb | |
|
41fdf486de | |
|
e00072cb80 | |
|
4fe1976c18 | |
|
58f31a97df | |
|
12b7191d72 | |
|
9de26ba618 | |
|
c40affdc54 | |
|
7624b12ffc | |
|
617183e460 | |
|
a33f32dafe | |
|
7aa1d03890 | |
|
af08a0b4ba | |
|
558292a733 | |
|
54f17571e3 | |
|
4bff29858f | |
|
f7b221ff78 | |
|
fe9a6796e4 | |
|
ae1bcb2a03 | |
|
3f2924954c | |
|
8032f3653c | |
|
246644f177 | |
|
a76273d5db | |
|
2699a8bc38 | |
|
919ea51223 | |
|
3de8010e9c | |
|
9161ca703d | |
|
ac3c3b7d2b | |
|
d5995e999d | |
|
03bc984ea3 | |
|
dbee3ab99a | |
|
09c23c3605 | |
|
7438813547 | |
|
0507df8f6f | |
|
691d433d3b | |
|
a421fb83ef | |
|
6848fca459 | |
|
c50775bc36 | |
|
a4c6be8899 | |
|
d29b92a024 | |
|
488b90c3ed | |
|
a276a2f100 | |
|
53c3bbe6f8 | |
|
b1e8eaa641 | |
|
529fd3ae8d | |
|
a119e3a352 | |
|
0a2a2478bb | |
|
70a14e0f87 | |
|
85a97da2da | |
|
67449e947d | |
|
fa48070766 | |
|
b6dd108602 | |
|
a768031fa1 | |
|
73da8ee445 | |
|
8d85ce183f | |
|
aebb769a54 | |
|
431e7e42c7 | |
|
93040a0082 | |
|
b9961e5e37 | |
|
8d64943ee4 | |
|
3b59d454d6 | |
|
99cadae614 | |
|
a431795c2e | |
|
415240e841 | |
|
e75a431ae4 | |
|
7a071576e2 | |
|
d594a3fd1c | |
|
b6ee951e62 | |
|
12268c6da6 | |
|
da7a5acf5c | |
|
09291b3b26 | |
|
17ad97f044 | |
|
6f7cc00f46 | |
|
2bba6349ac | |
|
1146a74be6 | |
|
2669431ad2 | |
|
88e3a23680 | |
|
a5c8025f1a | |
|
41d93ca3f6 | |
|
4051911c63 | |
|
eb4df8a5d0 | |
|
9cae78a820 | |
|
0cac8d58ce | |
|
66a5463f04 | |
|
c18bc2741a | |
|
ec1404ba56 | |
|
797f3499ee | |
|
e2a6ac2600 | |
|
3162f0c58a | |
|
965268de04 | |
|
c7b7d0d173 | |
|
692332d061 | |
|
4fe741ec24 | |
|
5a31d1a4fe | |
|
f851cf5095 | |
|
c4cbe0aff9 | |
|
7512c7e83c | |
|
6583f7e2b3 | |
|
f18f305c77 | |
|
4135c41a92 | |
|
3b408368c7 | |
|
9e40968809 | |
|
7c86b56b28 | |
|
7d6b07675f | |
|
9db9fc43d2 | |
|
509c360937 | |
|
42f7d11e69 | |
|
185f41578c | |
|
d600513e92 | |
|
c438d600c9 | |
|
8476c85e97 | |
|
4270549acc | |
|
29d9e138ff | |
|
32a7104e84 | |
|
c1f0c658cf | |
|
d53e3cc1b0 | |
|
716e279e8e | |
|
3407d782dd | |
|
41b65cd6fc | |
|
1bdb74e303 | |
|
6de5a32562 | |
|
ac50fec2ff | |
|
912a31c782 | |
|
1d179bb28a | |
|
367dc92b19 | |
|
dcf1ee220d | |
|
66e4c46c99 | |
|
497493b47f | |
|
f9674053d1 | |
|
2784a6678d | |
|
eb2eae0b2a | |
|
bf8ec07b7b | |
|
fe1da0733d | |
|
afa0244bb2 | |
|
cd794888a5 | |
|
c8fd20f194 | |
|
7cb08e98b9 | |
|
14aa4d88d5 | |
|
4e68943786 | |
|
24483cc177 | |
|
c2a8f24179 | |
|
b70237268d | |
|
5ec2f1c167 | |
|
9c4c018471 | |
|
e89824d668 | |
|
07e8ad2038 | |
|
15a56a1497 | |
|
02d6512264 | |
|
4fe37c0678 | |
|
1ca7fa15ad | |
|
68da37ac11 | |
|
d1c2521052 | |
|
0d5d2ca502 | |
|
ff5beef01d | |
|
bf90875271 | |
|
d97c52b99a | |
|
d6287489cb | |
|
434e7e0519 | |
|
0484acf5b6 | |
|
5dd68f60e0 | |
|
f0ca70f387 | |
|
0e53dac121 | |
|
f9c9b2afd7 | |
|
5d655eff64 | |
|
cddf8efafd | |
|
aa6933fafb | |
|
d34ce88d90 | |
|
1fd979d4a6 | |
|
203525c0b7 | |
|
444d0f3056 | |
|
b8faba471d | |
|
b0843f7981 | |
|
6e9edfb253 | |
|
6d22f31244 | |
|
503fd08d90 | |
|
f73466e435 | |
|
a85abcddd7 | |
|
333a73b0fa | |
|
ef2a17f399 | |
|
c9c407aebb | |
|
9b8750bb08 | |
|
9873f96d12 | |
|
1e464f179a | |
|
7e1d2ad9a3 | |
|
c5dc1d9226 | |
|
a909f3fcff | |
|
dce76fce1e | |
|
3f1ef1ca6b | |
|
06c7e36fa9 | |
|
e7b5c43828 | |
|
a02972ed76 | |
|
d892972f2f | |
|
d5ae3019fa | |
|
4b0add41bf | |
|
19806cfe15 | |
|
515050e4fb | |
|
d56859f320 | |
|
d801d3912b | |
|
3ed49d18b9 | |
|
fbd8f233be | |
|
2f3796faf5 | |
|
65dd9e13b3 | |
|
f246cfdd66 | |
|
3a22df0971 | |
|
f6a96c83e5 | |
|
068c9e0ac1 | |
|
5934f2a796 | |
|
b45e965720 | |
|
76a1361acf | |
|
ce7ee271e4 | |
|
bc914d252d | |
|
4c01a094d3 | |
|
4f666cb375 | |
|
abb6086791 | |
|
f0c111cfef | |
|
bd789f2a1c | |
|
42fcb250f5 | |
|
f14554b925 | |
|
7ddde544ef | |
|
fcf8454e4c | |
|
be53f43530 | |
|
82d0f01b31 | |
|
8949aae905 | |
|
00c9f976c5 | |
|
bf3cfd894b | |
|
1bd50cba34 | |
|
d565505f90 | |
|
1b7fb0833b | |
|
848f5c0ef5 | |
|
58960da251 | |
|
a958f02a11 | |
|
a24552fc9f | |
|
9f1dc2c6d5 | |
|
f331d92eea | |
|
c08f0b754b | |
|
bb447e8565 | |
|
d6a3509eb8 | |
|
4682993f30 | |
|
27e648a56c | |
|
065d9bafa6 | |
|
6517503207 | |
|
d43ebd8cf2 | |
|
66c206f46d | |
|
40da04b937 | |
|
3c5735930f | |
|
12dcc98c1e | |
|
af85df80a4 | |
|
d765d01759 | |
|
05f31f8df1 | |
|
b920840be9 | |
|
66519be18b | |
|
17ca3ccfdd | |
|
5920482ce0 | |
|
09b413ea2a | |
|
1fb3381f5c | |
|
b7af4121c3 | |
|
3a5e7d951d | |
|
1c00c1ca35 | |
|
4a4a68defe | |
|
c2f1e059c0 | |
|
df9b613565 | |
|
1383b39b4f | |
|
4fd7c04ecf | |
|
c7cfb0f524 | |
|
c0a2c1967c | |
|
4cd9412d02 | |
|
b63dc269c7 | |
|
f16368c662 | |
|
1499f48180 | |
|
f01e5c8d71 | |
|
22501a9f69 | |
|
af417ab5ab | |
|
687247b25c | |
|
79c59ff111 | |
|
b894c55c4b | |
|
8638288490 | |
|
3873e1f4fd | |
|
58fe02891f | |
|
04fa42eaff | |
|
ab4baa0244 | |
|
f86b05bceb | |
|
77d2fc4ec9 | |
|
155118c2ad | |
|
f57b88185f | |
|
d92468a658 | |
|
1e47799993 | |
|
2b6e063292 | |
|
72eb59168b | |
|
7f3439a7c7 | |
|
4a59d6547c | |
|
9c43d33835 | |
|
b760175958 | |
|
f99593ef5b | |
|
f187600f36 | |
|
6834063ac0 | |
|
7edfab34c7 | |
|
af1b05377b | |
|
e9d0ab1930 | |
|
2fbe61cfaa | |
|
5179c04555 | |
|
61af7ad722 | |
|
bfc921efbb | |
|
9cfff1faf7 | |
|
68cddbbac5 | |
|
6700f629f5 | |
|
79112af641 | |
|
5119f435fc | |
|
bce6d90e6b | |
|
38547a5178 | |
|
52352d3d60 | |
|
349427322e | |
|
29259bd6f7 | |
|
f2cbfe7c6c | |
|
5c221d6c4d | |
|
240a9874f6 | |
|
fcb6d102fa | |
|
3582fc3db7 | |
|
f3a1257322 | |
|
3f8b3d3ca4 | |
|
89b1e8676a | |
|
e6e09b0209 | |
|
5b58de4d7e | |
|
c0d3939fe8 | |
|
ae6415d054 | |
|
63bca77074 | |
|
f376fa3d17 | |
|
6ec1d82eb8 | |
|
ec63c2ed70 | |
|
db3b737f3e | |
|
e1c1aa147d | |
|
c089f132ab | |
|
7b690a906d | |
|
c33714d5f4 | |
|
d5f6fa85fd | |
|
3aee2c7798 | |
|
21c8f3a17a | |
|
7a63501d89 | |
|
7850696914 | |
|
8b82903eae | |
|
bb32c95d09 | |
|
ce3f0b4708 | |
|
0e2db1ddf3 | |
|
3da0395fa1 | |
|
9c321dfc4a | |
|
329f815019 | |
|
70097376dc | |
|
3f8a88f1d2 | |
|
5802fffc30 | |
|
7dde9ce63c | |
|
ae9128c84a | |
|
e622ed6787 | |
|
b998672b58 | |
|
e56f293f32 | |
|
e9c7ae14a2 | |
|
82fd518b3b | |
|
5e20a216ab | |
|
d3f82b1962 | |
|
97cf8c22f3 | |
|
c31bba7ba7 | |
|
53439c2b9f | |
|
d1f500d6e4 | |
|
7c3526c2c7 | |
|
05d6f48b8c | |
|
795cea3022 | |
|
67a39fe733 | |
|
a4303f71cd | |
|
f39da532aa | |
|
bb4d4366d1 | |
|
59bd73d0d4 | |
|
566f0afb37 | |
|
dd7a3095df | |
|
b8eb2661b3 | |
|
008e36ba93 | |
|
52333fba85 | |
|
560e474690 | |
|
44d3c1b596 | |
|
a40ce894c9 | |
|
6063276daa | |
|
a452feea5e | |
|
81402f200b | |
|
fc95ab5eb2 | |
|
ef24727335 | |
|
88d82396a3 | |
|
d99c569544 | |
|
85d6bd0ea3 | |
|
f86af3798e | |
|
660eef6717 | |
|
ea623493be | |
|
33211997f2 | |
|
da98d5bc6f | |
|
d97ce6c3d1 | |
|
d4561cbf3a | |
|
293a233926 | |
|
4f779860c5 | |
|
540b418e7e | |
|
680996bc20 | |
|
90c8ec1a7a | |
|
5e6c407858 | |
|
f4ecc1f9a3 | |
|
6c025ed87f | |
|
95e98adb4e | |
|
0bd906bfc2 | |
|
0c06c8d549 | |
|
f34384c289 | |
|
fccd0ca0f7 | |
|
a7ab1a9745 | |
|
6735bc1124 | |
|
38ad3156ca | |
|
1654d81b3b | |
|
93fbaee70a | |
|
e230fbe83c | |
|
062a80e240 | |
|
c8b45ca073 | |
|
6d9acea214 | |
|
0de8785469 | |
|
1c387b7d49 | |
|
852aa64736 | |
|
d38a413284 | |
|
156ff3b064 | |
|
75397fa09b | |
|
03b9346b29 | |
|
4883676a78 | |
|
1f0ca842d4 | |
|
2e3b1e6bcf | |
|
f6b3207957 | |
|
b6ac5cf918 | |
|
36f0a39150 | |
|
bd4ee39e59 | |
|
74baf83044 | |
|
1ca71a64c4 | |
|
0f9f43fdda | |
|
14c6811bf3 | |
|
46ce2f9e12 | |
|
296d72b251 | |
|
2a4c234ef6 | |
|
44903822d7 | |
|
ba4bd5c75d | |
|
4f466ba347 | |
|
f05898425d | |
|
46ff64600a | |
|
edbdbee647 | |
|
0512e7380f | |
|
780f1430fc | |
|
1034a4edc7 | |
|
03e2b66234 | |
|
8b91facb66 | |
|
46ff98fd9f | |
|
9ba5d0306e | |
|
1428b7865c | |
|
a48bc124cf | |
|
337b07d381 | |
|
3312f386e8 | |
|
de80939613 | |
|
9a2af23681 | |
|
c776bcfc55 | |
|
f8fd81f4be | |
|
f92178ef6a | |
|
8b9da28907 | |
|
354bb89cc4 | |
|
a56e42d4c9 | |
|
f42e49b272 | |
|
7953984149 | |
|
a874101716 | |
|
a8a527737c | |
|
0c210ac0f0 | |
|
9bea52b905 | |
|
ee5f1c4669 | |
|
9e3c23be51 | |
|
af2f8e3adf | |
|
143ea20869 | |
|
92da5608d6 | |
|
5cf29ed9a9 | |
|
ee9fbaeee2 | |
|
dc60e5167a | |
|
365f70f96d | |
|
5b5eebaaa0 | |
|
f7fff53599 | |
|
5daa64d81b | |
|
8b5cac88ef | |
|
4fc2254cff | |
|
95f5aa37d8 | |
|
524557e642 | |
|
6b07f2762b | |
|
cb886da3f3 | |
|
20ea9bb930 | |
|
ad61754def | |
|
88a3d7d15e | |
|
c2f3a3e879 | |
|
c8662d02f9 | |
|
0503872e54 | |
|
9be80af33b | |
|
0932b023c8 | |
|
a3f0d93b8b | |
|
c29e53a528 | |
|
ffe89f8991 | |
|
edfbbfb5ff | |
|
b4e8a9a200 | |
|
b2cfa4d45b | |
|
8b23877816 | |
|
261735813f | |
|
7ab8544bc1 | |
|
63c672eb98 | |
|
c61514c9be | |
|
ff1ec98164 | |
|
1f2b46611f | |
|
2f300f80e3 | |
|
41ada8be8d | |
|
09b4000571 | |
|
bded359ea0 | |
|
edaba47e34 | |
|
5be27cd28b | |
|
d85119f9f5 | |
|
7b9f742a59 | |
|
ca8e0ef194 | |
|
52cefac8d1 | |
|
70e885a8c8 | |
|
0127dee702 | |
|
06bdcf0096 | |
|
c683f55e8a | |
|
dae4aa98fd | |
|
08905f0a9a | |
|
f540eb7d86 | |
|
2b9402f723 | |
|
693817db0b | |
|
e45bc657e0 | |
|
ed3f86a4c9 | |
|
410bf69a62 | |
|
4bbca02712 | |
|
09c0985da1 | |
|
7e14ce8c73 | |
|
8138ba80f2 | |
|
21748fff2b | |
|
45c3cdc697 | |
|
bbb89d5036 | |
|
c6f2b722bf | |
|
9158bd0cd4 | |
|
93cb27a39c | |
|
eef9bdc669 | |
|
46bf6be242 | |
|
a3cf113e0c | |
|
144b8fee34 | |
|
99f6861262 | |
|
24674afc6f | |
|
07959b8236 | |
|
e27b5d2e53 | |
|
c7b0a4ae54 | |
|
ee0bddc479 | |
|
7cf709ff94 | |
|
921d002ea7 | |
|
6b04201394 | |
|
745a090ced | |
|
bb38b93346 | |
|
0bd0719054 | |
|
fde0adf889 | |
|
d253125312 | |
|
fa6c04c736 | |
|
1919dd7d01 | |
|
74d0bfaf6d | |
|
fb3ab08c40 | |
|
e7bf65399b | |
|
b430b4d5df | |
|
0dd3b7ebdc | |
|
ca642b34a9 | |
|
0238829c66 | |
|
ee7040c5bb | |
|
fdabd9ac17 | |
|
a5a76fdf58 | |
|
c1b4aade1d | |
|
fb3c0f6829 | |
|
6cb1e7f581 | |
|
41be9b73a6 | |
|
de3fefaf9c | |
|
beb7045a4b | |
|
0ad874770e | |
|
aea8409f61 | |
|
aff9245e11 | |
|
8d234d7236 | |
|
f6aad80432 | |
|
97d22e47c9 | |
|
47a6ddc517 | |
|
e9bca7e9f9 | |
|
88d7e81151 | |
|
41d8380e08 | |
|
50ab91e110 | |
|
13cdf8b6dd | |
|
81cd5875e1 | |
|
b92eff77cf | |
|
9fdf3afec9 | |
|
f5cb571f2c | |
|
9d46f89fd5 | |
|
faebe8fe09 | |
|
75aa4300aa | |
|
76e06e8235 | |
|
e5ede7ee48 | |
|
b52e9e2ead | |
|
4264a4c1a9 | |
|
3272c4ff8c | |
|
aec5d9402d | |
|
68eec8c933 | |
|
4bd724f230 | |
|
16c00835b8 | |
|
2b90806e0c | |
|
d42770681b | |
|
8456a554d4 | |
|
fcae976b16 | |
|
41369c8b12 | |
|
e560dea9ff | |
|
1e268f7678 | |
|
2e98e3ed73 | |
|
4cb0d7d948 | |
|
41424d590c | |
|
282c267755 | |
|
e169cf9538 | |
|
ebbe30a177 | |
|
d0a03bd5e3 | |
|
c6aa2d0f33 | |
|
7bdea34a5b | |
|
3cd876c5d8 | |
|
7dfb71e0e7 | |
|
367dd3ed7b | |
|
522b02b981 | |
|
32e9405766 | |
|
976bd2898d | |
|
9b80dd2bba | |
|
d1e90715e9 | |
|
df670f3be0 | |
|
fecc133e7a | |
|
c69d59e64c | |
|
1b9fcdd0cb | |
|
5a91c2fc8b | |
|
94e281777a | |
|
bdabd1e833 | |
|
43856f2159 | |
|
b8d1f2f74d | |
|
736eb0714b | |
|
6bd8dba853 | |
|
7f277b88c8 | |
|
6c11d551d5 | |
|
6852f45ee8 | |
|
cd4b2bae69 | |
|
32973757ba | |
|
1a20daf0ab | |
|
0f8196a8fe | |
|
f35b69b448 | |
|
cb0f4a83b0 | |
|
3bf56d7f2d | |
|
2b063df4a3 | |
|
bd7bf326f8 | |
|
16a2db6e5d | |
|
bdebab2779 | |
|
d5f4cf144b | |
|
04d329fb74 | |
|
50b782e232 | |
|
f152ecb804 | |
|
303146fc2e | |
|
6b6140d0c2 | |
|
73c6997e16 | |
|
525271fd3b | |
|
e68c7ae677 | |
|
268aac5ada | |
|
a1e1316cd1 | |
|
9bc14b581e | |
|
54a7fcb264 | |
|
684a760feb | |
|
296c816b93 | |
|
c485f450d6 | |
|
4a219e447e | |
|
c50a706b9e | |
|
17be0a9132 | |
|
293de7756e | |
|
659faed16b | |
|
5d7acd1569 | |
|
182d7e3e74 | |
|
0f43214541 | |
|
731ded3e67 | |
|
83d49692a1 | |
|
e07d13fe7d | |
|
bbc826d71d | |
|
efaa77b589 | |
|
9849e90bd2 | |
|
cf1ea731c5 | |
|
ed0792ef55 | |
|
38da8f535e | |
|
a58d4d9c93 | |
|
2194ac1d77 | |
|
9c2b54cecb | |
|
f6e1c1a445 | |
|
751722880c | |
|
d230ecf37a | |
|
4b3b1c3ad1 | |
|
dd10b4f55b | |
|
12e17046c3 | |
|
8e3ca2e7f5 | |
|
13bd5f38d0 | |
|
cd6d09fd9e | |
|
5f31617661 | |
|
57904dfc6f | |
|
f57a39eac7 | |
|
a8ba03aaec | |
|
ed48279265 | |
|
d0f5663756 | |
|
01b31fb0a0 | |
|
2109d2908b | |
|
8519bbe24f | |
|
98d4944ee5 | |
|
1dc74e2ad3 | |
|
01184087f5 | |
|
3f43fbfd43 | |
|
21e781aae8 | |
|
301f098ae1 | |
|
c8eed5abc6 | |
|
25442028d9 | |
|
bc52adc340 | |
|
5962fcd7a6 | |
|
23dce75bb1 | |
|
054e1db510 | |
|
b123e94808 | |
|
811f0a5dee | |
|
37eba15e5f | |
|
7bd80695cc | |
|
001dd6247f | |
|
bd7d198e1e | |
|
07752902f3 | |
|
1cbf0fbf90 | |
|
b25e0a5b72 | |
|
5376f8a30b | |
|
186bb06aba | |
|
cf5c761365 | |
|
9428cfb198 | |
|
d5df13471d | |
|
cb394f8bd8 | |
|
9c81809b6b | |
|
3838617889 | |
|
b1ecee6df6 | |
|
82fbacc06a | |
|
b383992db3 | |
|
609de9f107 | |
|
b4157acc8a | |
|
583ea4442e | |
|
0d41750975 | |
|
9293eba9f5 | |
|
9d63dc29a4 | |
|
383845a08b | |
|
24ea4e3952 | |
|
ec589362d6 | |
|
4267e186ac | |
|
62c16fa823 | |
|
f2dc5720d7 | |
|
b6ef518c18 | |
|
059dc29f8c | |
|
8dec3c31e5 | |
|
99a0f49b0f | |
|
20e443a0cc | |
|
76b4b255ef | |
|
a32a41d51f | |
|
aab5baa476 | |
|
7f557df384 | |
|
26d6c46019 | |
|
c2055430f5 | |
|
8403b0b6c4 | |
|
8d78a897b5 | |
|
eaded49a19 | |
|
5d52fe348b | |
|
30827fb2c1 | |
|
c98431ef41 | |
|
dfdbd67991 | |
|
8645c07165 | |
|
df4d5a509d | |
|
120e48ccdf | |
|
f005302f51 | |
|
89d8f4a049 | |
|
79c8b2ce04 | |
|
7dea886935 | |
|
3cd19f9a8f | |
|
601b726d61 | |
|
310469f203 | |
|
9c4c84ec63 | |
|
325319e279 | |
|
3539bfaaa5 | |
|
b2c3c9fea4 | |
|
3a2295568f | |
|
dff56eedaa | |
|
91795e3d5f | |
|
e9a596c08c | |
|
9eaea14e00 | |
|
cdbde95525 | |
|
1b6b76746a | |
|
781ac6c6b4 | |
|
24619dd5f1 | |
|
c839666b28 | |
|
b2ea8586b5 | |
|
149eef6613 | |
|
bd716d5b67 | |
|
8cbde49de2 | |
|
2eab53e900 | |
|
c8e346f209 | |
|
42e2e79189 | |
|
64a817b871 | |
|
8df911ef8d | |
|
903c7c9e53 | |
|
622f4c6fa5 | |
|
66489e75a6 | |
|
cf462a61cd | |
|
d67ae434b0 | |
|
17c495f518 | |
|
b0fd649bef | |
|
6e84553968 | |
|
be1eee385f | |
|
7cdd96995c | |
|
7b16c0ed1b | |
|
1fbcf137c0 | |
|
53c32ba945 | |
|
9f59f726fb | |
|
7e3dd29383 | |
|
87755f8760 | |
|
237bbe374f | |
|
3747f8b999 | |
|
7dd3bb44fa | |
|
9ac84eac63 | |
|
859c159054 | |
|
7c94fbfc47 | |
|
7274170ab4 | |
|
c07b4b3d94 | |
|
99e27d08a9 | |
|
c298e591df | |
|
1dc71fcf6b | |
|
f32516f983 | |
|
85c96f1580 | |
|
1d86fe8a63 | |
|
c2dc2dc947 | |
|
71371298ba | |
|
b5c2921387 | |
|
244d873d7a | |
|
e117dc6215 | |
|
475d4b2f1e | |
|
1200b3d796 | |
|
403b5a60f4 | |
|
b062cfa84b | |
|
10be41b3df | |
|
f15d890dfc | |
|
9d6cb8be6d | |
|
fb40ae08ab | |
|
b36ba23037 | |
|
20a2bb0375 | |
|
7363457c38 | |
|
909695d83a | |
|
c8f76cac53 | |
|
7367af5d72 | |
|
6c54e3fdcc | |
|
a0e06c55d9 | |
|
4c61916fea | |
|
7b99e62879 | |
|
857afffc16 | |
|
627adcd57a | |
|
0cdc1e25d5 | |
|
437abac28d | |
|
e7cb6ec945 | |
|
c4a9f1c216 | |
|
addc1a3876 | |
|
d1b593c1b1 | |
|
fcfaab25bf | |
|
cd319a3f36 | |
|
7dea389dc6 | |
|
e165464a59 | |
|
9d508d6837 | |
|
6c84caa945 | |
|
858a09475d | |
|
9867beba7d | |
|
56484c75e5 | |
|
3cc323f877 | |
|
ed6f180b9c | |
|
ef1a3b6c74 | |
|
a2cc207e1f | |
|
d782aecf68 | |
|
ab7bc4ed86 | |
|
a4a40c77c8 | |
|
76d89541f0 | |
|
ab4e91c7ef | |
|
7a88ae8582 | |
|
f5b4d17dbe | |
|
f73edef48e | |
|
9122049ae6 | |
|
3f675e1a43 | |
|
fd4b8fd7c2 | |
|
93fb39b643 | |
|
8464154a6e | |
|
20585dbe45 | |
|
577d16aaf3 | |
|
5ec13fb3be | |
|
0ca5fd8e14 | |
|
856f018c0b | |
|
42ee7bce44 | |
|
0773bbefe9 | |
|
9a82eda500 | |
|
cc7e704232 | |
|
910c63ea95 | |
|
e65e608d7b | |
|
1cebd7cd18 | |
|
b2756d7d05 | |
|
3a636b3282 | |
|
77098bc887 | |
|
db170cacb0 | |
|
7226658183 | |
|
5dcfae1646 | |
|
2f986a4f6e | |
|
24b574dd53 | |
|
0e08a4ea8a | |
|
46e1856381 | |
|
c44943c06d | |
|
14192dff5b | |
|
c44f69f9a3 | |
|
3c0ed76b4d | |
|
8034a369d4 | |
|
4c7ef8aa7c | |
|
06e8d6e517 |
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
### Don't create an issue
|
||||
### This is the upstream landscapeapp
|
||||
|
||||
If you see an error in the cards or missing ones, please don't create an issue here. Instead, edit landscape.yml (you can even do it via the GitHub web interface) and open a pull request. Then, review the preview staging server that's posted to your pull request and add a comment if your new or updated card looks correct and is ready to merge. Before going forward, please carefully review the sections of the Readme covering new entries, corrections, and logos: https://github.com/cncf/landscape/blob/master/README.md.
|
||||
If you want to modify the content in the CNCF or LFAI landscapes, please make changes to those downstream repos:
|
||||
* https://github.com/cncf/landscape
|
||||
* https://github.com/lfai/lfai-landscape
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
### Pre-submission checklist:
|
||||
|
||||
*Please check each of these after submitting your pull request:*
|
||||
|
||||
* [ ] Are you only including a `repo_url` if your project is 100% open source? If so, you need to pick the single best GitHub repository for your project, not a GitHub organization.
|
||||
* [ ] Is your project closed source or, if it is open source, does your project have at least 250 GitHub stars?
|
||||
* [ ] Have you picked the single best (existing) category for your project?
|
||||
* [ ] Does it follow the other guidelines from the [new entries](https://github.com/cncf/landscape#new-entries) section?
|
||||
* [ ] Have you included a URL for your SVG or added it to `hosted_logos` and referenced it there?
|
||||
* [ ] Does your logo clearly state the name of the project/product and follow the other logo [guidelines](https://github.com/cncf/landscape#logos)?
|
||||
* [ ] Does your project/product name match the text on the logo?
|
||||
* [ ] Have you verified that the Crunchbase data for your organization is correct (including headquarters and LinkedIn)?
|
||||
* [ ] ~10 minutes after opening the pull request, the CNCF-Bot will post the URL for your staging server. Have you confirmed that it looks good to you and then added a comment to the PR saying "LGTM"?
|
|
@ -28,6 +28,8 @@ node_modules
|
|||
#dist folder
|
||||
dist
|
||||
|
||||
public
|
||||
|
||||
# IDEA/Webstorm project files
|
||||
.idea
|
||||
*.iml
|
||||
|
@ -39,4 +41,14 @@ dist
|
|||
.DS_Store
|
||||
|
||||
/cncf
|
||||
/lfdl
|
||||
/lfai
|
||||
|
||||
# Yarn, without .pnp.js
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
.pnp.*
|
||||
|
||||
.next
|
||||
out
|
||||
tmp
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,3 @@
|
|||
enableProgressBars: false
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
|
@ -0,0 +1,23 @@
|
|||
## Cloud Native Landscape
|
||||
|
||||
[](https://landscape.cncf.io/images/landscape.png)
|
||||
|
||||
## Serverless Landscape
|
||||
|
||||
[](https://landscape.cncf.io/images/serverless.png)
|
||||
|
||||
## CNCF Member Landscape
|
||||
|
||||
[](https://landscape.cncf.io/images/members.png)
|
||||
|
||||
## LF Artificial Intelligence Landscape
|
||||
|
||||
[](https://landscape.lfai.foundation/images/landscape.png)
|
||||
|
||||
## ASWF Landscape
|
||||
|
||||
[](https://landscape.aswf.io/images/landscape.png)
|
||||
|
||||
## Open Mainframe Landscape
|
||||
|
||||
[](https://landscape.openmainframeproject.org/images/landscape.png)
|
23
INSTALL.md
23
INSTALL.md
|
@ -2,28 +2,21 @@
|
|||
|
||||
## Install on Mac
|
||||
1. Install [Homebrew](https://brew.sh/)
|
||||
2. `brew install node yarn`
|
||||
2. `brew install node`
|
||||
3. `git clone git@github.com:cncf/landscape.git`
|
||||
|
||||
## Install on Linux
|
||||
1. `git clone git@github.com:cncf/landscape.git`
|
||||
2. Please follow [this script](https://github.com/cncf/landscape/blob/master/update_server/setup.bash) to install correct versions of `yarn`, `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. `yarn` (installs dependencies)
|
||||
* `yarn open:src` (starts a development server) or
|
||||
* `yarn build`, then `yarn open:dist` (compiles and opens a production build)
|
||||
|
||||
## Review build details
|
||||
1. `yarn 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 `yarn fetch` to fetch any needed data and generate [processed_landscape.yml](processed_landscape.yml) and [data.json](https://github.com/cncf/landscape/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).
|
||||
|
||||
`yarn 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:
|
||||
`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:
|
||||
|
||||
| Data cached | easy | medium | hard | complete |
|
||||
|------------------------|--------|----------|--------|------------|
|
||||
|
@ -37,9 +30,9 @@ After making your changes to `landscape.yml`, run `yarn fetch` to fetch any need
|
|||
* **Hard** also fetches GitHub start dates. These should not change so this should not be necessary.
|
||||
* **Complete** also re-fetches all images. This is problematic because images tend to become unavailable (404) over time, even though the local cache is fine.
|
||||
|
||||
Easy mode (the default) is what you should use if you update `landscape.yml` and want to see the results locally. The Netlify build server runs easy mode, which makes it possible to just commit a change to landscape.yml and have the results reflected in production. Run with `yarn fetch`.
|
||||
Easy mode (the default) is what you should use if you update `landscape.yml` and want to see the results locally. The Netlify build server runs easy mode, which makes it possible to just commit a change to landscape.yml and have the results reflected in production. Run with `npm run fetch`.
|
||||
|
||||
Medium mode is what is run by the update server, with commits back to the repo. It needs to be run regularly to update last commit date, stars, and Crunchbase info. Run with `yarn update`.
|
||||
Medium mode is what is run by the update server, with commits back to the repo. It needs to be run regularly to update last commit date, stars, and Crunchbase info. Run with `npm run update`.
|
||||
|
||||
Hard and complete modes should be unnecessary except in cases of possible data corruption. Even then, it's better to just delete any problematic entries from `processed_landscape.yml` and easy mode will recreate them with up-to-date information.
|
||||
|
||||
|
@ -50,6 +43,6 @@ If you can't find the right logo on the web, you can create a custom one and hos
|
|||
1. Save the logo to `src/hosted_logos/`, for example, `src/hosted_logos/apex.svg`. Use lowercase spinal case (i.e., hyphens) for the name.
|
||||
1. Update landscape.yml, for example, `logo: ./src/hosted_logos/apex.svg`. The location must start with`./src/hosted_logos`.
|
||||
1. If you've updated the local logo since a previous commit, you need to delete the cached version in `src/logos/`. E.g., delete `src/logos/apex.svg`.
|
||||
1. Update `processed_landscape.yml` with `yarn fetch`.
|
||||
1. Update `processed_landscape.yml` with `npm run fetch`.
|
||||
1. Commit and push. Double-check your work in the Netlify preview after opening a pull request.
|
||||
|
||||
|
|
383
README.md
383
README.md
|
@ -1,5 +1,380 @@
|
|||
Landscape App
|
||||
# Landscapeapp
|
||||
|
||||
Local Development: every script and webpack thinks that project specific data is
|
||||
stored in a $PROJECT_PATH variable, thus use that command for a development
|
||||
export PROJECT_PATH=/Users/user/Documents/cncf
|
||||
[](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)
|
||||
|
||||
- [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).
|
||||
|
||||
## Adding and managing landscape entries
|
||||
|
||||
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.
|
||||
|
||||
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 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 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.
|
||||
|
||||
##### 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
|
||||
1. Export file by going to File > Export > Export As in top menu
|
||||
1. Select SVG from the format drop down and make sure that "Use Artboards" is checked
|
||||
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
|
||||
|
||||
##### Inkscape
|
||||
|
||||
1. Select the text
|
||||
1. Ctrl+K (path combine)
|
||||
1. Ctrl+J (dynamic offset)
|
||||
1. Save
|
||||
|
||||
### 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:
|
||||
|
||||
* Project info from GitHub
|
||||
* Funding info from [Crunchbase](https://www.crunchbase.com/)
|
||||
* Market cap data from Yahoo Finance
|
||||
* CII Best Practices Badge [data](https://bestpractices.coreinfrastructure.org/)
|
||||
|
||||
The update server enhances the source data with the fetched data and saves the result in `processed_landscape.yml` and as `data.json`, the latter of which is what the app loads to display data.
|
||||
|
||||
## Creating a New Landscape
|
||||
|
||||
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.
|
||||
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:
|
||||

|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
```sh
|
||||
export CRUNCHBASE_KEY_4="key-here"
|
||||
export TWITTER_KEYS=keys-here
|
||||
export GITHUB_KEY=key-here
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
So, if you're in a directory called `dev`, you would do:
|
||||
```sh
|
||||
dev$ git clone git@github.com:cncf/landscapeapp.git
|
||||
dev$ git clone git@github.com:cdfoundation/cdf-landscape.git
|
||||
dev$ cd landscapeapp
|
||||
dev$ npm install -g yarn@latest
|
||||
dev$ yarn
|
||||
```
|
||||
|
||||
### 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 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.
|
||||
|
||||
## Vulnerability reporting
|
||||
|
||||
Please open an [issue](https://github.com/cncf/landscapeapp/issues/new) or, for sensitive information, email info@cncf.io.
|
||||
|
||||
## Continuous Integration and NPM Publishing
|
||||
|
||||
We have a sophisticated build system.
|
||||
We build this landscapeapp repo together with every landscape after each commit
|
||||
to the landscapeapp. A list of landscapes is stored in the landscapes.yml
|
||||
An individual landscape is built on a PR to that landscape.
|
||||
|
||||
Details about building a repo on netlify:
|
||||
|
||||
### Building an individual landscape
|
||||
|
||||
To build an individual landscape, we use Netlify. Netlify has certain
|
||||
issues with the performance and their caching algorithm is ineffective, thus in
|
||||
order to produce the fastest build, these steps are done
|
||||
|
||||
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 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
|
||||
own build server. The problem is that Netlify uses very slow and cheap amazon
|
||||
virtual machines, while our build server has a lot of CPUs and enough of RAM, that
|
||||
allows further parallelization during build steps.
|
||||
|
||||
#### Running "remotely" on our build server (fast and by default)
|
||||
|
||||
When an environment variable BUILD_SERVER is set, the following steps will occur:
|
||||
- the interactive-landscape package of the latest version is downloaded from npm
|
||||
- a current checkout of an individual landscape with a `landscapeapp` in a
|
||||
`package` folder is rsynced and sent to our build server
|
||||
- we use a hash of .nvmrc + package.json + npm-shrinkwrap.json from the
|
||||
`landscapeapp` repo as a key to cache `node_modules`, `~/.nvm` and `~/.npm` folders,
|
||||
this way if the hash has not changed - we reuse existing node_modules without any
|
||||
setup
|
||||
- if a hash is different, we install node_modules and cache `~/.nvm`,
|
||||
`~/.npm` and `node_modules` for further usage
|
||||
- finally, we run a build on our remote server via ssh, and when the build is
|
||||
done, the output is returned via rsync
|
||||
|
||||
Those extra steps allow us to run a build faster because we avoid an `npm install` step
|
||||
almost every time and extra RAM and CPU allow running npm tasks `renderLandscape`,
|
||||
`checkLandscape` and `jest` in parallel.
|
||||
|
||||
Still, if for certain reasons, remote solution stopped to work and we need to
|
||||
restore the Netlify build process as soon as possible, BUILD_SERVER variable
|
||||
should be set to empty in either a given landscape or in a shared variables
|
||||
section. Usually, the build will fail for all the landscapes, thus renaming the
|
||||
variable to BUILD_SERVER_1 in shared variables is the most efficient way.
|
||||
|
||||
One of the possible issues why remote builds would stop to work,
|
||||
although let's hope that will never change, would be that a cache folder is broken, therefore
|
||||
`ssh root@${BUILD_SERVER}` and then calling `rm -rf /root/build` on our build server will clear all the caches used for node_modules.
|
||||
Then you need to trigger a Netlify build again.
|
||||
|
||||
#### Running "locally" on Netlify instances (if the remote server is broken)
|
||||
|
||||
Without BUILD_SERVER variable, the following steps are done, from a file netlify/netlify.sh
|
||||
- the interactive-landscape package of the latest version is downloaded from npm
|
||||
- we go to that folder
|
||||
- we install node_modules via `npm install`
|
||||
- we run `PROJECT_PATH=.. npm run` build from the interactive-landscape package
|
||||
|
||||
### Building this repo, `landscapeapp` on a Netlify
|
||||
|
||||
We want to ensure that we are making builds of all the landscapes, defined in
|
||||
`landscapes.yml`
|
||||
Netlify parameters are stored in the `notilfy.toml` file, and it runs the
|
||||
`node netlify/landscapeapp.js` from the `netlify` folder.
|
||||
|
||||
First, we check if the hash of `.nvmrc`, `package.json` and `npm-shrinkwrap`
|
||||
file already exists as a key of our cache on our remote server.
|
||||
If it does exist, it means we can use this folder for `node_modules`, `.npm`
|
||||
and `.nvm` folders for every individual landscape.
|
||||
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
|
||||
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}`
|
||||
subfolder and logs are shown.
|
||||
Then _redirects and _headers files are generated to allow us to view
|
||||
individual landscapes from a Netlify build.
|
||||
|
||||
This repo is built only on our build server because Netlify has a 30 minutes timeout and we can not build individual landscapes there in parallel. Still,
|
||||
if every build fails and there are no obvious reasons, it may help to clear a
|
||||
node_modules cache: `ssh root@${BUILD_SERVER}` and then calling `rm -rf /root/build` and then running a new build on Netlify again
|
||||
### Setting up our build server to speed up Netlify builds
|
||||
If for some reasons our current server is lost or wiped, or we have to rent a different build server, these are required steps
|
||||
1. Install docker on a new server. Just the latest docker, nothing else is
|
||||
required
|
||||
2. Generate a new pair of ssh keys, and add a public key to the
|
||||
`/root/.ssh/authorized_keys` file
|
||||
3. Take a private key without first and last lines, replace \n with space, and
|
||||
add as a BUILDBOT_KEY variable to the shared variable on a Netlify website
|
||||
4. Update the BUILD_SERVER shared variable on a Netlify website and provide
|
||||
the IP address of the new build server
|
||||
|
||||
To just check that all is fine, go to the `netlify` folder on your computer,
|
||||
checkout any branch you want or even make local changes, and run `node
|
||||
landscapeapp.js`, do not forget to set all required variables, including the
|
||||
BUILDBOT_KEY and BUILD_SERVER. The build should finish with the success and
|
||||
copy generated files and folders to the `dist` folder in the root of the repo checkout
|
||||
|
||||
|
||||
## Keeping Project Up to Date
|
||||
We have an issue #75, where we update all out packages. This is how an update
|
||||
is usually done:
|
||||
1. Create a new folder like 75-update-2019-10-16
|
||||
2. Run `ncu -u` which is same as `npm-check-updates -u`, do not forget to
|
||||
install `npm install -g npm-check-updates`
|
||||
3. Run `npm install` , commit and push and make a PR
|
||||
4. Check that everything runs locally, i.e. `npm run open:src should still work
|
||||
well`
|
||||
5. Check that there are no layout issues on generated landscapes
|
||||
6. Do not forget to read README about those npm packages, which are mentioned in
|
||||
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
|
||||
|
||||
You can embed the landscape in a website in a few different ways...
|
||||
|
||||
- If you want just a full static image of the landscape in landscape mode, you can do:
|
||||
|
||||
```
|
||||
<!-- Embed ASWF landscape as a PNG -->
|
||||
<img src="https://landscape.aswf.io/images/landscape.png" alt="Academy Software Foundation Landscape Image">
|
||||
```
|
||||
|
||||
- If you want to embed the card mode for listing a category of entries ( for example members in a foundation or entries in a certain program ), you can do:
|
||||
|
||||
```
|
||||
<!-- Embed list of all Open Mainframe Project members -->
|
||||
<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.
|
||||
|
|
7
_headers
7
_headers
|
@ -1,2 +1,9 @@
|
|||
/*
|
||||
X-Robots-Tag: all
|
||||
Access-Control-Allow-Origin: *
|
||||
|
||||
/*.svg
|
||||
Content-Type: image/svg+xml; charset=utf-8
|
||||
|
||||
/*.json
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
/* /index.html 200
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
binpath=$(dirname $0)
|
||||
landscapes=$($binpath/list-all)
|
||||
|
||||
for landscape in $landscapes
|
||||
do
|
||||
cmd="PROJECT_PATH=$landscape $1"
|
||||
echo "Executing command: $cmd"
|
||||
eval $cmd
|
||||
done
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
parentdir=$(cd $(dirname "$0")/../..; pwd)
|
||||
find $parentdir -maxdepth 2 -type f -name "landscape.yml" | xargs -n1 dirname
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
binpath=$(dirname $0)
|
||||
landscapes=$($binpath/list-all)
|
||||
|
||||
for landscape in $landscapes
|
||||
do
|
||||
cd $landscape
|
||||
git reset --hard
|
||||
git checkout master
|
||||
git pull
|
||||
done
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
binpath=$(dirname $0)
|
||||
landscapes=$($binpath/list-all)
|
||||
|
||||
for landscape in $landscapes
|
||||
do
|
||||
kill -9 `ps ux | grep srcServer | grep -v grep | awk -F ' ' '{print $2}'` 2>/dev/null || true
|
||||
(PROJECT_PATH=$landscape npm run open:src &) && sleep 10 && PROJECT_PATH=$landscape PORT=3000 npm run integration-test
|
||||
done
|
18
build.sh
18
build.sh
|
@ -1,20 +1,8 @@
|
|||
set -e
|
||||
rm -rf $2 || true
|
||||
git clone https://github.com/$1 $2
|
||||
timeout 120s git clone --quiet https://github.com/$1 $2
|
||||
cd $2
|
||||
git checkout origin/$3
|
||||
git remote -v
|
||||
cd ..
|
||||
export PROJECT_PATH=$2
|
||||
export PROJECT_PATH=$PWD/$2
|
||||
PROJECT_NAME=$2 yarn build
|
||||
mkdir -p dist
|
||||
cp -r $2/dist dist/$2
|
||||
rm -rf ./$2
|
||||
echo "/$2/* /$2/index.html 200" >> dist/_redirects
|
||||
echo "<div><a href="$2/"><h1>$2</h1></a></div>" >> dist/index.html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -1,23 +0,0 @@
|
|||
set -e
|
||||
rm -rf dist || true
|
||||
mkdir -p dist
|
||||
|
||||
bash build.sh LFDLFoundation/landscape lfdl 33-switch-to-upstream
|
||||
bash build.sh cncf/landscape cncf 1015-try-upstream
|
||||
|
||||
# This will increase a version and publish to an npm
|
||||
# If there is an existing package
|
||||
if [ $BRANCH = "master" ]; then
|
||||
git config --global user.email "info@cncf.io"
|
||||
git config --global user.name "Netlify Publisher"
|
||||
git remote rm github || true
|
||||
git remote add github "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/cncf/landscapeapp"
|
||||
git fetch github
|
||||
yarn 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 --tags
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
yarn publish
|
||||
fi
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
"verbose": true
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
# This is a main bash file to run on an update server every day.
|
||||
|
||||
# Our first goal is to ensure that all components are installed
|
||||
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 gawk aha nginx libgbm-dev
|
||||
|
||||
# Then we run our wrapper to process all landscapes
|
||||
. ~/.nvm/nvm.sh
|
||||
npm install
|
||||
nvm install `cat .nvmrc`
|
||||
nvm use
|
||||
npm install -g npm
|
||||
npm install -g yarn
|
||||
yarn
|
||||
yarn node tools/landscapes.js
|
|
@ -0,0 +1,83 @@
|
|||
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 default branch build
|
||||
- landscape:
|
||||
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: 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
|
||||
hook: 5d8b7e9b2571ad2a23a392b8
|
||||
- landscape:
|
||||
name: graphql
|
||||
repo: graphql/graphql-landscape
|
||||
hook: 5d5c7ccf64ecb5bd3d2592f7
|
||||
required: true
|
||||
- landscape:
|
||||
name: lfai
|
||||
repo: lfai/landscape
|
||||
hook: 60648e5b74c76017210a2f53
|
||||
required: true
|
||||
- landscape:
|
||||
name: lfph
|
||||
repo: lfph/lfph-landscape
|
||||
hook: 5ec5145565b543b0b01aa9d7
|
||||
required: true
|
||||
- landscape:
|
||||
name: omp
|
||||
repo: openmainframeproject/omp-landscape
|
||||
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
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,194 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Linux Foundation Landscapes </title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.13.1/js-yaml.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.2/css/bulma.min.css">
|
||||
<style>
|
||||
html {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
body .container {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.header,
|
||||
.main {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
table.table {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td.organization a,
|
||||
td.status a {
|
||||
float: left;
|
||||
display: block;
|
||||
}
|
||||
|
||||
td.organization a,
|
||||
td.organization img,
|
||||
.logo {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.previews {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.preview-logo-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
td.status a,
|
||||
td.status img {
|
||||
height: 18px;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container is-widescreen">
|
||||
<div class="header is-clipped">
|
||||
<h1 class="title is-marginless is-pulled-left">Linux Foundation Landscapes</h1>
|
||||
<div class="is-pulled-right">
|
||||
<a href="#" data-behavior="toggle" class="button is-light is-link">Show Previews</a>
|
||||
<a href="#" class="is-hidden button is-light is-link" data-behavior="toggle">Show List</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<table class="table is-fullwidth" data-behavior="toggle-target">
|
||||
<thead>
|
||||
<th>Organization</th>
|
||||
<th>Landscape</th>
|
||||
<th>Repo</th>
|
||||
<th>Status</th>
|
||||
<th># of Tweets</th>
|
||||
<th>Published</th>
|
||||
<th>Data Refreshed</th>
|
||||
</thead>
|
||||
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<div class="is-hidden previews columns is-multiline" data-behavior="toggle-target"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const toggleTargets = document.querySelectorAll('[data-behavior="toggle"], [data-behavior="toggle-target"]')
|
||||
|
||||
document.querySelectorAll('[data-behavior="toggle"]').forEach(el => {
|
||||
el.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
toggleTargets.forEach(({ classList }) => classList.toggle('is-hidden'))
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
let reportsIp = null;
|
||||
const fetchFile = async path => await (await fetch(`https://raw.githubusercontent.com/${path}`)).text()
|
||||
|
||||
const tableEl = document.querySelector("tbody")
|
||||
const previewsEl = document.querySelector(".previews")
|
||||
|
||||
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, 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)
|
||||
const appendColumn = (args = {}) => {
|
||||
let column = document.createElement('td')
|
||||
column.setAttribute("class", args.className)
|
||||
column.innerText = "Fetching..."
|
||||
rowEl.append(column)
|
||||
return column
|
||||
}
|
||||
|
||||
const appendPreview = () => {
|
||||
let preview = document.createElement('div')
|
||||
preview.setAttribute("class", "preview column is-one-third-widescreen is-half-desktop is-full-tablet")
|
||||
previewsEl.append(preview)
|
||||
return preview
|
||||
}
|
||||
|
||||
let organizationEl = appendColumn({ className: "organization" })
|
||||
let landscapeEl = appendColumn()
|
||||
let repoEl = appendColumn()
|
||||
let statusEl = appendColumn({ className: "status" })
|
||||
let numTweetsEl = appendColumn()
|
||||
let publishedEl = appendColumn()
|
||||
let updatedEl = appendColumn()
|
||||
let previewEl = appendPreview()
|
||||
|
||||
repoEl.innerHTML = linkTo(`https://github.com/${repo}`, repo)
|
||||
|
||||
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)
|
||||
landscapeEl.innerHTML = linkTo(website, short_domain)
|
||||
previewEl.innerHTML = `
|
||||
<div class="preview-logo-wrapper">
|
||||
<img src="${website}/images/right-logo.svg" class="logo" alt="${short_name}"/>
|
||||
</div>
|
||||
${imageLinkTo(website, `${website}/images/landscape_preview.png`, short_domain)}`
|
||||
const landscapeIndex = await (await fetch(website)).text()
|
||||
const publishedAt = landscapeIndex.match(/Updated:\s*([^"]*)/)[1]
|
||||
publishedEl.innerHTML = publishedAt ? dateTimeFormat.format(new Date(publishedAt)) : 'UNKNOWN'
|
||||
})
|
||||
|
||||
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}/HEAD/processed_landscape.yml`)
|
||||
.then(processedLandscape => {
|
||||
const { updated_at, twitter_options } = jsyaml.load(processedLandscape)
|
||||
const updatedAt = updated_at ? new Date(updated_at) : null
|
||||
const oneDayAgo = new Date() - 24 * 60 * 60 * 1000
|
||||
const className = updatedAt && updatedAt > oneDayAgo ? '' : 'has-text-danger'
|
||||
|
||||
numTweetsEl.innerHTML = (twitter_options ? twitter_options.count : 0).toString()
|
||||
|
||||
updatedEl.innerHTML = `<div class="${className}">
|
||||
<div> ${updated_at ? dateTimeFormat.format(updatedAt) : 'UNKNOWN' } </div>
|
||||
<a href="http://${reportsIp}/${name}.html">VIEW DAILY AUTOUPDATE LOGS</a>
|
||||
</div>`
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
|
@ -0,0 +1,13 @@
|
|||
We use netlify for builds and deploys.
|
||||
For landscapeapp project we rely on parallel builds.
|
||||
Netlify runs a build for a given PR on a landscapeapp.
|
||||
The netlifyBuild.js file then uses rsync to xcopy the current folder to
|
||||
our prepared packet server. On that packet server we run all the landscapes in
|
||||
parallel using a netlify docker image. The output from each landscape is captured and returned back
|
||||
to the Netlify cloud server. The dist folder is returned back to the cloud
|
||||
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.
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[build]
|
||||
ignore = "false"
|
||||
environment = { RUBY_VERSION = "2.6.2", NODE_VERSION = "14.3" }
|
||||
|
||||
[functions]
|
||||
directory = "netlify/functions"
|
|
@ -0,0 +1,32 @@
|
|||
module.exports = results => (`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.2/css/bulma.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="columns is-multiline">
|
||||
${results.map(({ landscape, exitCode }) => {
|
||||
return `
|
||||
<div class="column is-one-third-widescreen is-half-desktop is-full-tablet">
|
||||
<div style="display: flex; justify-content: center">
|
||||
<img src="${landscape.name}/images/right-logo.svg" style="height: 35px"
|
||||
alt="${landscape.name}"/>
|
||||
</div>
|
||||
${ exitCode === 0 ?
|
||||
`<a href="${landscape.name}/" target="_blank" rel="noopener noreferrer">
|
||||
<img src="${landscape.name}/images/landscape_preview.png"/>
|
||||
</a>` :
|
||||
`<div class="message is-danger is-large"><div class="message-body" style="border: none; text-align: center">FAILED</div></div>` }
|
||||
</div>
|
||||
`
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,261 @@
|
|||
// We will execute this script from a landscape build,
|
||||
// "prepublish": "cp yarn.lock _yarn.lock",
|
||||
// "postpublish": "rm _yarn.lock || true"
|
||||
const remote = `root@${process.env.BUILD_SERVER}`;
|
||||
const dockerImage = 'netlify/build:focal';
|
||||
const dockerHome = '/opt/buildhome';
|
||||
|
||||
const secrets = [
|
||||
process.env.CRUNCHBASE_KEY_4, process.env.TWITTER_KEYS, process.env.GITHUB_TOKEN, process.env.GITHUB_USER, process.env.GITHUB_KEY
|
||||
].filter( (x) => !!x);
|
||||
|
||||
const maskSecrets = function(x) {
|
||||
let result = x;
|
||||
const replaceAll = function(s, search, replacement) {
|
||||
var target = s;
|
||||
return target.split(search).join(replacement);
|
||||
};
|
||||
for (var secret of secrets) {
|
||||
const safeString = secret.substring(0, 2) + '***' + secret.substring(secret.length -2);
|
||||
result = replaceAll(result, secret, safeString);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
for (var secret of secrets) {
|
||||
console.info(maskSecrets(`We have a secret: ${secret}`));
|
||||
}
|
||||
const debug = function() {
|
||||
if (process.env.DEBUG_BUILD) {
|
||||
console.info.apply(console, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
const runLocal = function(command, showProgress) {
|
||||
|
||||
// report the output once every 5 seconds
|
||||
let lastOutput = { s: '', time: new Date().getTime() };
|
||||
let displayIfRequired = function(text) {
|
||||
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}`]);
|
||||
let output = [];
|
||||
child.stdout.on('data', function(data) {
|
||||
const text = maskSecrets(data.toString('utf-8'));
|
||||
output.push(text);
|
||||
displayIfRequired(text);
|
||||
//Here is where the output goes
|
||||
});
|
||||
child.stderr.on('data', function(data) {
|
||||
const text = maskSecrets(data.toString('utf-8'));
|
||||
output.push(text);
|
||||
displayIfRequired(text);
|
||||
//Here is where the error output goes
|
||||
});
|
||||
child.on('close', function(exitCode) {
|
||||
lastOutput.done = true;
|
||||
displayIfRequired('');
|
||||
resolve({text: output.join(''), exitCode});
|
||||
//Here you can get the exit code of the script
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const runLocalWithoutErrors = async function(command) {
|
||||
debug(command);
|
||||
const result = await runLocal(command);
|
||||
console.info(result.text);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to execute ${command}, exit code: ${result.exitCode}`);
|
||||
}
|
||||
return result.text.trim();
|
||||
}
|
||||
|
||||
const key = `
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
${(process.env.BUILDBOT_KEY || '').replace(/\s/g,'\n')}
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
`.split('\n').slice(1).join('\n');
|
||||
require('fs').writeFileSync('/tmp/buildbot', key);
|
||||
require('fs').chmodSync('/tmp/buildbot', 0o600);
|
||||
|
||||
const runRemote = async function(command, count = 3) {
|
||||
const bashCommand = `
|
||||
nocheck=" -o StrictHostKeyChecking=no "
|
||||
ssh -i /tmp/buildbot $nocheck ${remote} << 'EOSSH'
|
||||
set -e
|
||||
${command}
|
||||
EOSSH
|
||||
`
|
||||
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());
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to execute remote ${command}, exit code: ${result.exitCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
const makeRemoteBuildWithCache = async function() {
|
||||
await runLocalWithoutErrors(`
|
||||
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
|
||||
const getHash = function() {
|
||||
const crypto = require('crypto');
|
||||
const p0 = require('fs').readFileSync('packageRemote/.nvmrc', 'utf-8').trim();
|
||||
const p1 = crypto.createHash('sha256').update(require('fs').readFileSync('packageRemote/package.json')).digest('hex');
|
||||
const p2 = crypto.createHash('sha256').update(require('fs').readFileSync('packageRemote/yarn.lock')).digest('hex');
|
||||
const p3 = crypto.createHash('sha256').update(require('fs').readFileSync('packageRemote/.yarnrc.yml')).digest('hex');
|
||||
return p0 + p1 + p2 + p3;
|
||||
}
|
||||
|
||||
const getTmpFile = () => new Date().getTime().toString() + Math.random();
|
||||
const nvmrc = require('fs').readFileSync('packageRemote/.nvmrc', 'utf-8').trim();
|
||||
console.info(`node version:`, nvmrc);
|
||||
// now our goal is to run this on a remote server. Step 1 - xcopy the repo
|
||||
const folder = getTmpFile();
|
||||
|
||||
await runLocalWithoutErrors(`
|
||||
rm -rf remoteDist || true
|
||||
mkdir -p remoteDist
|
||||
`);
|
||||
await runRemoteWithoutErrors(`mkdir -p /root/builds`);
|
||||
await runRemoteWithoutErrors(`docker pull ${dockerImage}`);
|
||||
await runLocalWithoutErrors(`
|
||||
rsync --exclude="package" -az -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " . ${remote}:/root/builds/${folder}
|
||||
`);
|
||||
await runRemoteWithoutErrors(`chmod -R 777 /root/builds/${folder}`);
|
||||
|
||||
const hash = getHash();
|
||||
const tmpHash = require('crypto').createHash('sha256').update(getTmpFile()).digest('hex');
|
||||
// lets guarantee npm install for this folder first
|
||||
// do not pass REVIEW_ID because on failure we will run it locally and report
|
||||
// from there
|
||||
const vars = [
|
||||
'CRUNCHBASE_KEY_4',
|
||||
'GITHUB_KEY',
|
||||
'TWITTER_KEYS',
|
||||
'GA',
|
||||
'BRANCH',
|
||||
'GITHUB_TOKEN',
|
||||
'GITHUB_USER',
|
||||
'REPOSITORY_URL'
|
||||
];
|
||||
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`
|
||||
].join(' && ');
|
||||
|
||||
const dockerCommand = `
|
||||
mkdir -p /root/builds/${outputFolder}
|
||||
chmod -R 777 /root/builds/${outputFolder}
|
||||
chmod -R 777 /root/builds/${folder}
|
||||
|
||||
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/${folder}:/opt/repo \
|
||||
-v /root/builds/${outputFolder}:/dist \
|
||||
${dockerImage} /bin/bash -lc "${buildCommand}"
|
||||
|
||||
`;
|
||||
|
||||
debug(dockerCommand);
|
||||
|
||||
// run a build command remotely for a given repo
|
||||
let output;
|
||||
output = await runRemote(dockerCommand);
|
||||
console.info(`Output from remote build, exit code: ${output.exitCode}`);
|
||||
if (output.exitCode === 255) { // a single ssh failure
|
||||
console.info('SSH failure! Retrying ...');
|
||||
output = await runRemote(dockerCommand);
|
||||
console.info(`Output from remote build, exit code: ${output.exitCode}`);
|
||||
} else if (output.exitCode !== 0) {
|
||||
console.info(output.text);
|
||||
throw new Error('Remote build failed');
|
||||
// console.info('Retrying with reinstalling npm');
|
||||
// output = await runRemote(dockerCommandWithNpmInstall);
|
||||
// console.info(`Output from remote build, exit code: ${output.exitCode}`);
|
||||
}
|
||||
console.info(output.text);
|
||||
// a build is done
|
||||
console.info(await runLocalWithoutErrors(
|
||||
`
|
||||
mkdir -p distRemote
|
||||
rsync -az --chmod=a+r -p -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " ${remote}:/root/builds/${outputFolder}/dist/* distRemote
|
||||
`
|
||||
));
|
||||
await runRemote(
|
||||
`
|
||||
rm -rf /root/builds/${folder}
|
||||
rm -rf /root/builds/${outputFolder}
|
||||
`
|
||||
)
|
||||
|
||||
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
|
||||
mv netlify/dist/functions netlify/functions
|
||||
cp -r netlify/functions functions # Fix netlify bug
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.info('starting', process.cwd());
|
||||
process.chdir('..');
|
||||
await runLocal('rm package*.json');
|
||||
|
||||
const cleanPromise = runRemoteWithoutErrors(`
|
||||
find builds/ -maxdepth 1 -not -path "builds/node_cache" -mtime +1 -exec rm -rf {} +;
|
||||
`).catch(function() {
|
||||
console.info('Failed to clean up a builds folder');
|
||||
});
|
||||
|
||||
await Promise.all([makeRemoteBuildWithCache().catch(function(ex) {
|
||||
console.info('build failed', ex);
|
||||
process.exit(1);
|
||||
}), cleanPromise]);
|
||||
}
|
||||
|
||||
main().catch(function(ex) {
|
||||
console.info(ex);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const pause = function(i) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(resolve, i * 1000);
|
||||
})
|
||||
};
|
||||
|
||||
const yaml = require('./jsyaml');
|
||||
process.chdir('..');
|
||||
const landscapesInfo = yaml.load(require('fs').readFileSync('landscapes.yml'));
|
||||
|
||||
const dockerImage = 'netlify/build:focal';
|
||||
const dockerHome = '/opt/buildhome';
|
||||
|
||||
|
||||
async function main() {
|
||||
const nvmrc = require('fs').readFileSync('.nvmrc', 'utf-8').trim();
|
||||
const secrets = [
|
||||
process.env.CRUNCHBASE_KEY_4, process.env.TWITTER_KEYS, process.env.GITHUB_TOKEN, process.env.GITHUB_USER, process.env.GITHUB_KEY
|
||||
].filter( (x) => !!x);
|
||||
|
||||
const maskSecrets = function(x) {
|
||||
let result = x;
|
||||
const replaceAll = function(s, search, replacement) {
|
||||
var target = s;
|
||||
return target.split(search).join(replacement);
|
||||
};
|
||||
for (var secret of secrets) {
|
||||
const safeString = secret.substring(0, 2) + '***' + secret.substring(secret.length -2);
|
||||
result = replaceAll(result, secret, safeString);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
for (var secret of secrets) {
|
||||
console.info(maskSecrets(`We have a secret: ${secret}`));
|
||||
}
|
||||
|
||||
const key = `
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
${process.env.BUILDBOT_KEY.replace(/\s/g,'\n')}
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
`.split('\n').slice(1).join('\n');
|
||||
require('fs').writeFileSync('/tmp/buildbot', key);
|
||||
require('fs').chmodSync('/tmp/buildbot', 0o600);
|
||||
|
||||
|
||||
// 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.199.15';
|
||||
|
||||
const runRemote = async function(command) {
|
||||
const bashCommand = `
|
||||
nocheck=" -o StrictHostKeyChecking=no "
|
||||
ssh -i /tmp/buildbot $nocheck ${remote} << 'EOSSH'
|
||||
set -e
|
||||
${command}
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
result.text = newOutput.join('\n');
|
||||
return result;
|
||||
};
|
||||
|
||||
const runLocal = function(command) {
|
||||
return new Promise(function(resolve) {
|
||||
let finished = false;
|
||||
let timeout = setTimeout(function() {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
child.kill();
|
||||
resolve({
|
||||
exitCode: 'timeout',
|
||||
text: 'A command took more than 25 minutes. \n' + output.join('')
|
||||
});
|
||||
}, 25 * 60 * 1000);
|
||||
var spawn = require('child_process').spawn;
|
||||
var child = spawn('bash', ['-lc',`set -e \n${command}`]);
|
||||
let output = [];
|
||||
child.stdout.on('data', function(data) {
|
||||
const text = maskSecrets(data.toString('utf-8'));
|
||||
// console.info(text);
|
||||
output.push(text);
|
||||
//Here is where the output goes
|
||||
});
|
||||
child.stderr.on('data', function(data) {
|
||||
const text = maskSecrets(data.toString('utf-8'));
|
||||
// console.info(text);
|
||||
output.push(text);
|
||||
//Here is where the error output goes
|
||||
});
|
||||
child.on('close', function(exitCode) {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
clearTimeout(timeout);
|
||||
resolve({text: output.join(''), exitCode});
|
||||
}
|
||||
//Here you can get the exit code of the script
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const runLocalWithoutErrors = async function(command) {
|
||||
const result = await runLocal(command);
|
||||
console.info(result.text);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to execute ${command}, exit code: ${result.exitCode}`);
|
||||
}
|
||||
return result.text.trim();
|
||||
}
|
||||
|
||||
const runRemoteWithoutErrors = async function(command) {
|
||||
const result = await runRemote(command);
|
||||
console.info(result.text);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed to execute remote ${command}, exit code: ${result.exitCode}`);
|
||||
}
|
||||
return result.text.trim();
|
||||
}
|
||||
|
||||
await runLocalWithoutErrors(`
|
||||
rm -rf dist || true
|
||||
mkdir -p dist netlify/functions
|
||||
`);
|
||||
await runRemoteWithoutErrors(`mkdir -p /root/builds`);
|
||||
await runRemoteWithoutErrors(`docker pull ${dockerImage}`);
|
||||
await runLocalWithoutErrors(`
|
||||
rsync --exclude="node_modules" --exclude="dist" -az -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " . ${remote}:/root/builds/${folder}
|
||||
`);
|
||||
console.info('Rsync done');
|
||||
await runRemoteWithoutErrors(`chmod -R 777 /root/builds/${folder}`);
|
||||
|
||||
// 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} >/dev/null`,
|
||||
`nvm use ${nvmrc}`,
|
||||
`npm install -g yarn --no-progress --silent`,
|
||||
`cd /opt/repo`,
|
||||
`yarn >/dev/null`,
|
||||
`yarn eslint`
|
||||
].join(' && ');
|
||||
const npmInstallCommand = `
|
||||
mkdir -p /root/builds/${folder}_node
|
||||
mkdir -p /root/builds/${folder}_node/{yarnGlobal,nvm}
|
||||
chmod -R 777 /root/builds/${folder}_node
|
||||
docker run --shm-size 1G --rm -t \
|
||||
-v /root/builds/${folder}_node/nvm:${dockerHome}/.nvm \
|
||||
-v /root/builds/${folder}_node/yarnGlobal:${dockerHome}/.yarn \
|
||||
-v /root/builds/${folder}:/opt/repo \
|
||||
${dockerImage} /bin/bash -lc "${buildCommand}"
|
||||
chmod -R 777 /root/builds/${folder}_node
|
||||
`
|
||||
debug(npmInstallCommand);
|
||||
console.info(`Installing npm packages`);
|
||||
const output = await runRemote(npmInstallCommand);
|
||||
console.info(`Output from npm install: exit code: ${output.exitCode}`);
|
||||
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);
|
||||
|
||||
}
|
||||
// all landscapes
|
||||
|
||||
const results = await Promise.all(landscapesInfo.landscapes.map(async function(landscape, i) {
|
||||
await pause(i);
|
||||
const vars = ['CRUNCHBASE_KEY_4', 'GITHUB_KEY', 'TWITTER_KEYS'];
|
||||
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}`,
|
||||
`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}
|
||||
|
||||
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/${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}`);
|
||||
debug(dockerCommand);
|
||||
|
||||
|
||||
// run a build command remotely for a given repo
|
||||
let output;
|
||||
|
||||
output = await runRemote(dockerCommand);
|
||||
output.landscape = landscape;
|
||||
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;
|
||||
console.info('Retrying ...');
|
||||
console.info(`Output from: ${output.landscape.name}, exit code: ${output.exitCode}`);
|
||||
console.info(output.text);
|
||||
}
|
||||
landscape.done = true;
|
||||
console.info(`Remaining : ${landscapesInfo.landscapes.filter( (x) => !x.done).map( (x) => x.name).join(',')}`);
|
||||
|
||||
await runLocal(
|
||||
`
|
||||
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}
|
||||
`
|
||||
)
|
||||
return output;
|
||||
}));
|
||||
await runRemote(`
|
||||
rm -rf /root/builds/${folder}
|
||||
rm -rf /root/builds/${folder}_node || true
|
||||
`);
|
||||
for (let x of results) {
|
||||
if (x.exitCode !== 0 && x.landscape.required) {
|
||||
console.info(`a landscape ${x.landscape.name} failed but it is required = ${x.landscape.required}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const index = generateIndex(results)
|
||||
const robots = `
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
`;
|
||||
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: /");
|
||||
|
||||
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 "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
|
||||
for (let landscape of landscapesInfo.landscapes) {
|
||||
console.info(`triggering a hook for ${landscape.name}`);
|
||||
await runLocalWithoutErrors(`curl -X POST -d {} https://api.netlify.com/build_hooks/${landscape.hook}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
main().then(function() {
|
||||
process.exit(0);
|
||||
}).catch(function(ex) {
|
||||
console.info(ex);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -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?
|
241
package.json
241
package.json
|
@ -1,190 +1,97 @@
|
|||
{
|
||||
"name": "interactive-landscape",
|
||||
"version": "1.0.6",
|
||||
"description": "Interactive visualization of cloud native projects and products",
|
||||
"version": "1.0.657",
|
||||
"description": "Visualization tool for building interactive landscapes",
|
||||
"engines": {
|
||||
"npm": ">=3",
|
||||
"node": ">= 10.5"
|
||||
},
|
||||
"scripts": {
|
||||
"open:src": "npm run yaml2json && babel-node tools/srcServer.js",
|
||||
"open:dist": "babel-node tools/distServer.js",
|
||||
"lint": "esw webpack.config.* src tools --color",
|
||||
"lint:watch": "npm run lint -- --watch",
|
||||
"fetch": "babel-node tools/validateLandscapeKeys && babel-node tools/addExternalInfo.js && npm run yaml2json",
|
||||
"update": "(rm /tmp/landscape.json || true) && babel-node tools/validateLandscapeKeys && npm run remove-quotes && LEVEL=medium babel-node tools/addExternalInfo.js && npm run yaml2json",
|
||||
"yaml2json": "babel-node tools/generateJson.js",
|
||||
"remove-quotes": "babel-node tools/removeQuotes",
|
||||
"check-links": "babel-node tools/checkLinks",
|
||||
"fetchAll": "LEVEL=complete npm run fetch",
|
||||
"clean-dist": "npm run remove-dist && mkdir \"$PROJECT_PATH\"/dist",
|
||||
"remove-dist": "rimraf \"$PROJECT_PATH\"/dist",
|
||||
"precommit": "npm run fetch",
|
||||
"start-ci": "(babel-node tools/ciServer &) & sleep 10",
|
||||
"stop-ci": "kill -9 `cat /tmp/ci.pid` && rm /tmp/ci.pid",
|
||||
"integration-test": "jest",
|
||||
"check-landscape": "babel-node tools/checkLandscape",
|
||||
"render-landscape": "babel-node tools/renderLandscape",
|
||||
"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",
|
||||
"setup-robots": "babel-node tools/sitemap && babel-node tools/addRobots",
|
||||
"quick-build": "babel-node tools/build.js && npm run copy-dist && npm run setup-robots",
|
||||
"prebuild": "npm config set scripts-prepend-node-path true && npm run fetch && npm run clean-dist",
|
||||
"build": "babel-node tools/build.js && npm run copy-dist && npm run setup-robots && npm run start-ci && npm run integration-test && npm run check-landscape && npm run render-landscape && npm run stop-ci && babel-node tools/fundingForMasterBranch",
|
||||
"show-report": "open dist/report.html",
|
||||
"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",
|
||||
"fetch": "node tools/validateLandscape && node tools/checkWrongCharactersInFilenames && node tools/addExternalInfo.js && yarn yaml2json",
|
||||
"fetchAll": "LEVEL=complete yarn fetch",
|
||||
"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",
|
||||
"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",
|
||||
"test:CI": "babel-node tools/testCi.js",
|
||||
"test:watch": "jest --watch",
|
||||
"analyze-bundle": "babel-node ./tools/analyzeBundle.js"
|
||||
"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": "node tools/resetTweetCount.js",
|
||||
"prepublish": "cp yarn.lock _yarn.lock",
|
||||
"postpublish": "rm _yarn.lock || true",
|
||||
"preview": "yarn fetch && yarn prepare-landscape && yarn export-functions"
|
||||
},
|
||||
"author": "CNCF",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.1.5",
|
||||
"@babel/core": "7.1.6",
|
||||
"@babel/node": "7.0.0",
|
||||
"@babel/plugin-proposal-class-properties": "7.1.0",
|
||||
"@babel/plugin-proposal-decorators": "7.1.6",
|
||||
"@babel/plugin-proposal-do-expressions": "7.0.0",
|
||||
"@babel/plugin-proposal-export-default-from": "7.0.0",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.0.0",
|
||||
"@babel/plugin-proposal-function-bind": "7.0.0",
|
||||
"@babel/plugin-proposal-function-sent": "7.1.0",
|
||||
"@babel/plugin-proposal-json-strings": "7.0.0",
|
||||
"@babel/plugin-proposal-logical-assignment-operators": "7.0.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.0.0",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.0.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.0.0",
|
||||
"@babel/plugin-proposal-pipeline-operator": "7.0.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.0.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.0.0",
|
||||
"@babel/plugin-syntax-import-meta": "7.0.0",
|
||||
"@babel/plugin-transform-async-to-generator": "7.1.0",
|
||||
"@babel/plugin-transform-react-constant-elements": "7.0.0",
|
||||
"@babel/plugin-transform-regenerator": "7.0.0",
|
||||
"@babel/plugin-transform-runtime": "7.1.0",
|
||||
"@babel/polyfill": "7.0.0",
|
||||
"@babel/preset-env": "7.1.6",
|
||||
"@babel/preset-react": "7.0.0",
|
||||
"@babel/register": "7.0.0",
|
||||
"@material-ui/core": "3.5.1",
|
||||
"@material-ui/icons": "3.0.1",
|
||||
"autoprefixer": "9.3.1",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-jest": "23.6.0",
|
||||
"babel-loader": "8.0.4",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"babel-plugin-root-import": "6.1.0",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.20",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"babel-preset-latest-node": "2.0.0-beta.3",
|
||||
"bluebird": "3.5.3",
|
||||
"browser-sync": "2.26.3",
|
||||
"chalk": "2.4.1",
|
||||
"change-case": "3.0.2",
|
||||
"cheerio": "1.0.0-rc.2",
|
||||
"classnames": "2.2.6",
|
||||
"colors": "1.3.2",
|
||||
"comma-number": "2.0.0",
|
||||
"connect-history-api-fallback": "1.5.0",
|
||||
"convert-svg-to-png": "0.4.0",
|
||||
"css-loader": "1.0.1",
|
||||
"cssnano": "4.1.7",
|
||||
"current-device": "0.7.8",
|
||||
"debug": "4.1.0",
|
||||
"ejs": "2.6.1",
|
||||
"ejs-loader": "0.3.1",
|
||||
"eslint": "5.9.0",
|
||||
"eslint-plugin-import": "2.14.0",
|
||||
"eslint-plugin-react": "7.11.1",
|
||||
"eslint-watch": "4.0.2",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"anchorme": "^2.1.2",
|
||||
"axios": "^0.27.2",
|
||||
"bluebird": "3.7.2",
|
||||
"change-case": "^4.1.2",
|
||||
"cheerio": "^1.0.0-rc.11",
|
||||
"colors": "1.4.0",
|
||||
"debug": "^4.3.4",
|
||||
"eslint": "latest",
|
||||
"event-emitter": "0.3.5",
|
||||
"feed": "2.0.1",
|
||||
"file-loader": "2.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",
|
||||
"git-last-commit": "0.3.0",
|
||||
"history": "4.7.2",
|
||||
"html-webpack-plugin": "^3.0.7",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"iframe-resizer": "3.6.3",
|
||||
"jest": "23.6.0",
|
||||
"jest-cli": "23.6.0",
|
||||
"jimp": "^0.2.28",
|
||||
"js-yaml": "3.12.0",
|
||||
"jsdom": "13.0.0",
|
||||
"json-loader": "0.5.7",
|
||||
"json2csv": "4.3.1",
|
||||
"lodash-es": "4.17.11",
|
||||
"millify": "2.0.1",
|
||||
"mini-css-extract-plugin": "0.4.4",
|
||||
"minimatch": "3.0.4",
|
||||
"mockdate": "2.0.2",
|
||||
"node-sass": "4.10.0",
|
||||
"object-assign": "4.1.1",
|
||||
"opn-cli": "4.0.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"prompt": "1.0.0",
|
||||
"prop-types": "15.6.2",
|
||||
"puppeteer": "1.10.0",
|
||||
"query-string": "6.2.0",
|
||||
"raf": "3.4.1",
|
||||
"react": "16.6.3",
|
||||
"react-dom": "16.6.3",
|
||||
"react-ga": "2.5.3",
|
||||
"react-hot-loader": "4.3.12",
|
||||
"react-key-handler": "1.2.0-beta.3",
|
||||
"react-redux": "5.1.1",
|
||||
"react-router-dom": "4.3.1",
|
||||
"react-router-redux": "5.0.0-alpha.8",
|
||||
"react-test-renderer": "16.6.3",
|
||||
"react-twitter-widgets": "1.7.1",
|
||||
"redux": "4.0.1",
|
||||
"redux-immutable-state-invariant": "2.1.0",
|
||||
"redux-mock-store": "1.5.3",
|
||||
"redux-thunk": "2.3.0",
|
||||
"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",
|
||||
"replace": "1.0.0",
|
||||
"request": "2.88.0",
|
||||
"request-promise": "4.2.2",
|
||||
"require-promise": "1.0.1",
|
||||
"reselect": "4.0.0",
|
||||
"rimraf": "2.6.2",
|
||||
"sass-loader": "7.1.0",
|
||||
"sitemap": "2.1.0",
|
||||
"string-replace-all": "1.0.3",
|
||||
"style-loader": "0.23.1",
|
||||
"terser-webpack-plugin": "1.1.0",
|
||||
"sanitize-html": "^2.7.0",
|
||||
"showdown": "^2.1.0",
|
||||
"sitemap": "^7.1.1",
|
||||
"svg-autocrop": "^2.0.41",
|
||||
"traverse": "0.6.6",
|
||||
"twitter": "1.7.1",
|
||||
"typeface-roboto": "0.0.54",
|
||||
"uglifyjs-webpack-plugin": "2.0.1",
|
||||
"url-loader": "1.1.2",
|
||||
"webapp-webpack-plugin": "2.3.1",
|
||||
"webpack": "4.26.0",
|
||||
"webpack-bundle-analyzer": "3.0.3",
|
||||
"webpack-dev-middleware": "3.4.0",
|
||||
"webpack-hot-middleware": "2.24.3",
|
||||
"webpack-md5-hash": "0.0.6",
|
||||
"xml2js": "0.4.19",
|
||||
"yahoo-finance": "0.3.5",
|
||||
"yaml-loader": "0.5.0"
|
||||
"yarn": "^1.22.18"
|
||||
},
|
||||
"keywords": [],
|
||||
"keywords": [
|
||||
"landscape",
|
||||
"interactive",
|
||||
"map",
|
||||
"categories",
|
||||
"crunchbase",
|
||||
"github",
|
||||
"ecosystem",
|
||||
"stars",
|
||||
"funding"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cncf/landscapeapp"
|
||||
},
|
||||
"jest": {
|
||||
"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": {
|
||||
"postinstall-prepare": "1.0.1"
|
||||
}
|
||||
"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,62 +1,165 @@
|
|||
import puppeteer from "puppeteer";
|
||||
import devices from 'puppeteer/DeviceDescriptors';
|
||||
import { settings } from '../tools/settings'
|
||||
const port = process.env.PORT || '4000';
|
||||
const appUrl = `http://localhost:${port}`;
|
||||
const puppeteer = require("puppeteer");
|
||||
require('expect-puppeteer');
|
||||
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 page;
|
||||
|
||||
let browser;
|
||||
let page;
|
||||
let close = () => test('Closing a browser', async () => await browser.close());
|
||||
|
||||
if (process.env.SHOW_BROWSER) {
|
||||
jest.setTimeout(30000);
|
||||
}
|
||||
function mainTest() {
|
||||
describe("Main test", () => {
|
||||
test("I visit a main page and have all required elements", async () => {
|
||||
console.info('about to open a page', appUrl);
|
||||
await page.goto(appUrl);
|
||||
console.info('page is open');
|
||||
//header
|
||||
await page.waitForXPath(`//h1[text() = '${settings.test.header}']`);
|
||||
console.info('header is present');
|
||||
//group headers
|
||||
await page.waitForXPath(`//a[contains(text(), '${settings.test.section}')]`);
|
||||
console.info('group headers are ok');
|
||||
//card
|
||||
await page.waitForSelector(`.mosaic img[src='./logos/${settings.test.logo}']`);
|
||||
console.info('there is a kubernetes card');
|
||||
//click on a card
|
||||
await page.click(`.mosaic img[src='./logos/${settings.test.logo}']`);
|
||||
console.info('it is clickable');
|
||||
//await for a modal
|
||||
await page.waitForSelector(".modal-content");
|
||||
console.info('modal appears');
|
||||
}, 6 * 60 * 1000); //give it up to 1 min to execute
|
||||
});
|
||||
}
|
||||
expect.extend({
|
||||
async toHaveElement(page, selectorOrXpath) {
|
||||
const method = selectorOrXpath.slice(0, 2) === '//' ? '$x' : '$$';
|
||||
const elements = await page[method](selectorOrXpath);
|
||||
const pass = elements.length > 0;
|
||||
const message = () => {
|
||||
return `Element "${selectorOrXpath}" ${this.isNot ? "was not supposed to" : "could not"} be found.`
|
||||
};
|
||||
return { pass, message };
|
||||
},
|
||||
})
|
||||
jest.setTimeout(process.env.SHOW_BROWSER ? 30000 : 30000);
|
||||
|
||||
describe("Normal browser", function() {
|
||||
beforeAll(async function() {
|
||||
browser = await puppeteer.launch({headless: !process.env.SHOW_BROWSER});
|
||||
page = await browser.newPage();
|
||||
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 page.goto(initialUrl);
|
||||
await page.setViewport({ width, height });
|
||||
})
|
||||
afterAll(async function() {
|
||||
browser.close();
|
||||
})
|
||||
mainTest();
|
||||
return page;
|
||||
} catch(ex) {
|
||||
try {
|
||||
console.info('retrying...', ex);
|
||||
browser.close();
|
||||
} catch(ex2) {
|
||||
console.info('failed to close browser', ex2);
|
||||
}
|
||||
return await makePage(initialUrl);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForSelector(page, selector) {
|
||||
await page.waitForFunction(`document.querySelector('${selector}') && document.querySelector('${selector}').clientHeight != 0`);
|
||||
}
|
||||
|
||||
async function waitForSummaryText(page, text) {
|
||||
await page.waitForFunction(`document.querySelector('.summary') && document.querySelector('.summary').innerText.includes('${text}')`);
|
||||
}
|
||||
|
||||
async function waitForHeaderText(page, text) {
|
||||
await page.waitForFunction(`[...document.querySelectorAll('.sh_wrapper')].find( (x) => x.innerText.includes('${text}'))`);
|
||||
}
|
||||
|
||||
// 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("iPhone simulator", function() {
|
||||
beforeAll(async function() {
|
||||
browser = await puppeteer.launch({headless: !process.env.SHOW_BROWSER});
|
||||
page = await browser.newPage();
|
||||
await page.emulate(devices['iPhone X'])
|
||||
})
|
||||
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");
|
||||
});
|
||||
|
||||
afterAll(async function() {
|
||||
browser.close();
|
||||
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();
|
||||
})
|
||||
mainTest();
|
||||
});
|
||||
|
||||
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}/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}/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);
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
const { actualTwitter } = require('../../tools/actualTwitter');
|
||||
|
||||
describe('Twitter URL', () => {
|
||||
describe('when crunchbase data not set', () => {
|
||||
const node = { twitter: 'https://twitter.com/foo' };
|
||||
|
||||
test('returns URL from node', async () => {
|
||||
expect(actualTwitter(node, null)).toBe(node.twitter)
|
||||
})
|
||||
});
|
||||
|
||||
describe('when node does not have twitter URL', () => {
|
||||
const crunchbaseData = { twitter: 'https://twitter.com/foo' };
|
||||
|
||||
test('returns URL from node', async () => {
|
||||
expect(actualTwitter({}, crunchbaseData)).toBe(crunchbaseData.twitter)
|
||||
})
|
||||
});
|
||||
|
||||
describe('when node has twitter URL set to null', () => {
|
||||
const crunchbaseData = { twitter: 'https://twitter.com/foo' };
|
||||
const node = { twitter: null };
|
||||
|
||||
test('returns undefined', async () => {
|
||||
expect(actualTwitter(node, crunchbaseData)).toBe(undefined)
|
||||
})
|
||||
});
|
||||
|
||||
describe('when both node and crunchbase have twitter URL', () => {
|
||||
const node = { twitter: 'https://twitter.com/main' };
|
||||
const crunchbaseData = { twitter: 'https://twitter.com/other' };
|
||||
|
||||
test('returns URL from node', async () => {
|
||||
expect(actualTwitter(node, crunchbaseData)).toBe(node.twitter)
|
||||
})
|
||||
});
|
||||
|
||||
describe('when twitter URL is not set anywhere', () => {
|
||||
const node = {};
|
||||
const crunchbaseData = {};
|
||||
|
||||
test('returns undefined', async () => {
|
||||
expect(actualTwitter(node, crunchbaseData)).toBe(undefined)
|
||||
})
|
||||
});
|
||||
|
||||
describe('cleaning up twitter URL', () => {
|
||||
test('replaces http with https', async () => {
|
||||
const node = { twitter: 'http://twitter.com/foo' };
|
||||
expect(actualTwitter(node)).toBe('https://twitter.com/foo')
|
||||
});
|
||||
|
||||
test('removes www', async () => {
|
||||
const node = { twitter: 'https://www.twitter.com/foo' };
|
||||
expect(actualTwitter(node)).toBe('https://twitter.com/foo')
|
||||
});
|
||||
|
||||
test('query string', async () => {
|
||||
const node = { twitter: 'https://twitter.com/foo?omg' };
|
||||
expect(actualTwitter(node)).toBe('https://twitter.com/foo')
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,22 +0,0 @@
|
|||
import React from 'react';
|
||||
import { OutboundLink } from 'react-ga';
|
||||
import settings from 'project/settings.yml';
|
||||
|
||||
const Ad = () => {
|
||||
|
||||
const entries = settings.ads;
|
||||
|
||||
|
||||
return <div id="kubecon">
|
||||
{ entries.map( (entry) => (
|
||||
<OutboundLink className="sidebar-event"
|
||||
key={entry.image}
|
||||
eventLabel={entry.url}
|
||||
to={entry.url}
|
||||
target="_blank">
|
||||
<img src={entry.image} />
|
||||
</OutboundLink>
|
||||
)) }
|
||||
</div>
|
||||
}
|
||||
export default Ad;
|
|
@ -1,43 +0,0 @@
|
|||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
|
||||
import { MainFullscreenLandscapeContainer, ExtraFullscreenLandscapeContainer } from "./BigPicture";
|
||||
import HomePageContainer from './HomePageContainer';
|
||||
import NotFoundPage from './NotFoundPage';
|
||||
import settings from 'project/settings.yml';
|
||||
const mainSettings = settings.big_picture.main;
|
||||
const extraSettings = settings.big_picture.extra;
|
||||
|
||||
// detect an initial prefix, like /cncf/ or /lfdl/ , but it can be just /
|
||||
const possiblePrefix = window.possiblePrefix || '';
|
||||
const prefix = (possiblePrefix && location.pathname.indexOf(possiblePrefix) === 1) ? (possiblePrefix + '/') : '';
|
||||
window.prefix = prefix;
|
||||
|
||||
// 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 {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<CssBaseline />
|
||||
<Switch>
|
||||
<Route exact path={`/${prefix}`} component={HomePageContainer} />
|
||||
{ extraSettings && <Route exact path={`/${prefix}${extraSettings.url}`} component={ExtraFullscreenLandscapeContainer}/> }
|
||||
<Route exact path={`/${prefix}${mainSettings.url}`} component={MainFullscreenLandscapeContainer}/>
|
||||
<Route path={`/${prefix}`} component={HomePageContainer} />
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
children: PropTypes.element
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -1,261 +0,0 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash'
|
||||
import InternalLink from '../InternalLink';
|
||||
import Fade from '@material-ui/core/Fade';
|
||||
import fields from '../../types/fields';
|
||||
|
||||
const itemWidth = 36;
|
||||
const itemHeight = 32;
|
||||
|
||||
const isLargeFn = function(x) {
|
||||
const relationInfo = _.find(fields.relation.values, {id: x.relation});
|
||||
return !!relationInfo.big_picture_order;
|
||||
}
|
||||
|
||||
const Item = function({zoom, item, x, y, isLarge, onSelectItem}) {
|
||||
if (isLarge) {
|
||||
return new LargeItem({zoom, item, x, y, onSelectItem});
|
||||
}
|
||||
const k = 1;
|
||||
return <div style={{
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
left: (itemWidth * x) * zoom,
|
||||
top: (itemHeight * y) * zoom,
|
||||
width: (itemWidth * k) * zoom,
|
||||
height: (itemHeight * k) * zoom }}
|
||||
key={item.id}
|
||||
>
|
||||
<img src={item.href} style={{
|
||||
width: (itemWidth * k - 2) * zoom,
|
||||
height: (itemHeight * k - 2) * zoom,
|
||||
margin: 2 * zoom,
|
||||
padding: 2 * zoom,
|
||||
border: `${1 * zoom}px solid grey`,
|
||||
borderRadius: 3 * zoom,
|
||||
background: item.oss ? '' : '#eee'
|
||||
}}
|
||||
onClick={ () => onSelectItem(item.id)}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const LargeItem = function({zoom, item, x, y, onSelectItem}) {
|
||||
const k = 2;
|
||||
const z = function(x) {
|
||||
return Math.round(x * zoom * 2) / 2;
|
||||
};
|
||||
const relationInfo = _.find(fields.relation.values, {id: item.relation});
|
||||
const color = relationInfo.big_picture_color;
|
||||
const label = relationInfo.big_picture_label;
|
||||
return <div style={{
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
border: `${z(2)}px solid ${color}`,
|
||||
left: (itemWidth * x + 3) * zoom,
|
||||
top: (itemHeight * y + 3) * zoom,
|
||||
width: (itemWidth * k) * zoom,
|
||||
height: (itemHeight * k - 5) * zoom }}
|
||||
onClick={ () => onSelectItem(item.id)}
|
||||
key={item.id}
|
||||
>
|
||||
<img src={item.href} style={{
|
||||
width: (itemWidth * k - 2 - 5) * zoom,
|
||||
height: (itemHeight * k - 9 - 2 - 10) * zoom,
|
||||
margin: z(2),
|
||||
padding: z(2)
|
||||
}} />
|
||||
<div style={{position: 'absolute', left: 0, right: 0, bottom: 0, height: 10 * zoom, textAlign: 'center', background: color, color: 'white', fontSize: 6.7 * zoom, lineHeight: `${13 * zoom}px`}}>
|
||||
{label}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const HorizontalSubcategory = function({zoom, subcategory, rows, onSelectItem, parentHeight, xRatio }) {
|
||||
const categoryHeight = rows;
|
||||
const total = _.sumBy(subcategory.allItems, function(item) {
|
||||
return isLargeFn(item) ? 4 : 1;
|
||||
});
|
||||
const filteredItems = subcategory.items;
|
||||
let cols = Math.max(Math.ceil(total / categoryHeight ), 2);
|
||||
// what if we have 3 cols but first 2 items are large items, effectively
|
||||
// requiring 4 columns?
|
||||
if (cols % 2 === 1 && subcategory.allItems.slice(0, Math.trunc(cols / 2) + 1).every( (x) => isLargeFn(x))) {
|
||||
cols += 1;
|
||||
}
|
||||
const width = itemWidth * (cols - 1) * xRatio + itemWidth;
|
||||
const height = itemHeight * categoryHeight;
|
||||
const offset = (parentHeight - 20 - height) / 2;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let busy = {};
|
||||
return <div style={{ width: width * zoom, height: height * zoom, top: -40 * zoom, marginTop: (20 + offset) * zoom, position: 'relative' }}>
|
||||
{ subcategory.allItems.map(function(item) {
|
||||
const isVisible = !! _.find(filteredItems, function(x) { return x.id === item.id });
|
||||
const isLarge = isLargeFn(item);
|
||||
const result = {key: item.name, zoom: zoom, item, y: y, x: x, isLarge: isLarge, onSelectItem: onSelectItem};
|
||||
busy[`${x}:${y}`] = true;
|
||||
if (isLarge) {
|
||||
busy[`${x + 1}:${y}`] = true;
|
||||
busy[`${x}:${y+1}`] = true;
|
||||
busy[`${x + 1}:${y+1}`] = true;
|
||||
}
|
||||
while(busy[`${x}:${y}`]) {
|
||||
x += 1;
|
||||
if (x >= cols) {
|
||||
x = 0;
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
return <Fade timeout={1000} in={isVisible}>
|
||||
{new Item({...result, x: result.x * xRatio})}
|
||||
</Fade>;
|
||||
}) }
|
||||
</div>
|
||||
};
|
||||
|
||||
const VerticalSubcategory = function({zoom, subcategory, cols, onSelectItem, xRatio}) {
|
||||
const categoryWidth = cols;
|
||||
const total = _.sumBy(subcategory.allItems, function(item) {
|
||||
return isLargeFn(item) ? 4 : 1;
|
||||
});
|
||||
const filteredItems = subcategory.items;
|
||||
const raws = Math.ceil(total / categoryWidth );
|
||||
const height = itemHeight * raws;
|
||||
const width = itemWidth * categoryWidth;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let busy = {};
|
||||
return <div style={{ left: 5 * zoom, width: width * zoom, height: height * zoom, position: 'relative' }} >
|
||||
{ subcategory.allItems.map(function(item) {
|
||||
const isVisible = !! _.find(filteredItems, function(x) { return x.id === item.id });
|
||||
const isLarge = isLargeFn(item);
|
||||
const result = {key: item.name, zoom: zoom, item, y: y, x: x, isLarge: isLarge, onSelectItem: onSelectItem};
|
||||
busy[`${x}:${y}`] = true;
|
||||
if (isLarge) {
|
||||
busy[`${x + 1}:${y}`] = true;
|
||||
busy[`${x}:${y+1}`] = true;
|
||||
busy[`${x + 1}:${y+1}`] = true;
|
||||
}
|
||||
while(busy[`${x}:${y}`]) {
|
||||
x += 1;
|
||||
if (x >= categoryWidth) {
|
||||
x = 0;
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return <Fade timeout={1000} in={isVisible}>
|
||||
{new Item({...result, x: result.x * xRatio})}
|
||||
</Fade>;
|
||||
}) }
|
||||
</div>
|
||||
};
|
||||
|
||||
const getSubcategoryWidth = function({subcategory, rows}) {
|
||||
const categoryHeight = rows;
|
||||
const total = _.sumBy(subcategory.allItems, function(item) {
|
||||
return isLargeFn(item) ? 4 : 1;
|
||||
});
|
||||
const cols = Math.max(Math.ceil(total / categoryHeight ), 2);
|
||||
const width = itemWidth * cols;
|
||||
console.info(`Subcategory ${subcategory.name} has a width: ${width}`);
|
||||
return width;
|
||||
}
|
||||
|
||||
const HorizontalCategory = function({header, subcategories, rows, width, height, top, left, zoom, color, href, onSelectItem, fitWidth}) {
|
||||
|
||||
let innerWidth = _.sumBy(subcategories, (subcategory) => getSubcategoryWidth({subcategory, rows}));
|
||||
if (subcategories.length > 1) {
|
||||
console.info(`${header} has a width of ${innerWidth}, but expected width is ${width}`);
|
||||
}
|
||||
const xRatio = fitWidth ? (width - 50 ) / innerWidth : 1.05;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute', height: height * zoom, margin: 5 * zoom, width: width * zoom, top: (top - 5) * zoom, left: left * zoom
|
||||
}} className="big-picture-section" >
|
||||
<div style={{position: 'absolute', top: 20 * zoom, height: (height - 20) * zoom, width: 30 * zoom, opacity: 0.5, zIndex: 10}}>
|
||||
<InternalLink to={href}>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}></div>
|
||||
</InternalLink>
|
||||
</div>
|
||||
<div style={{transform: 'rotate(-90deg)', width: (height - 20) * zoom, height: 30 * zoom, top: ((height + 20) / 2 - 30 / 2) * zoom, left: (-(height / 2 - 30/2) + 20/2) * zoom, textAlign: 'center', position: 'absolute', background:color, color: 'white', fontSize: 13 * zoom}}>
|
||||
<div style={{
|
||||
color: 'white',
|
||||
fontSize: 12 * zoom,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
left: '50%',
|
||||
top:'50%'}}>{header}</div>
|
||||
</div>
|
||||
<div style={{width: 40 * zoom, display: 'inline-block'}} />
|
||||
<div style={{position: 'absolute', border: `${1 * zoom}px solid ${color}`, background: 'white', top: 20 * zoom, bottom: 0, left: 30 * zoom, right: 0}}></div>
|
||||
<div style={{position: 'absolute', top: 20 * zoom, bottom: 0, left: 0, right: 0,
|
||||
boxShadow: `0 ${4 * zoom}px ${8 * zoom}px 0 rgba(0, 0, 0, 0.2), 0 ${6 * zoom}px ${20 * zoom}px 0 rgba(0, 0, 0, 0.19)`
|
||||
}}></div>
|
||||
<div style={{position: 'absolute', left: 35 * zoom, top: 0, right: 10 * zoom, bottom: 0, display: 'flex', justifyContent: 'space-between'}}>
|
||||
{subcategories.map(function(subcategory, index, all) {
|
||||
return [
|
||||
<div key={subcategory.name} style={{position: 'relative', fontSize: `${10 * zoom}px`}}>
|
||||
<div style={{position: 'relative', width: '100%', height: 40 * zoom, top: -14 * zoom}}>
|
||||
<span style={{textAlign: 'center', position: 'absolute', width: '100%', minWidth: 100 * zoom, transform: 'translate(-50%, -50%)', left: '50%', top:'50%'}}>
|
||||
<InternalLink to={subcategory.href}>
|
||||
<span style={{
|
||||
color: 'white',
|
||||
fontSize: 10 * zoom
|
||||
}}>{subcategory.name}</span>
|
||||
</InternalLink>
|
||||
</span>
|
||||
</div>
|
||||
<HorizontalSubcategory subcategory={subcategory} rows={rows} zoom={zoom} onSelectItem={onSelectItem} parentHeight={height} xRatio={xRatio} key={subcategory.name}/>
|
||||
</div>,
|
||||
index !== all.length - 1 && <div key={index} style={{ top: 40 * zoom, height: `calc(100% - ${50 * zoom}px)`, border: `${Math.max(Math.round(zoom) / 2, 0.5)}px solid #777`, position: 'relative' }}></div>
|
||||
]
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
const VerticalCategory = function({header, subcategories, cols = 6, top, left, width, height, color, zoom, href, onSelectItem}) {
|
||||
const xRatio = 1.07;
|
||||
return (<div style={{}}>
|
||||
<div style={{
|
||||
position: 'absolute', top: top -5 * zoom, left: left * zoom, height: height * zoom, margin: 5 * zoom, width: (width + 2) * zoom, background: 'white', border: `${1 * zoom}px solid ${color}`,
|
||||
boxShadow: `0 ${4 * zoom}px ${8 * zoom}px 0 rgba(0, 0, 0, 0.2), 0 ${6 * zoom}px ${20 * zoom}px 0 rgba(0, 0, 0, 0.19)`
|
||||
}} className="big-picture-section">
|
||||
<div style={{ width: width * zoom, height: 20 * zoom, lineHeight: `${20 * zoom}px`, textAlign: 'center', color: 'white', background: color, fontSize: 12 * zoom}}>
|
||||
<InternalLink to={href}>
|
||||
<span style={{
|
||||
color: 'white',
|
||||
fontSize: 12 * zoom
|
||||
}}>{header}</span>
|
||||
</InternalLink>
|
||||
</div>
|
||||
{subcategories.map(function(subcategory) {
|
||||
return <div key={subcategory.name} style={{position: 'relative'}}>
|
||||
<div style={{ fontSize: 10 * zoom, lineHeight: `${15 * zoom}px`, textAlign: 'center', color: color}}>
|
||||
<InternalLink to={subcategory.href}>
|
||||
<span style={{
|
||||
color: color,
|
||||
fontSize: 10 * zoom
|
||||
}}>{subcategory.name}</span>
|
||||
</InternalLink>
|
||||
</div>
|
||||
<VerticalSubcategory subcategory={subcategory} zoom={zoom} cols={cols} onSelectItem={onSelectItem} xRatio={xRatio} />
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
export {
|
||||
HorizontalCategory,
|
||||
VerticalCategory
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import qs from 'query-string';
|
||||
import FullscreenLandscape from './FullscreenLandscape';
|
||||
import { bigPictureMethods } from '../../utils/itemsCalculator';
|
||||
import settings from 'project/settings.yml'
|
||||
const extraSettings = settings.big_picture.extra;
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
ready: state.main.ready,
|
||||
groupedItems: state.main.ready && bigPictureMethods[extraSettings.method](state),
|
||||
landscapeSettings: settings.big_picture.extra,
|
||||
showPreview:location.search.indexOf('preview') === -1,
|
||||
version:qs.parse(location.search).version
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FullscreenLandscape);
|
|
@ -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 settings from 'project/settings.yml'
|
||||
|
||||
const mainSettings = settings.big_picture.main;
|
||||
const extraSettings = settings.big_picture.extra || {};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
groupedItems: getGroupedItemsForBigPicture(state),
|
||||
zoom: state.main.zoom,
|
||||
landscapeSettings: extraSettings,
|
||||
showPreview: true
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
onSelectItem: changeSelectedItemId,
|
||||
switchToOther: () => changeMainContentMode(mainSettings.url)
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LandscapeContent);
|
|
@ -1,24 +0,0 @@
|
|||
// locate zoom buttons
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
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" style={{position:'absolute', zIndex: 7, display: 'inline-block', width: 40}}>
|
||||
{ isFullscreen ?
|
||||
<IconButton onClick={disableFullscreen}>
|
||||
<FullscreenExitIcon />
|
||||
</IconButton>
|
||||
:
|
||||
<IconButton onClick={enableFullscreen} >
|
||||
<FullscreenIcon />
|
||||
</IconButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
export default 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,69 +0,0 @@
|
|||
import React from 'react';
|
||||
import LandscapeContent from './LandscapeContent';
|
||||
import HomePageUrlContainer from '../HomePageUrlContainer';
|
||||
import qs from 'query-string';
|
||||
|
||||
const Fullscreen = ({ready, groupedItems, landscapeSettings, showPreview, version}) => {
|
||||
if (!ready) {
|
||||
return (
|
||||
<div>
|
||||
<HomePageUrlContainer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{zoom: 4, fontFamily: 'roboto'}}>
|
||||
<HomePageUrlContainer />
|
||||
<div className="gradient-bg" style={{
|
||||
width: landscapeSettings.fullscreen_size.width,
|
||||
height: landscapeSettings.fullscreen_size.height,
|
||||
position: 'relative'}}>
|
||||
<LandscapeContent style={{top: 50, left: 20}} groupedItems={groupedItems} zoom={1} showPreview={showPreview} landscapeSettings={landscapeSettings} />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 15,
|
||||
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>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 15,
|
||||
right: 12,
|
||||
fontSize: 6,
|
||||
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 Fullscreen;
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {HorizontalCategory, VerticalCategory } from './Elements';
|
||||
import LandscapeInfo from './LandscapeInfo';
|
||||
import OtherLandscapeLink from './OtherLandscapeLink';
|
||||
|
||||
const LandscapeContent = ({groupedItems, onSelectItem, style, showPreview, switchToOther, zoom, landscapeSettings }) => {
|
||||
const elements = landscapeSettings.elements.map(function(element) {
|
||||
if (element.type === 'HorizontalCategory') {
|
||||
const cat = _.find(groupedItems, {key: element.category});
|
||||
return <HorizontalCategory {...cat} {..._.pick(element, ['rows','width','height','top','left','color']) }
|
||||
fitWidth={element.fit_width}
|
||||
zoom={zoom}
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
}
|
||||
if (element.type === 'VerticalCategory') {
|
||||
const cat = _.find(groupedItems, {key: element.category});
|
||||
return <VerticalCategory {...cat} {..._.pick(element, ['cols','width','height','top','left','color']) }
|
||||
zoom={zoom}
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
}
|
||||
if (element.type === 'OtherLandscapeLink') {
|
||||
return <OtherLandscapeLink {..._.pick(element, ['width','height','top','left','color', 'layout', 'title', 'url']) }
|
||||
zoom={zoom}
|
||||
showPreview={showPreview}
|
||||
onClick={switchToOther}
|
||||
/>
|
||||
}
|
||||
if (element.type === 'LandscapeInfo') {
|
||||
return <LandscapeInfo {..._.pick(element, ['width', 'height', 'top', 'left']) } childrenInfo={element.children}
|
||||
zoom={zoom}
|
||||
/>
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return (<div style={{...style, position: 'relative', ...landscapeSettings.size }}>
|
||||
{elements}
|
||||
</div>);
|
||||
};
|
||||
|
||||
export default LandscapeContent;
|
|
@ -1,50 +0,0 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
const LandscapeInfo = ({zoom, width, height, top, left, childrenInfo}) => {
|
||||
const children = childrenInfo.map(function(info) {
|
||||
const positionProps = {
|
||||
position: 'absolute',
|
||||
top: _.isUndefined(info.top) ? null : info.top * zoom,
|
||||
left: _.isUndefined(info.left) ? null : info.left * zoom,
|
||||
right: _.isUndefined(info.right) ? null : info.right * zoom,
|
||||
bottom: _.isUndefined(info.bottom) ? null : info.bottom * zoom ,
|
||||
width: _.isUndefined(info.width) ? null : info.width * zoom,
|
||||
height: _.isUndefined(info.height) ? null : info.height * zoom
|
||||
};
|
||||
if (info.type === 'text') {
|
||||
return <div style={{
|
||||
...positionProps,
|
||||
fontSize: info.font_size * zoom,
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'justify'
|
||||
}}> {info.text} </div>
|
||||
}
|
||||
if (info.type === 'title') {
|
||||
return <div style= {{
|
||||
...positionProps,
|
||||
fontSize: info.font_size * zoom,
|
||||
color: '#666'
|
||||
}}>{info.title}</div>
|
||||
}
|
||||
if (info.type === 'image') {
|
||||
return <img src={`/images/${info.image}`} style={{
|
||||
...positionProps
|
||||
}} />
|
||||
}
|
||||
});
|
||||
|
||||
return <div style={{
|
||||
position: 'absolute',
|
||||
width: width * zoom,
|
||||
height: (height - 20) * zoom,
|
||||
top: top * zoom,
|
||||
left: left * zoom,
|
||||
border: `${1 * zoom}px solid black`,
|
||||
background: 'white',
|
||||
borderRadius: 15 * zoom,
|
||||
marginTop: 20 * zoom,
|
||||
boxShadow: `0 ${4 * zoom}px ${8 * zoom}px 0 rgba(0, 0, 0, 0.2), 0 ${6 * zoom}px ${20 * zoom}px 0 rgba(0, 0, 0, 0.19)`
|
||||
}}>{children}</div>
|
||||
}
|
||||
export default LandscapeInfo;
|
|
@ -1,18 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import qs from 'query-string';
|
||||
import FullscreenLandscape from './FullscreenLandscape';
|
||||
import { bigPictureMethods } from '../../utils/itemsCalculator';
|
||||
import settings from 'project/settings.yml'
|
||||
const mainSettings = settings.big_picture.main;
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
ready: state.main.ready,
|
||||
groupedItems: state.main.ready && bigPictureMethods[mainSettings.method](state),
|
||||
landscapeSettings: settings.big_picture.main,
|
||||
showPreview:location.search.indexOf('preview') === -1,
|
||||
version:qs.parse(location.search).version
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FullscreenLandscape);
|
|
@ -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 settings from 'project/settings.yml'
|
||||
|
||||
const mainSettings = settings.big_picture.main;
|
||||
const extraSettings = settings.big_picture.extra || {};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
groupedItems: getGroupedItemsForBigPicture(state),
|
||||
zoom: state.main.zoom,
|
||||
landscapeSettings: mainSettings,
|
||||
showPreview: true
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
onSelectItem: changeSelectedItemId,
|
||||
switchToOther: () => changeMainContentMode(extraSettings.url)
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LandscapeContent);
|
|
@ -1,30 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const OtherLandscapeLink = function({zoom, top, left, height, width, color, showPreview, onClick, title, url, layout}) {
|
||||
if (layout === 'category') {
|
||||
return (<div style={{
|
||||
position: 'absolute', top: (top - 5) * zoom, left: left * zoom, height: height * zoom, margin: 5 * zoom, width: (width + 2) * zoom, background: 'white', border: `${1 * zoom}px solid ${color}`,
|
||||
cursor: 'pointer',
|
||||
boxShadow: `0 ${4 * zoom}px ${8 * zoom}px 0 rgba(0, 0, 0, 0.2), 0 ${6 * zoom}px ${20 * zoom}px 0 rgba(0, 0, 0, 0.19)`
|
||||
}} onClick={onClick} >
|
||||
<div style={{ width: width * zoom, height: 20 * zoom, lineHeight: `${20 * zoom}px`, textAlign: 'center', color: 'white', background: color, fontSize: 12 * zoom}}> {title} </div>
|
||||
{ showPreview &&
|
||||
<div style={{ width: (width - 10) * zoom, height: (height - 40) * zoom, margin: 5 * zoom,
|
||||
backgroundImage: `url("/images/${url}_preview.png")`, backgroundSize: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }}></div>
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
if (layout === 'subcategory') {
|
||||
return (<div style={{
|
||||
position: 'absolute', top: (top -5) * zoom, left: left * zoom, height: height * zoom, margin: 5 * zoom, width: (width + 2) * zoom,
|
||||
cursor: 'pointer',
|
||||
}} onClick={onClick} >
|
||||
<div style={{ width: width * zoom, height: 20 * zoom, lineHeight: `${20 * zoom}px`, textAlign: 'center', color: 'white', fontSize: 11 * zoom}}> {title}</div>
|
||||
{ showPreview &&
|
||||
<div style={{ width: (width - 10) * zoom, height: (height - 40) * zoom, margin: 5 * zoom,
|
||||
backgroundImage: `url("/images/${url}_preview.png")`, backgroundSize: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }}></div>
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
export default OtherLandscapeLink;
|
|
@ -1,23 +0,0 @@
|
|||
import React from 'react';
|
||||
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"
|
||||
value={mainContentMode}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
onChange={(_event, value) => changeMainContentMode(value)}
|
||||
>
|
||||
{ cards.map(function(card) {
|
||||
return <Tab key={card.mode} label={card.title} component={(props) => <InternalLink to={card.url} {...props}></InternalLink>} value={card.mode} />
|
||||
}) }
|
||||
</Tabs>
|
||||
}
|
||||
export default SwitchButton;
|
|
@ -1,25 +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 = [{ title: 'Card Mode', mode: 'card'}];
|
||||
const landscapes = _.map(settings.big_picture, function(section) {
|
||||
return {
|
||||
title: section.name,
|
||||
mode: section.url
|
||||
}
|
||||
});
|
||||
const cards = mainCard.concat(landscapes);
|
||||
|
||||
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,10 +0,0 @@
|
|||
// locate zoom buttons
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const Zoom = function({zoom, children}) {
|
||||
return <div style={{position:'relative', zoom: zoom}}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
export default Zoom;
|
|
@ -1,20 +0,0 @@
|
|||
// locate zoom buttons
|
||||
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" style={{position:'absolute'}}>
|
||||
<IconButton disabled={!canZoomOut} onClick={onZoomOut}>
|
||||
<RemoveCircleIcon />
|
||||
</IconButton>
|
||||
<Button onClick={onZoomReset}>{zoomText}</Button>
|
||||
<IconButton disabled={!canZoomIn} onClick={onZoomIn}>
|
||||
<AddCircleIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
export default 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,19 +0,0 @@
|
|||
import MainLandscapeContentContainer from './MainLandscapeContentContainer';
|
||||
import ExtraLandscapeContentContainer from './ExtraLandscapeContentContainer';
|
||||
import SwitchButtonContainer from './SwitchButtonContainer';
|
||||
import ZoomContainer from './ZoomContainer';
|
||||
import ZoomButtonsContainer from './ZoomButtonsContainer';
|
||||
import MainFullscreenLandscapeContainer from './MainFullscreenLandscapeContainer';
|
||||
import ExtraFullscreenLandscapeContainer from './ExtraFullscreenLandscapeContainer';
|
||||
import FullscreenButtonContainer from './FullscreenButtonContainer';
|
||||
|
||||
export {
|
||||
MainLandscapeContentContainer,
|
||||
ExtraLandscapeContentContainer,
|
||||
SwitchButtonContainer,
|
||||
ZoomContainer,
|
||||
ZoomButtonsContainer,
|
||||
MainFullscreenLandscapeContainer,
|
||||
ExtraFullscreenLandscapeContainer,
|
||||
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 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) {
|
||||
console.info(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 CheckboxSelector;
|
|
@ -1,40 +0,0 @@
|
|||
import React from 'react';
|
||||
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) {
|
||||
console.info(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 ComboboxSelector;
|
|
@ -1,23 +0,0 @@
|
|||
import React from 'react';
|
||||
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 ComboboxSelector;
|
|
@ -1,143 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import isMobile from '../utils/isMobile';
|
||||
|
||||
export default class AutoSizer extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
onResize: () => {},
|
||||
disableHeight: false,
|
||||
disableWidth: false,
|
||||
style: {},
|
||||
};
|
||||
|
||||
state = {
|
||||
height: this.props.defaultHeight || 0,
|
||||
width: this.props.defaultWidth || 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();
|
||||
this._timerId = setInterval( () => this._onResize(), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._detectElementResize && this._parentNode) {
|
||||
this._detectElementResize.removeResizeListener(
|
||||
this._parentNode,
|
||||
this._onResize,
|
||||
);
|
||||
}
|
||||
clearTimeout(this._timerId);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
disableHeight,
|
||||
disableWidth,
|
||||
style,
|
||||
} = this.props;
|
||||
const {height, width} = 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 outerStyle = {overflow: 'visible'};
|
||||
const childParams = {};
|
||||
|
||||
if (!disableHeight) {
|
||||
outerStyle.height = 0;
|
||||
childParams.height = height;
|
||||
}
|
||||
|
||||
if (!disableWidth) {
|
||||
outerStyle.width = 0;
|
||||
childParams.width = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Avoid rendering children before the initial measurements have been collected.
|
||||
* At best this would just be wasting cycles.
|
||||
* Add this check into version 10 though as it could break too many ref callbacks in version 9.
|
||||
* Note that if default width/height props were provided this would still work with SSR.
|
||||
if (
|
||||
height !== 0 &&
|
||||
width !== 0
|
||||
) {
|
||||
child = children({ height, width })
|
||||
}
|
||||
*/
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
ref={this._setRef}
|
||||
style={{
|
||||
...outerStyle,
|
||||
...style,
|
||||
}}>
|
||||
{children(childParams)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_onResize = () => {
|
||||
const {disableHeight, disableWidth, onResize} = this.props;
|
||||
|
||||
if (this._parentNode) {
|
||||
// Guard against AutoSizer component being removed from the DOM immediately after being added.
|
||||
// This can result in invalid style values which can result in NaN values if we don't handle them.
|
||||
// See issue #150 for more context.
|
||||
|
||||
|
||||
const style = window.getComputedStyle(this._parentNode) || {};
|
||||
const paddingLeft = parseInt(style.paddingLeft, 10) || 0;
|
||||
const paddingRight = parseInt(style.paddingRight, 10) || 0;
|
||||
const paddingTop = parseInt(style.paddingTop, 10) || 0;
|
||||
const paddingBottom = parseInt(style.paddingBottom, 10) || 0;
|
||||
|
||||
const height = window.innerHeight;
|
||||
const width = window.innerWidth;
|
||||
|
||||
const rect = this._parentNode.getBoundingClientRect();
|
||||
const isFullscreen = document.querySelector('html').classList.contains('fullscreen');
|
||||
|
||||
const offset = isMobile ? 10 : isFullscreen ? 0 : 25;
|
||||
|
||||
const newHeight = height - paddingTop - paddingBottom - rect.bottom + offset;
|
||||
const newWidth = width - paddingLeft - paddingRight - rect.left;
|
||||
|
||||
if (
|
||||
(!disableHeight && this.state.height !== newHeight) ||
|
||||
(!disableWidth && this.state.width !== newWidth)
|
||||
) {
|
||||
this.setState({
|
||||
height: newHeight,
|
||||
width: newWidth
|
||||
});
|
||||
|
||||
onResize({height, width});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_setRef = (autoSizer) => {
|
||||
this._autoSizer = 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,8 +0,0 @@
|
|||
import React from 'react';
|
||||
const EmbeddedFooter = () => {
|
||||
const originalLink = window.location.pathname.replace('&embed=yes', '').replace('&embed=true', '');
|
||||
return <h1 style={{ marginTop: 20, width: '100%', textAlign: 'center' }}>
|
||||
<a target="_blank" href={originalLink}>View</a> the full interactive landscape
|
||||
</h1>
|
||||
}
|
||||
export default EmbeddedFooter;
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
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 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,53 +0,0 @@
|
|||
import React from 'react';
|
||||
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 Filters;
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react';
|
||||
import { OutboundLink } from 'react-ga';
|
||||
import settings from 'project/settings.yml'
|
||||
|
||||
const Footer = () => {
|
||||
return <div style={{ marginTop: 10, fontSize:'9pt', width: '100%', textAlign: 'center' }}>
|
||||
Crunchbase data is used under license from Crunchbase to {settings.global.short_name}. For more information, please see the <OutboundLink eventLabel="crunchbase-terms" to={`https://github.com/${settings.global.repo}/blob/master/README.md#license`} target="_blank">license</OutboundLink> info.
|
||||
</div>
|
||||
}
|
||||
export default 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,14 +0,0 @@
|
|||
import React from 'react';
|
||||
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 Grouping;
|
|
@ -1,31 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ComboboxSelector from './ComboboxSelector';
|
||||
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
|
||||
};
|
||||
}).filter(function(x) {
|
||||
return ! fields[x.id].hideInGrouping;
|
||||
}));
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
value: state.main.grouping,
|
||||
options: options
|
||||
});
|
||||
const onChange = function(newValue) {
|
||||
return changeGrouping(newValue);
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
onChange: onChange
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ComboboxSelector);
|
|
@ -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,19 +0,0 @@
|
|||
import React from 'react';
|
||||
import { OutboundLink } from 'react-ga';
|
||||
import settings from 'project/settings.yml';
|
||||
|
||||
const Header = ({reset}) => {
|
||||
return (
|
||||
<div className="header_container">
|
||||
<div className="header">
|
||||
<span className="landscape-logo"><img onClick={reset} src="./images/left-logo.svg" /></span>
|
||||
<OutboundLink eventLabel="projet" to={settings.global.company_url} target="_blank" rel="noopener noreferrer" className="landscapeapp-logo">
|
||||
<img src="./images/right-logo.svg" />
|
||||
</OutboundLink>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
|
@ -1,12 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Header from './Header';
|
||||
import { resetParameters } from '../reducers/mainReducer.js';
|
||||
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
});
|
||||
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,224 +0,0 @@
|
|||
import React from 'react';
|
||||
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 Presets from './Presets';
|
||||
import Ad from './Ad';
|
||||
import AutoSizer from './CustomAutoSizer';
|
||||
import {
|
||||
MainLandscapeContentContainer,
|
||||
ExtraLandscapeContentContainer,
|
||||
SwitchButtonContainer,
|
||||
ZoomButtonsContainer,
|
||||
FullscreenButtonContainer
|
||||
} from './BigPicture';
|
||||
import MainContentContainer from './MainContentContainer';
|
||||
import HomePageUrlContainer from './HomePageUrlContainer';
|
||||
import HomePageScrollerContainer from './HomePageScrollerContainer';
|
||||
import ResetFiltersContainer from './ResetFiltersContainer';
|
||||
import ItemDialogContainer from './ItemDialogContainer';
|
||||
import ItemDialogButtonsContainer from './ItemDialogButtonsContainer';
|
||||
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 isMobile from '../utils/isMobile';
|
||||
import isGoogle from '../utils/isGoogle';
|
||||
import bus from '../reducers/bus';
|
||||
import settings from 'project/settings.yml'
|
||||
|
||||
const mainSettings = settings.big_picture.main;
|
||||
const extraSettings = settings.big_picture.extra || {};
|
||||
|
||||
const state = {
|
||||
lastScrollPosition: 0
|
||||
};
|
||||
|
||||
bus.on('scrollToTop', function() {
|
||||
document.scrollingElement.scrollTop = 0;
|
||||
});
|
||||
|
||||
|
||||
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.scrollTop;
|
||||
}
|
||||
document.querySelector('html').classList.add('has-selected-item');
|
||||
document.scrollingElement.scrollTop = 0;
|
||||
} else {
|
||||
document.querySelector('html').classList.remove('has-selected-item');
|
||||
if (document.querySelector('.iphone-scroller')) {
|
||||
document.scrollingElement.scrollTop = state.lastScrollPosition;
|
||||
}
|
||||
}
|
||||
//try to get a current scroll if we are in a normal mode
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
function handleShadowClick(e) {
|
||||
if (!(isIphone && hasSelectedItem)) {
|
||||
return;
|
||||
}
|
||||
if (window.matchMedia("(orientation: portrait)").matches) {
|
||||
const x = e.clientX / window.innerWidth;
|
||||
const y = e.clientY / window.innerHeight;
|
||||
const marginX = 0.125;
|
||||
const marginY = 0.06;
|
||||
if ( x > marginX && x < 1 - marginX && y > marginY && y < 1 - marginY ) {
|
||||
console.info('a click inside the mask, ignoring');
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
if (window.matchMedia("(orientation: landscape)").matches) {
|
||||
const x = e.clientX / window.innerWidth;
|
||||
const y = e.clientY / window.innerHeight;
|
||||
const marginX = 0.07;
|
||||
const marginY = 0.1;
|
||||
if ( x > marginX && x < 1 - marginX && y > marginY) {
|
||||
console.info('a click inside the mask, ignoring');
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hideTopPart = isEmbed || isFullscreen || (isMobile && isBigPicture);
|
||||
|
||||
return (
|
||||
<div onClick={handleShadowClick} >
|
||||
<HomePageScrollerContainer/>
|
||||
<ItemDialogContainer/>
|
||||
{ isIphone && <ItemDialogButtonsContainer/> }
|
||||
<div className={classNames('app',{'filters-opened' : filtersVisible, 'background': isIphone && hasSelectedItem})}>
|
||||
<div className={classNames({"shadow": isIphone && hasSelectedItem})} />
|
||||
<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" onClick={showFilters}><MenuIcon /></IconButton> }
|
||||
{ !isEmbed && !isFullscreen && <div className="sidebar">
|
||||
<div className="sidebar-scroll">
|
||||
<IconButton className="sidebar-collapse" onClick={hideFilters}><CloseIcon /></IconButton>
|
||||
<ResetFiltersContainer />
|
||||
<Grouping/>
|
||||
<Sorting/>
|
||||
<Filters />
|
||||
<Presets />
|
||||
<ExportCsvContainer />
|
||||
<Ad />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="app-overlay" onClick={hideFilters}></div>
|
||||
|
||||
<HomePageUrlContainer />
|
||||
|
||||
<div className={classNames('main', {'embed': isEmbed})}>
|
||||
{ isMobile && <SwitchButtonContainer /> }
|
||||
{ !hideTopPart && <div className="disclaimer">
|
||||
<span dangerouslySetInnerHTML={{__html: settings.home.header}} />
|
||||
Please <a target="_blank" href={`https://github.com/${settings.global.repo}`}>open</a> a pull request to
|
||||
correct any issues. Greyed logos are not open source. Last Updated: {window.lastUpdated}
|
||||
</div> }
|
||||
{ !hideTopPart && <SummaryContainer /> }
|
||||
{ !isMobile && <SwitchButtonContainer /> }
|
||||
{ isBigPicture &&
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<div style={{width:width, height: height, position: 'relative', background: 'rgb(134,175,188)'}}>
|
||||
<ZoomButtonsContainer />
|
||||
<FullscreenButtonContainer />
|
||||
<div style={{width: '100%', height: '100%', position: 'relative', overflow: 'scroll', padding: 10}}>
|
||||
{ mainContentMode === mainSettings.url && <MainLandscapeContentContainer /> }
|
||||
{ mainContentMode === extraSettings.url && <ExtraLandscapeContentContainer /> }
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
||||
}
|
||||
{ !isBigPicture && <MainContentContainer/> }
|
||||
{ !isEmbed && !isBigPicture && <Footer/> }
|
||||
{ isEmbed && <EmbeddedFooter/> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default 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';
|
||||
|
||||
const defaultTitle = `CNCF Cloud Native Interactive Landscape`;
|
||||
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,17 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import getGroupedItems from '../utils/itemsCalculator';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
groupedItems: getGroupedItems(state)
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
const Component = function() {
|
||||
setTimeout(function() {
|
||||
document.scrollingElement.scrollTop = 0;
|
||||
}, 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Component);
|
|
@ -1,41 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import createSelector from '../utils/createSelector';
|
||||
import { parseUrl } from '../utils/syncToUrl';
|
||||
import { changeParameters} from '../reducers/mainReducer';
|
||||
|
||||
// var bouncedFn = _.debounce((fn, args) => {console.info('Real Change!', args);fn(args)}, 10000);
|
||||
import { history} from '../store/configureStore';
|
||||
|
||||
history.listen(function(x) {
|
||||
console.info('Url changed: ', x);
|
||||
});
|
||||
|
||||
|
||||
|
||||
const getParameters = createSelector(
|
||||
(state) => state.routing.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
|
||||
console.info('Want to change : ', JSON.stringify(info));
|
||||
window.setTimeout(() => changeParameters(info), 1);
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(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,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import isEmbed from '../utils/isEmbed';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
const skipDefaultHandler = (e) => e.preventDefault();
|
||||
const InternalLink = ({to, children, onClick, ...other}) => {
|
||||
if (onClick) {
|
||||
other.onClick = function(e) {
|
||||
skipDefaultHandler(e);
|
||||
onClick();
|
||||
};
|
||||
}
|
||||
if (isEmbed) {
|
||||
return <span {...other}>{children}</span>;
|
||||
} else {
|
||||
return <NavLink {...other} to={to}>{children}</NavLink>
|
||||
}
|
||||
}
|
||||
export default 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,49 +0,0 @@
|
|||
import React from 'react';
|
||||
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 isIphone from '../utils/isIphone';
|
||||
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;
|
||||
}
|
||||
if (isIphone) {
|
||||
if (!itemInfo) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={classNames('modal', 'product', {nonoss : recentItemInfo.oss === false})} style={getRelationStyle(recentItemInfo.relation)} >
|
||||
{ /* Note - we move buttons away from here to the HomePage because of Safari Issues */ }
|
||||
{ <ItemDialogContent itemInfo={itemInfo || lastItemInfo}/> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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 ItemDialog;
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
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 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,370 +0,0 @@
|
|||
import React from 'react';
|
||||
import Timeline from 'react-twitter-widgets/dist/components/Timeline.js';
|
||||
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 'react-ga';
|
||||
import millify from 'millify';
|
||||
import relativeDate from 'relative-date';
|
||||
import { filtersToUrl } from '../utils/syncToUrl';
|
||||
import formatNumber from '../utils/formatNumber';
|
||||
import isMobile from '../utils/isMobile';
|
||||
import InternalLink from './InternalLink';
|
||||
import '../styles/itemModal.scss';
|
||||
import fields from '../types/fields';
|
||||
import isGoogle from '../utils/isGoogle';
|
||||
import settings from 'project/settings.yml';
|
||||
|
||||
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 projectTag = function({relation, member, project}) {
|
||||
if (relation === false) {
|
||||
return null;
|
||||
}
|
||||
const { prefix, tag } = _.find(fields.relation.values, {id: relation});
|
||||
|
||||
if (settings.global.flags.cncf_sandbox) {
|
||||
if (project === 'sandbox') {
|
||||
return (<InternalLink to={filtersToUrl({filters:{relation: project}})} className="tag tag-blue">
|
||||
<span className="tag-name">Cloud Native</span>
|
||||
<span className="tag-value">Sandbox Project</span>
|
||||
</InternalLink>)
|
||||
}
|
||||
}
|
||||
|
||||
if (relation === 'member' || relation === 'company') {
|
||||
console.info(settings.membership, member);
|
||||
const { name, label } = settings.membership[member];
|
||||
return (<InternalLink to={filtersToUrl({filters:{relation: relation}})} className="tag tag-blue">
|
||||
<span className="tag-name">{name}</span>
|
||||
<span className="tag-value">{label}</span>
|
||||
</InternalLink>)
|
||||
}
|
||||
|
||||
return (<InternalLink to={filtersToUrl({filters:{relation: relation}})} className="tag tag-blue">
|
||||
<span className="tag-name">{prefix}</span>
|
||||
<span className="tag-value">{tag}</span>
|
||||
</InternalLink>)
|
||||
};
|
||||
|
||||
const openSourceTag = function(oss) {
|
||||
if (!oss) {
|
||||
return null;
|
||||
}
|
||||
return (<InternalLink to={filtersToUrl({grouping: 'license', filters: {license: 'Open Source'}})} className="tag tag-orange">
|
||||
<span className="tag-value">Open Source Software</span>
|
||||
</InternalLink>)
|
||||
}
|
||||
const licenseTag = function({relation, license}) {
|
||||
if (relation === 'company') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = _.find(fields.license.values, {id: license}).label;
|
||||
return (<InternalLink to={filtersToUrl({grouping: 'license', filters:{license: license}})} className="tag tag-purple">
|
||||
<span className="tag-name">License</span>
|
||||
<span className="tag-value">{text}</span>
|
||||
</InternalLink>);
|
||||
}
|
||||
const badgeTag = function(itemInfo) {
|
||||
if (!itemInfo.bestPracticeBadgeId) {
|
||||
if (itemInfo.oss) {
|
||||
const emptyUrl="https://bestpractices.coreinfrastructure.org/";
|
||||
return (<OutboundLink eventLabel={emptyUrl} to={emptyUrl} target="_blank" 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' : ('In progress: ' + itemInfo.bestPracticePercentage + '%');
|
||||
return (<OutboundLink eventLabel={url} to={url} target="_blank" className="tag tag-grass">
|
||||
<span className="tag-name">CII Best Practices</span>
|
||||
<span className="tag-value">{label}</span>
|
||||
</OutboundLink>);
|
||||
}
|
||||
|
||||
function handleUp() {
|
||||
productScrollEl.scrollBy({top: -200, behavior: 'smooth'});
|
||||
}
|
||||
function handleDown() {
|
||||
productScrollEl.scrollBy({top: 200, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
const $script = require('scriptjs'); // eslint-disable-line global-require
|
||||
$script('https://platform.twitter.com/widgets.js', 'twitter-widgets');
|
||||
|
||||
|
||||
const ItemDialogContent = ({itemInfo}) => {
|
||||
|
||||
|
||||
|
||||
|
||||
const linkToOrganization = filtersToUrl({grouping: 'organization', filters: {organization: itemInfo.organization}});
|
||||
const itemCategory = function(path) {
|
||||
var separator = <span className="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 eventLabel={itemInfo.twitter} to={itemInfo.twitter} target="_blank">{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 eventLabel={itemInfo.twitter} to={itemInfo.twitter} target="_blank">{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 eventLabel={itemInfo.firstCommitLink} to={itemInfo.firstCommitLink} target="_blank">{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 eventLabel={itemInfo.contributorsLink} to={itemInfo.contributorsLink} target="_blank">{itemInfo.contributorsCount}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 = 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
|
||||
target="_blank"
|
||||
eventLabel={itemInfo.crunchbase + '#section-funding-rounds'}
|
||||
to={itemInfo.crunchbase + '#section-funding-rounds'}
|
||||
>{'$' + millify(itemInfo.amount)}
|
||||
</OutboundLink>
|
||||
</div>
|
||||
}
|
||||
{ itemInfo.amountKind !== 'funding' &&
|
||||
<div className="product-property-value tight-col col-60">
|
||||
<OutboundLink
|
||||
target="_blank"
|
||||
eventLabel={'https://finance.yahoo.com/quote/' + itemInfo.yahoo_finance_data.effective_ticker}
|
||||
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 target="_blank" eventLabel={"https://finance.yahoo.com/quote/" + itemInfo.yahoo_finance_data.effective_ticker} 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 eventLabel={itemInfo.latestCommitLink} to={itemInfo.latestCommitLink} target="_blank">{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 eventLabel={itemInfo.releaseLink} to={itemInfo.releaseLink} target="_blank">{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>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="modal-content">
|
||||
<KeyHandler keyEventName="keydown" keyValue="ArrowUp" onKeyHandle={handleUp} />
|
||||
<KeyHandler keyEventName="keydown" keyValue="ArrowDown" onKeyHandle={handleDown} />
|
||||
<div className="product-logo" style={getRelationStyle(itemInfo.relation)}>
|
||||
<img src={itemInfo.href} className='product-logo-img'/>
|
||||
</div>
|
||||
|
||||
<div className="product-tags">
|
||||
<div>{projectTag(itemInfo)}</div>
|
||||
<div>{openSourceTag(itemInfo.oss)}</div>
|
||||
<div>{licenseTag(itemInfo)}</div>
|
||||
<div>{badgeTag(itemInfo)}</div>
|
||||
</div>
|
||||
|
||||
<div className="product-scroll" ref={(x) => productScrollEl = x }>
|
||||
|
||||
<div className="product-main">
|
||||
<div className="product-name">{itemInfo.name}</div>
|
||||
<div className="product-parent"><InternalLink to={linkToOrganization}>{itemInfo.organization}</InternalLink></div>
|
||||
<div className="product-category">{itemCategory(itemInfo.landscape)}</div>
|
||||
<div className="product-description">{itemInfo.description}</div>
|
||||
</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 eventLabel={itemInfo.homepage_url} to={itemInfo.homepage_url} target="_blank">{itemInfo.homepage_url}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
{itemInfo.repo_url &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20">Repository</div>
|
||||
<div className="product-property-value product-repo col col-80">
|
||||
<OutboundLink eventLabel={itemInfo.repo_url} to={itemInfo.repo_url} target="_blank">{itemInfo.repo_url}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{itemInfo.starsAsText &&
|
||||
<div className="product-property row">
|
||||
<div className="product-property-name col col-20"></div>
|
||||
<div className="product-property-value col col-80">
|
||||
<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 eventLabel={itemInfo.crunchbase} to={itemInfo.crunchbase} target="_blank">{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 eventLabel={itemInfo.crunchbaseData.linkedIn} to={itemInfo.crunchbaseData.linkedin} target="_blank">{itemInfo.crunchbaseData.linkedin}</OutboundLink>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="row">
|
||||
{ isMobile && <div className="col col-50">
|
||||
{ twitterElement }
|
||||
{ latestTweetDateElement }
|
||||
{ firstCommitDateElement }
|
||||
{ latestCommitDateElement }
|
||||
{ contributorsCountElement }
|
||||
{ releaseDateElement }
|
||||
{ headquartersElement }
|
||||
{ crunchbaseEmployeesElement }
|
||||
{ amountElement }
|
||||
{ tickerElement }
|
||||
</div> }
|
||||
{ !isMobile && <div className="col col-50">
|
||||
{ twitterElement }
|
||||
{ firstCommitDateElement }
|
||||
{ contributorsCountElement }
|
||||
{ headquartersElement }
|
||||
{ amountElement }
|
||||
{ tickerElement }
|
||||
</div>
|
||||
}
|
||||
{ !isMobile && <div className="col col-50">
|
||||
{ latestTweetDateElement }
|
||||
{ latestCommitDateElement }
|
||||
{ releaseDateElement }
|
||||
{ crunchbaseEmployeesElement }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ showTwitter && itemInfo.twitter && (
|
||||
<div className="product-twitter">
|
||||
<Timeline
|
||||
dataSource={{
|
||||
sourceType: 'profile',
|
||||
screenName: itemInfo.twitter.split('/').filter( x => !!x).slice(-1)[0]
|
||||
}}
|
||||
options={{
|
||||
username: itemInfo.name,
|
||||
tweetLimit: 3
|
||||
}}
|
||||
onLoad={() => console.log('Timeline is loaded!')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue