Compare commits

...

262 Commits

Author SHA1 Message Date
CNCF-bot a882d0675b uniq items 2024-07-11 15:15:44 +01:00
CNCF-bot 5ffa213fc9 remove ospo 2024-07-11 15:03:50 +01:00
CNCF-bot 2344353df5 fix 2024-03-19 14:33:26 +00:00
CNCF-bot b7e063d94e fix 2024-03-08 13:46:19 +00:00
John Mertic e915165065
Re-write `mobile.twitter.com` to `twitter.com` (#893)
* Re-write `mobile.twitter.com` to `twitter.com`

Signed-off-by: John Mertic <jmertic@linuxfoundation.org>

* Fix typo

---------

Signed-off-by: John Mertic <jmertic@linuxfoundation.org>
2024-03-08 13:45:41 +00:00
CNCF-bot 3de416fa3a relax checks on circular 2024-01-23 22:25:06 +00:00
CNCF-bot 7dd727758c update twitter checks 2024-01-15 22:45:37 +00:00
CNCF-bot 5235155c90 no export 2024-01-10 17:05:43 +00:00
CNCF-bot d8e4f84e5c disable daily updated on cncf landscape v1 2024-01-09 14:20:00 +00:00
CNCF-bot 61014620c2 linkedin 2023-10-05 22:39:25 +01:00
CNCF-bot 8137985380 support hide license 2023-10-05 12:14:39 +01:00
CNCF-bot 8c4b4941de reduce request time 2023-09-25 17:16:07 +01:00
CNCF-bot b2c7d52080 2 steps in daily updates 2023-09-25 08:17:37 +01:00
CNCF-bot 5e05834a46 Merge branch 'master' of github.com:cncf/landscapeapp 2023-09-25 07:19:45 +01:00
CNCF-bot 450ca5e141 hide no best practices 2023-09-25 07:19:40 +01:00
knanao d524f88001
Fix the link of the First Commit (#861)
Signed-off-by: knanao <nao.7ken@gmail.com>
2023-09-22 23:07:24 +01:00
CNCF-bot 12667a2b52 fix 2023-09-04 01:17:26 +01:00
CNCF-bot 52f2254d91 fix 2023-09-04 01:07:08 +01:00
CNCF-bot 10155fd1ea remove caching 2023-09-04 01:05:26 +01:00
CNCF-bot bc541000c9 use npm9 2023-09-03 17:22:44 +01:00
CNCF-bot b3638c04aa fix 2023-09-03 17:07:59 +01:00
CNCF-bot bd563a8465 debug 2023-09-03 16:53:48 +01:00
CNCF-bot 485cb1158d fix node version 2023-09-03 16:41:53 +01:00
CNCF-bot 1a3e59e157 fix 2023-08-23 18:30:58 +01:00
CNCF-bot b784d7f896 progress 2023-08-23 16:45:05 +01:00
CNCF-bot b45ce81784 fixes 2023-08-14 17:57:37 +01:00
CNCF-bot 808d219202 skip market ca 2023-07-18 15:19:02 +01:00
CNCF-bot 2bda551a74 fix 2023-07-12 17:41:53 +01:00
CNCF-bot 3775e220a3 report progress 2023-07-12 13:40:53 +01:00
CNCF-bot 0b41bc54da fix 2023-07-10 23:22:05 +01:00
CNCF-bot 97e8e882a3 fixes 2023-07-10 22:35:53 +01:00
John Mertic 62fa27892c
Add Bolsa de Madrid (#796)
Signed-off-by: John Mertic <jmertic@linuxfoundation.org>
2023-06-01 13:58:50 +02:00
CNCF-bot 8ce89db6e3 changes 2023-05-30 09:05:46 +01:00
CNCF-bot 0b66094876 make daily updates faster 2023-05-24 08:40:13 +01:00
CNCF-bot d4409f142a do not calc number of tweets 2023-05-22 12:20:08 +01:00
CNCF-bot 28973ed0bc do not fetch twitter entries, that just does not work 2023-05-17 16:31:44 +01:00
CNCF-bot 7a983a6583 fix 2023-04-20 22:50:30 +01:00
CNCF-bot af91ca7e16 fix an embedded server 2023-04-20 13:00:00 +01:00
CNCF-bot d4850f64fd allow x large 2023-04-17 13:41:29 +01:00
CNCF-bot af77ba322a support other tabs without subcategories 2023-03-30 21:44:13 +01:00
CNCF-bot 73ac6d4efe support large icons for premier members 2023-03-21 14:20:06 +00:00
CNCF-bot ffbb154c51 update message 2023-03-20 12:00:23 +00:00
CNCF-bot 90d9bead25 a warning message 2023-03-20 09:51:29 +00:00
CNCF-bot a17dc8d542 fix 2023-03-14 14:35:56 +00:00
CNCF-bot 0b0314f636 support cors 2023-03-02 20:38:02 +00:00
CNCF-bot d8c453c471 update the ip address of a build server 2023-02-23 22:10:03 +00:00
CNCF-bot b85dbd9100 fix 2023-02-22 14:29:42 +00:00
CNCF-bot 3a01088cda fix --noc-ci 2023-02-22 14:26:57 +00:00
CNCF-bot b0f6c86335 update landscape script --no-ci 2023-02-22 14:20:37 +00:00
CNCF-bot 06d592b3ba fix 2023-02-08 15:27:38 +00:00
CNCF-bot 60ea7c8551 allow gray large items 2023-02-08 08:45:46 +00:00
CNCF-bot f9b1fbe8ab autoupdate riscv as well 2023-01-27 19:41:27 +00:00
CNCF-bot d9020382c6 make it a link 2023-01-27 10:32:08 +00:00
CNCF-bot 1c5e8aaee3 forgot a file 2023-01-26 10:20:56 +00:00
CNCF-bot 5b8254f1b2 support CLO entries 2023-01-26 10:14:55 +00:00
CNCF-bot e12aaca02f summary fixes 2023-01-25 14:38:00 +00:00
CNCF-bot 2775c25b5b tolerate when processed_landscape.yml is not in sync 2023-01-24 21:02:31 +00:00
CNCF-bot 4ffbff6f9c fix the summary layout 2023-01-23 12:59:55 +00:00
CNCF-bot 1ac6a2c97f add a way to remove puppeteer 2023-01-19 10:01:51 +00:00
CNCF-bot 9aa1006f60 speed up RiscV 2023-01-17 15:29:54 +00:00
CNCF-bot b5802a5545 better links rendering 2023-01-12 20:46:34 +00:00
CNCF-bot fe9351f824 allow to override languages 2023-01-09 09:07:37 +00:00
CNCF-bot 8434098eaf styles 2023-01-09 08:51:12 +00:00
CNCF-bot 8dc5fd6c82 fix 2023-01-09 00:12:28 +00:00
CNCF-bot f922f3831d test 2023-01-09 00:08:43 +00:00
CNCF-bot fd3fc93889 test 2023-01-09 00:07:10 +00:00
CNCF-bot 4723c94b56 debug 2023-01-09 00:06:22 +00:00
CNCF-bot 97dd8d8f8b Merge branch 'master' of github.com:cncf/landscapeapp 2023-01-09 00:00:12 +00:00
CNCF-bot aebf1d588e trying to get landscapeapp working properly 2023-01-08 23:59:55 +00:00
CNCF-Bot2 1eb67b3668
Update netlify.md
Signed-off-by: CNCF-Bot2 <117075760+CNCF-Bot2@users.noreply.github.com>
2023-01-08 23:38:43 +00:00
Andrey Kozlov a12ce99e58 CNCF-Bot2 credentials for the landscapeapp 2023-01-08 23:14:54 +00:00
Andrey Kozlov 862a33eacd test 2023-01-06 22:00:23 +00:00
Andrey Kozlov 495c7997e8 replaceAll 2023-01-06 21:42:41 +00:00
Andrey Kozlov bd8f8f8d03 fixes 2023-01-06 21:14:17 +00:00
Andrey Kozlov cc125f24dd test 2023-01-06 21:13:19 +00:00
Andrey Kozlov 250bbe68b8 test 2023-01-06 21:05:26 +00:00
Andrey Kozlov f987c05cb7 test 2023-01-06 21:04:02 +00:00
Andrey Kozlov e577cbd700 test 2023-01-06 20:57:25 +00:00
Andrey Kozlov 2ae215c8c2 make it possible to commit again 2023-01-06 20:51:05 +00:00
Andrey Kozlov 901182882c support one more ssh key 2022-12-30 10:46:48 +00:00
Andrey Kozlov a657ae8540 support bot3 2022-12-30 10:43:30 +00:00
Andrey Kozlov 628c6f71a5 make dlt first 2022-12-09 15:24:27 +00:00
Andrey Kozlov 60e5c99514 fix 2022-12-08 19:25:29 +00:00
Andrey Kozlov 2dc0d29598 add DLT landscape to the autoupdater 2022-12-08 19:20:06 +00:00
Andrey Kozlov d330c59d41 fix 2022-12-08 00:25:41 +00:00
Andrey Kozlov e0429f758d fix autoupdater 2022-12-08 00:24:44 +00:00
Andrey Kozlov 95e60ef4a9 use an ssh key 2022-12-07 22:46:45 +00:00
Andrey Kozlov 9304e3a777 remove duplicates 2022-12-05 12:45:03 +00:00
Andrey Kozlov c5b75d14e2 Merge branch 'master' of github.com:cncf/landscapeapp 2022-11-28 16:49:06 +00:00
Andrey Kozlov 899dca8561 fix 2022-11-28 16:48:55 +00:00
Milan b762217ab1
(URLs): Add configurable setting for self hosted repo url (#854)
Adds a boolean option, self_hosted_repo, that is set in
settings.yml so as to avoid assuming that the git repo
url is prefixed with https://github.com .

Signed-off-by: Milan Lakhani <mlakhani14@bloomberg.net>

Signed-off-by: Milan Lakhani <mlakhani14@bloomberg.net>
2022-11-16 13:39:46 +00:00
Milan 8a8003b41f
(dialog box): Hide twitter button when no twitter key is provided (#853)
This checks whether TWITTER_KEYS is set and if not does not show
the twitter button in the dialog box.

Signed-off-by: Milan Lakhani <mlakhani14@bloomberg.net>

Signed-off-by: Milan Lakhani <mlakhani14@bloomberg.net>
Co-authored-by: Milan Lakhani <27683+mlakhani14@users.noreply.bbgithub.dev.bloomberg.com>
2022-11-15 17:02:50 -06:00
dependabot[bot] c99ae2514a
Bump minimatch from 3.0.4 to 3.1.2 (#850)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-15 16:31:31 -06:00
Andrey Kozlov fdab31e0f7 color 2022-11-15 14:47:11 +00:00
Andrey Kozlov e449569b86 minor alignment 2022-11-15 14:28:26 +00:00
Andrey Kozlov d6c2ecad33 update a summary 2022-11-15 14:03:43 +00:00
Andrey Kozlov ad5d51afe7 support filtering by specification 2022-11-14 08:39:43 +00:00
Andrey Kozlov a49ff4b891 summary 2022-10-25 11:03:08 +01:00
Andrey Kozlov 5a572110fe test 2022-10-19 18:04:51 +01:00
Andrey Kozlov f6dbef23f3 fixes 2022-10-19 17:31:51 +01:00
Andrey Kozlov 461c9d6d8a styling the summary page 2022-10-19 17:22:26 +01:00
Andrey Kozlov dae74b9d6d test 2022-10-18 12:46:50 +01:00
Andrey Kozlov 21d8621271 ga4 snippet 2022-10-17 23:12:40 +01:00
Andrey Kozlov 60d7704d7d remove console info 2022-10-14 08:30:00 +01:00
Andrey Kozlov 526822323c light borders in summary 2022-10-14 08:29:27 +01:00
Andrey Kozlov 47a6b6fb00 better padding 2022-10-14 00:08:06 +01:00
Andrey Kozlov 17f088a0de update summary renderer 2022-10-13 23:58:38 +01:00
Andrey Kozlov f6cd147f77 update subcategory renderer 2022-10-06 15:26:40 +01:00
Andrey Kozlov 1088b323ae fix 2022-09-23 17:15:48 +01:00
Andrey Kozlov 8c75b38bd2 fix funding 2022-09-23 17:13:12 +01:00
Andrey Kozlov e347630c9e speed up funding 2022-09-22 23:08:40 +01:00
Andrey Kozlov 674dd36513 pass accessibility 2022-09-20 20:23:15 +01:00
Andrey Kozlov 2ba49cc7a6 improve accessibility 2022-09-19 19:50:39 +01:00
Andrey Kozlov 1ab80e01c1 provide only extra meta tags 2022-09-14 16:48:29 +01:00
Andrey Kozlov c5e8289c77 support extra meta tags 2022-09-14 16:10:14 +01:00
Andrey Kozlov 1c6df12abf update summary - allow filtering 2022-09-08 20:25:17 +01:00
Andrey Kozlov 5e5110ca52 fix 2022-09-02 10:45:21 +01:00
Andrey Kozlov 875014bac0 project tag in two lines when necessary 2022-09-01 14:49:38 +01:00
Andrey Kozlov 05723ec72f fix 2022-08-22 16:43:53 +01:00
Andrey Kozlov 68452b5dfd support multiline text in label captions 2022-08-17 10:08:11 +01:00
Andrey Kozlov 53d01e8982 fix an issue when number of tweets is not calculated yet 2022-08-17 09:42:32 +01:00
Andrey Kozlov 0d0004ae18 fix 2022-08-12 18:22:33 +01:00
Andrey Kozlov 921e8a3cdf saneName to affect only existing items 2022-08-12 16:15:54 +01:00
Andrey Kozlov 544ba79b1d update the saneName 2022-08-09 16:26:56 +01:00
Andrey Kozlov 645e77ea29 filters 2022-08-07 16:49:03 +01:00
Andrey Kozlov 8cd5ba7a86 wip 2022-08-07 13:53:17 +01:00
Andrey Kozlov 12de9ea4f8 show info about absolutely all CNCF projects 2022-08-05 12:29:17 +01:00
Andrey Kozlov 848670d1e6 better support for a summary table 2022-08-05 10:43:10 +01:00
Andrey Kozlov b03a7878f2 fix 2022-08-02 16:39:15 +01:00
Andrey Kozlov c71dea1f6e more info 2022-08-02 16:20:08 +01:00
Andrey Kozlov 14c675ecf5 first result 2022-08-02 12:09:58 +01:00
Andrey Kozlov 0889f7833a scroll by the first letter 2022-07-18 22:50:29 +01:00
Andrey Kozlov 6b234663e9 update to position: fixed 2022-07-15 13:58:02 +01:00
Andrey Kozlov 2b0075056a position them properly 2022-07-15 13:56:24 +01:00
Andrey Kozlov fba9b1c416 forgot an icon 2022-07-15 13:39:10 +01:00
Andrey Kozlov d338c0bb39 update a guide - add edit and report links, closes #831 2022-07-15 13:08:09 +01:00
Andrey Kozlov fa34affd2d extra special dates 2022-07-08 16:13:20 +01:00
Andrey Kozlov 1437c53605 fix local development 2022-07-06 15:13:48 +01:00
Andrey Kozlov 04ef289bae export extra fields 2022-07-06 10:28:31 +01:00
Andrey Kozlov 90daf28206 better errors for second_path 2022-06-23 16:02:38 +01:00
Andrey Kozlov cb1b161a76 minpr fixes 2022-06-23 08:23:00 +01:00
Andrey Kozlov 8a281f8be2 fix 2022-06-15 13:43:59 +01:00
Andrey Kozlov a59ccb133f typo 2022-06-15 13:31:52 +01:00
Andrey Kozlov 7b4580f15d fix style parameter 2022-06-15 09:45:29 +01:00
Andrey Kozlov b1748d9ad8 fixes 2022-06-15 09:24:14 +01:00
Andrey Kozlov 8316ec3a9c popup window for an embed mode 2022-06-15 08:57:36 +01:00
Andrey Kozlov 84ee4cae5d return back a basic embed support 2022-06-15 08:19:52 +01:00
Andrey Kozlov 8728f7a67d rename best practices 2022-06-11 17:56:26 +01:00
Andrey Kozlov 34a2801c12 realign zoom level 2022-06-08 22:44:02 +01:00
Andrey Kozlov 4274e2dce0 disable filters in a landscape mode 2022-06-08 22:41:42 +01:00
Andrey Kozlov 35e442b220 fix issues 2022-06-08 18:19:41 +01:00
Andrey Kozlov 9dd4cbbce7 one more fix 2022-06-08 14:09:34 +01:00
Andrey Kozlov f420554102 fixes 2022-06-08 13:48:38 +01:00
Andrey Kozlov 51b07fd90f fix api 2022-06-08 08:32:10 +01:00
Andrey Kozlov 5e8272e951 update zIndex 2022-06-07 16:37:31 +01:00
Andrey Kozlov dc9ab23e3c fix 2022-06-07 16:21:39 +01:00
Andrey Kozlov 944b9d4ca5 support a cached version of iframeResizer.js 2022-06-07 16:15:32 +01:00
Andrey Kozlov 1d976569e6 fixes 2022-06-07 15:57:23 +01:00
Andrey Kozlov 4afbf066e1 remove cache control headers 2022-06-07 15:34:18 +01:00
Andrey Kozlov 1311cc87b3 fix external 2022-06-07 15:23:10 +01:00
Andrey Kozlov b36a6dde0f fix iframe 2022-06-07 15:20:04 +01:00
Andrey Kozlov d30b8ad818 eslint 2022-06-07 15:14:38 +01:00
Andrey Kozlov 5a4955b941 fix script --no-ci 2022-06-07 15:03:29 +01:00
Andrey Kozlov ae5ce3d9b0 disable an iframe test for now 2022-06-07 14:54:44 +01:00
Andrey Kozlov 6bd23218e7 rework embedded pages 2022-06-07 13:43:10 +01:00
Andrey Kozlov f0fe09fbf7 server --no-ci 2022-06-06 22:54:30 +01:00
Andrey Kozlov ca1d9882af fix 2022-06-03 19:12:58 +01:00
Andrey Kozlov e286ddaef9 one more typoe 2022-06-03 19:10:24 +01:00
Andrey Kozlov b4f92d7bb9 fix eslint 2022-06-03 18:50:42 +01:00
Andrey Kozlov fab5d5ab34 Merge remote-tracking branch 'origin/master' 2022-06-03 18:47:23 +01:00
Andrey Kozlov 3eb7b5d0f0 do not use npm packages - just rely on a branch 2022-06-03 16:03:33 +01:00
CNCF-bot 5b9f12d80d Update to a new version [skip ci] 2022-06-03 14:36:14 +00:00
CNCF-bot a292620f7a Update to a new version [skip ci] 2022-06-03 14:17:05 +00:00
Andrey Kozlov 7a51987a03 update yarn 2022-06-03 15:15:28 +01:00
Andrey Kozlov 282460a37a update to latest packages 2022-06-03 15:10:50 +01:00
Andrey Kozlov 84749ce870
Remove react (#824)
* convert to no react

* wip

* proper oss/nonoss fix

* more progress

* wip

* more progress

* fixes

* even more fixes

* even more fixes

* more fixes

* eslint added

* fixes

* fixes

* fixes

* fix annoying issues

* issues with export functions fixed

* properly embed data

* debug netlify

* fixes

* fix

* debug

* forgot async

* more fixes

* minor fixes

* do not forget eslint

Co-authored-by: Andrey Kozlov <zeus@Andreys-MacBook-Pro.local>
2022-06-03 15:01:22 +01:00
CNCF-bot ebb53fe075 Update to a new version [skip ci] 2022-05-27 21:12:32 +00:00
Andrey Kozlov c72a1e6148 Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-27 22:06:10 +01:00
Andrey Kozlov 1d9f8c31d6 minor fixes 2022-05-27 22:06:04 +01:00
CNCF-bot 745e5057a4 Update to a new version [skip ci] 2022-05-25 21:23:24 +00:00
Andrey Kozlov 2eeafd345d Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-25 22:17:20 +01:00
Andrey Kozlov abc521cefc support 3 and more categories/subcategories 2022-05-25 22:17:09 +01:00
CNCF-bot 1aa6cf747e Update to a new version [skip ci] 2022-05-25 16:07:18 +00:00
Andrey Kozlov d2719d926b fix 2022-05-25 16:58:00 +01:00
CNCF-bot 03321ab164 Update to a new version [skip ci] 2022-05-25 15:48:57 +00:00
Andrey Kozlov 54a7575b94 render back acquisitions 2022-05-25 16:42:59 +01:00
CNCF-bot e8e823e25b Update to a new version [skip ci] 2022-05-24 18:52:19 +00:00
Andrey Kozlov e8a6db4982
allow a second category/subcategory (#823)
Co-authored-by: Andrey Kozlov <zeus@Andreys-MacBook-Pro.local>
2022-05-24 19:46:36 +01:00
CNCF-bot c1c2fb26d1 Update to a new version [skip ci] 2022-05-22 11:10:04 +00:00
Andrey Kozlov 9009ceb9ad Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-22 12:04:07 +01:00
Andrey Kozlov 991798616a update embedded pages - add extra ids 2022-05-22 12:03:57 +01:00
CNCF-bot 4c5e1df907 Update to a new version [skip ci] 2022-05-19 09:01:19 +00:00
Andrey Kozlov 11dba210df support sections and subsections from all landscape pages; 2022-05-19 09:55:37 +01:00
CNCF-bot e4e706e992 Update to a new version [skip ci] 2022-05-18 18:17:06 +00:00
Andrey Kozlov 3cd5090092 Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-18 19:11:24 +01:00
Andrey Kozlov dea1363831 fix nonoss background 2022-05-18 19:11:16 +01:00
CNCF-bot b0a6aa9ef8 Update to a new version [skip ci] 2022-05-17 22:02:18 +00:00
Andrey Kozlov 619f7b7847 Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-17 22:56:36 +01:00
Andrey Kozlov 7e645dbc3e disable auto redirect 2022-05-17 22:56:26 +01:00
CNCF-bot 6dba098fcc Update to a new version [skip ci] 2022-05-16 14:45:13 +00:00
Andrey Kozlov 0793fafd05 Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-16 15:34:22 +01:00
Andrey Kozlov 1f5285428e test 2022-05-16 15:34:13 +01:00
CNCF-bot 9adb65ee38 Update to a new version [skip ci] 2022-05-16 14:24:42 +00:00
Andrey Kozlov 892362ea1f Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-16 15:18:58 +01:00
Andrey Kozlov c080c125aa fix security issue --no-ci 2022-05-16 15:18:49 +01:00
CNCF-bot 550d8f30d7 Update to a new version [skip ci] 2022-05-16 14:06:40 +00:00
Andrey Kozlov a79556b99e Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-16 15:00:47 +01:00
Andrey Kozlov dec187b4e5 add a space 2022-05-16 15:00:37 +01:00
CNCF-bot 52efed17d6 Update to a new version [skip ci] 2022-05-14 21:47:59 +00:00
Andrey Kozlov 611cf5f654 Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-14 22:42:11 +01:00
Andrey Kozlov 20f32d35d0 update readme --no-ci 2022-05-14 22:42:02 +01:00
CNCF-bot 54fade6a94 Update to a new version [skip ci] 2022-05-14 17:58:56 +00:00
Andrey Kozlov 1f7c082cca loading=lazy for export 2022-05-14 18:53:13 +01:00
CNCF-bot 0d1e0e77e6 Update to a new version [skip ci] 2022-05-14 16:10:42 +00:00
Andrey Kozlov 853af61ab4
vanilla js - no libs, no frameworks (#821)
Vanilla js

Co-authored-by: Andrey Kozlov <zeus@Andreys-MacBook-Pro.local>
2022-05-14 17:05:02 +01:00
CNCF-bot 480aea9d04 Update to a new version [skip ci] 2022-05-09 17:55:46 +00:00
Andrey Kozlov 67a3b089de Merge branch 'master' of github.com:cncf/landscapeapp 2022-05-09 18:36:40 +01:00
CNCF-bot 27f62a94ae Update to a new version [skip ci] 2022-05-09 14:48:23 +00:00
CNCF-bot b21c5fd597 Update to a new version [skip ci] 2022-05-09 14:17:24 +00:00
Andrey Kozlov 926351f335 guarantee that keys are uniq 2022-05-09 15:14:35 +01:00
Andrey Kozlov 409f06a363 remove shadows 2022-05-09 14:58:46 +01:00
CNCF-bot 87f8445b71 Update to a new version [skip ci] 2022-04-25 15:21:11 +00:00
Andrey Kozlov cc71725661 lazy loading images 2022-04-25 16:03:49 +01:00
CNCF-bot e793eb733b Update to a new version [skip ci] 2022-04-10 15:06:25 +00:00
Andrey Kozlov 3eb689d578 fix for an issue in a prune command 2022-04-10 15:49:05 +01:00
CNCF-bot ee27648dfc Update to a new version [skip ci] 2022-03-23 19:10:51 +00:00
Andrey Kozlov bed59adea3 Merge branch 'master' of github.com:cncf/landscapeapp 2022-03-23 18:43:05 +00:00
Andrey Kozlov 3178e88459 proper handling of a license field 2022-03-23 18:42:56 +00:00
CNCF-bot 7babd5b764 Update to a new version [skip ci] 2022-03-23 18:35:58 +00:00
Andrey Kozlov 0d7155766b support explicit license 2022-03-23 18:18:45 +00:00
CNCF-bot adff0ebd1b Update to a new version [skip ci] 2022-03-13 09:20:46 +00:00
Andrey Kozlov 86e23443e4
weekly update (#812)
* weekly update

* weekly update

Co-authored-by: Andrey Kozlov <zeus@Andreys-MacBook-Pro.local>
2022-03-13 09:04:32 +00:00
CNCF-bot ae6b3f006e Update to a new version [skip ci] 2022-02-24 12:37:43 +00:00
Andrey Kozlov c484efebba
weekly update (#811)
Co-authored-by: Andrey Kozlov <zeus@Andreys-MacBook-Pro.local>
2022-02-24 12:23:28 +00:00
CNCF-bot ce1d6dbdfc Update to a new version [skip ci] 2022-02-07 21:00:37 +00:00
Andrey Kozlov dafa28aa1e
weekly update (#809)
Co-authored-by: Andrey Kozlov <zeus@Andreys-MacBook-Pro.local>
2022-02-07 20:47:47 +00:00
CNCF-bot 9065ea761a Update to a new version [skip ci] 2022-02-03 12:52:10 +00:00
Andrey Kozlov ab0c184625 fix 2022-02-03 12:39:27 +00:00
Andrey Kozlov 403a5651e0
support project_org for best practices lookup (#806)
Co-authored-by: Andrey Kozlov <zeus@Andreys-MacBook-Pro.local>
2022-02-01 15:00:33 +00:00
CNCF-bot a4e97097f8 Update to a new version [skip ci] 2022-01-21 08:15:52 +00:00
Trevor Bramwell 2f058973f0
Revert "Remove LFN landscape for now" (#756)
Now that LFN landscape is building successfully it can be added back.

This reverts commit 1dbe339543.
2022-01-21 08:03:52 +00:00
CNCF-bot 6bed68ba99 Update to a new version [skip ci] 2022-01-20 13:00:25 +00:00
Andrey Kozlov 9d20d4fe1a Merge branch 'master' of github.com:cncf/landscapeapp 2022-01-20 12:47:51 +00:00
Andrey Kozlov 6d9843cddf filter by best practices 2022-01-20 12:47:41 +00:00
CNCF-bot e9a3448952 Update to a new version [skip ci] 2022-01-20 09:45:05 +00:00
Andrey Kozlov 5dcf4a478a demand exact 100% matching 2022-01-20 09:32:39 +00:00
CNCF-bot cdbce7aa84 Update to a new version [skip ci] 2022-01-11 15:44:41 +00:00
Jordi Noguera eb7d99d9ba
Only match guide to main landscape for now (#797) 2022-01-11 16:32:17 +01:00
CNCF-bot 9b5c3c6ffb Update to a new version [skip ci] 2021-12-21 13:59:33 +00:00
ZeusTheTrueGod 71b54d5a62 Merge branch 'master' of github.com:cncf/landscapeapp 2021-12-21 13:47:08 +00:00
ZeusTheTrueGod fe515ca7d2 allow a guide without any sections 2021-12-21 13:46:56 +00:00
CNCF-bot 97f425b465 Update to a new version [skip ci] 2021-12-17 15:44:17 +00:00
Jordi Noguera c7e219b52c
Center category title (#795) 2021-12-17 16:31:58 +01:00
CNCF-bot 9a150a523c Update to a new version [skip ci] 2021-12-16 11:28:46 +00:00
Meet Gor 0588a0a63c
fix typos in README (#794)
* fixed typos in README

* changes after review

Co-authored-by: Bill Mulligan <billmulligan516@gmail.com>

Co-authored-by: Bill Mulligan <billmulligan516@gmail.com>
2021-12-16 12:16:32 +01:00
CNCF-bot 3d8522ba7e Update to a new version [skip ci] 2021-12-10 18:49:46 +00:00
ZeusTheTrueGod dfce515043 Merge branch 'master' of github.com:cncf/landscapeapp 2021-12-10 18:37:25 +00:00
ZeusTheTrueGod e180f8c6e5 fix 2021-12-10 18:37:20 +00:00
CNCF-bot 03cb35fad4 Update to a new version [skip ci] 2021-12-08 18:43:58 +00:00
ZeusTheTrueGod fe5fdec626 Merge branch 'master' of github.com:cncf/landscapeapp 2021-12-08 18:31:28 +00:00
ZeusTheTrueGod 624413a037 prerender items to use them in the online editor 2021-12-08 18:31:14 +00:00
CNCF-bot 6bac7f4231 Update to a new version [skip ci] 2021-12-07 11:31:22 +00:00
250 changed files with 24819 additions and 14725 deletions

View File

@ -1,16 +0,0 @@
const path = require('path')
// CAREFUL before adding more presets, next/babel already includes some
// see https://nextjs.org/docs/advanced-features/customizing-babel-config
module.exports = {
ignore: [".yarn", ".pnp.js"],
presets: ["next/babel"],
plugins: [
["module-resolver", {
alias: {
public: path.resolve(__dirname, 'public'),
project: process.env.PROJECT_PATH
}
}]
]
}

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
netlify/jsyaml.js

19
.eslintrc.js Normal file
View File

@ -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
}
}

2
.nvmrc
View File

@ -1 +1 @@
v16.12
v18.3

786
.yarn/releases/yarn-3.2.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
enableProgressBars: false
yarnPath: .yarn/releases/yarn-sources.cjs
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.1.cjs

View File

@ -26,7 +26,7 @@
- [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 managed by [Dan Kohn](https://www.dankohn.com) of [CNCF](https://www.cncf.io) and is under active development by [Andrey Kozlov](https://github.com/ZeusTheTrueGod) (who did most of the development to date) and [Jordi Noguera](https://jordinl.com).
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).
@ -55,7 +55,7 @@ Additional keys that can be set are defined below:
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 overriden 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 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:
@ -67,7 +67,7 @@ Additional keys that can be set are defined below:
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 mulitple entries with the same repo_url; set for each instance
# 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:
@ -77,7 +77,7 @@ 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 the up-to-date logos, and unfortunately, there are no shortcuts.
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.
@ -92,7 +92,7 @@ For new landscapes of any size, you will probably need a graphic artist to rebui
If the project is hosted/sponsored by an organization but doesn't have a logo, best practice is to use that organization's logo with the title of the project underneath ( [example](https://landscape.cncf.io/selected=netflix-eureka) ). You can use a tool such as [Inkscape](https://inkscape.org/) to add the text.
If you get an error with the image that it has a PNG embeded, you will need to find a different SVG that doesn't include a PNG or work with a graphic artist to rebuild the logo.
If you get an error with the image that it has a PNG embedded, you will need to find a different SVG that doesn't include a PNG or work with a graphic artist to rebuild the logo.
#### SVGs Can't Include Text
@ -128,6 +128,8 @@ We require all landscape entries to include a [Crunchbase](https://www.crunchbas
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:
@ -158,7 +160,7 @@ The update server enhances the source data with the fetched data and saves the r
If you want to create an interactive landscape for your project or organization:
1. Note ahead of time that the hardest part of building a landscape is getting hi-res images for every project. You *cannot* convert from a PNG or JPEG into an SVG. You need to get an SVG, AI, or EPS file. When those aren't available, you will need a graphic designer to recreate several images. Don't just use an auto-tracer to try to convert PNG to SVG because there is some artistry involved in making it look good. Please review this [primer](https://www.cncf.io/blog/2019/07/17/what-image-formats-should-you-be-using-in-2019/) on image formats.
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 privleges to [AndreyKozlov1984](https://github.com/AndreyKozlov1984), [jordinl83](https://github.com/jordinl83), and [CNCF-Bot](https://github.com/CNCF-Bot) and ping Dan after creating an account at [slack.cncf.io](https://slack.cncf.io). Alex Contini and Dan are available there to help you recreate SVGs based on a PNG of the company's logo, if necessary, and to fix other problems.
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:
![image](https://user-images.githubusercontent.com/3083270/66166276-dd62ad00-e604-11e9-87db-fd9ae7a80d1a.png)
5. Edit `settings.yml` and `landscape.yml` for your topic.
@ -168,7 +170,7 @@ If you want to create an interactive landscape for your project or organization:
### API Keys
You want to add the following to your `~/.bash_profile`. If you're with the LF, ask Dan Kohn on CNCF [Slack](https://slack.cncf.io) for the Crunchbase and Twitter keys.
You want to add the following to your `~/.bash_profile`. If you're with the LF, ask someone on CNCF [Slack](https://slack.cncf.io) for the Crunchbase and Twitter keys.
For the GitHub key, please go to https://github.com/settings/tokens and create a key (you can call it `personal landscape`) with *no* permissions. That is, don't click any checkboxes, because you only need to access public repos.
@ -190,22 +192,6 @@ dev$ cd landscapeapp
dev$ npm install -g yarn@latest
dev$ yarn
```
Now, to use the local landscapeapp you can add the following to your `~/.bash_profile` or `.zshrc`:
```sh
function y { export PROJECT_PATH=`pwd` && (cd ../landscapeapp && yarn run "$@")}
export -f y
# yf does a normal build and full test run
alias yf='y fetch'
alias yl='y check-links'
alias yq='y remove-quotes'
# yp does a build and then opens up the landscape in your browser ( can view the PDF and PNG files )
alias yp='y build && y open:dist'
# yo does a quick build and opens up the landscape in your browser
alias yo='y open:src'
alias a='for lpath in /Users/your-username/dev/{landscapeapp,cdf-landscape,lfai-landscape}; do echo $lpath; git -C $lpath pull -p; done; (cd /Users/your-username/dev/landscapeapp && yarn);'
```
Reload with `. ~/.bash_profile` and then use `yo`, `yf`, etc. to run functions on the landscape in your landscape directory. `a` will do a git pull on each of the project directories you specify and install any necessary node modules for landscapeapp.
### Adding to a google search console
Go to the google search console, add a new property, enter the url of the
@ -215,8 +201,8 @@ Reload with `. ~/.bash_profile` and then use `yo`, `yf`, etc. to run functions o
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 succesfully deploys
that dashbaord, verify the html tag in a google console. Do not forget to add
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.
@ -391,4 +377,4 @@ Don't include a title for the section, level 3 heading will be automatically gen
### Automatic generation of guide navigation
The guide will include a 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.
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.

View File

@ -2,15 +2,6 @@
X-Robots-Tag: all
Access-Control-Allow-Origin: *
/*.js
Cache-Control: public, max-age=31536000, immutable
/*.css
Cache-Control: public, max-age=31536000, immutable
/*.woff2
Cache-Control: public, max-age=31536000, immutable
/*.svg
Content-Type: image/svg+xml; charset=utf-8

9391
_yarn.lock Normal file

File diff suppressed because it is too large Load Diff

1
dyf.sh
View File

@ -1 +0,0 @@
docker run -e CRUNCHBASE_KEY_4=$CRUNCHBASE_KEY_4 -e TWITTER_KEYS=$TWITTER_KEYS -e GITHUB_KEY=$GITHUB_KEY -it --user root --rm -v $(pwd)/../landscapeapp:/landscapeapp -v $(pwd):/repo -w /usr/app netlify/build:xenial /bin/bash -c '. /opt/buildhome/.nvm/install.sh; . /root/.nvm/nvm.sh; cd /landscapeapp; nvm install `cat .nvmrc`; nvm use `cat .nvmrc`; npm install -g yarn; yarn; PROJECT_PATH=/repo yarn fetch'

1
dyo.sh
View File

@ -1 +0,0 @@
docker run -p 3000:3000 -e CRUNCHBASE_KEY_4=$CRUNCHBASE_KEY_4 -e TWITTER_KEYS=$TWITTER_KEYS -e GITHUB_KEY=$GITHUB_KEY -it --user root --rm -v $(pwd)/../landscapeapp:/landscapeapp -v $(pwd):/repo -w /usr/app netlify/build:xenial /bin/bash -c 'echo $CRUNCHBASE_KEY_4; . /opt/buildhome/.nvm/install.sh; . /root/.nvm/nvm.sh; cd /landscapeapp; nvm install `cat .nvmrc`; nvm use `cat .nvmrc`; npm install -g yarn; yarn; PROJECT_PATH=/repo yarn open:src'

3
jest.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
"verbose": true
}

View File

@ -16,4 +16,4 @@ nvm use
npm install -g npm
npm install -g yarn
yarn
yarn run babel-node tools/landscapes.js
yarn node tools/landscapes.js

View File

@ -1,19 +1,30 @@
ip: 145.40.64.211
ip: 147.75.72.237
landscapes:
# name: how we name a landscape project, used on a build server for logs and settings
# repo: a github repo for a specific landscape
# netlify: full | skip - do we build it on a netlify build or not
# hook: - id for a build hook, so it will be triggered after a 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: cncf
repo: cncf/landscape
hook: 5c1bd968fdd72a78a54bdcd1
required: true
- landscape:
name: cdf
repo: cdfoundation/cdf-landscape
@ -32,23 +43,11 @@ landscapes:
repo: graphql/graphql-landscape
hook: 5d5c7ccf64ecb5bd3d2592f7
required: true
- landscape:
name: lf
repo: jmertic/lf-landscape
hook: 606487123224e20fb8f3896e
- landscape:
name: lfai
repo: lfai/landscape
hook: 60648e5b74c76017210a2f53
required: true
- landscape:
name: lfedge
repo: State-of-the-Edge/lfedge-landscape
hook: 5c80e31894c5c7758edb31e4
- landscape:
name: lfenergy
repo: lf-energy/lfenergy-landscape
hook: 606487bb9da603110d4b8139
- landscape:
name: lfph
repo: lfph/lfph-landscape
@ -62,10 +61,6 @@ landscapes:
name: openssf
repo: ossf/ossf-landscape
hook: 613768338a16cb9182b21c3d
- landscape:
name: ospo
repo: todogroup/ospolandscape
hook: 6033b43a2572da242aee6a92
- landscape:
name: presto
repo: prestodb/presto-landscape
@ -78,3 +73,11 @@ landscapes:
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

View File

@ -9,3 +9,5 @@ server via rsync.
Most chances is that we will switch to a different build tool soon, so this is
an experimental approach to speedup netlify builds.

3875
netlify/jsyaml.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,10 @@
// We will execute this script from a landscape build,
// "prepublish": "cp yarn.lock _yarn.lock",
// "postpublish": "rm _yarn.lock || true"
const LANDSCAPEAPP = process.env.LANDSCAPEAPP || "latest"
const remote = `root@${process.env.BUILD_SERVER}`;
const dockerImage = 'netlify/build:xenial';
const dockerImage = 'netlify/build:focal';
const dockerHome = '/opt/buildhome';
const systemName = require('child_process').execSync('lsb_release -a').toString();
const is1604 = systemName.indexOf('16.04');
console.info(systemName, is1604);
if (!is1604) {
console.info('Please ensure that you have a 16.04 ubuntu image for this netlify project, current lsb_release -a', systemName);
process.exit(1);
}
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);
@ -39,27 +30,20 @@ const debug = function() {
}
}
const runLocal = function(command, options = {}) {
const { assignFn, showOutputFn } = options;
const runLocal = function(command, showProgress) {
// report the output once every 5 seconds
let lastOutput = { s: '', time: new Date().getTime() };
let displayIfRequired = function(text) {
if (showProgress) {
console.info(text);
}
lastOutput.s = lastOutput.s + text;
if (showOutputFn && showOutputFn()) {
if (lastOutput.done || new Date().getTime() > lastOutput.time + 5 * 1000) {
console.info(lastOutput.s);
lastOutput.s = "";
lastOutput.time = new Date().getTime();
};
}
}
return new Promise(function(resolve) {
var spawn = require('child_process').spawn;
var child = spawn('bash', ['-lc',`set -e \n${command}`]);
if (assignFn) {
assignFn(child);
}
let output = [];
child.stdout.on('data', function(data) {
const text = maskSecrets(data.toString('utf-8'));
@ -92,72 +76,6 @@ const runLocalWithoutErrors = async function(command) {
return result.text.trim();
}
let buildDone = false;
let localPid;
let remoteFailed = false;
let localFailed = false;
async function getPids() {
const result = await runLocalWithoutErrors(`ps`);
const lines = result.split('\n').map( (x) => x.trim()).filter( (x) => x).slice(1);
const pids = lines.map( (line) => line.split(' ')[0]);
console.info('pids:', pids);
return pids;
}
let initialPids;
const makeLocalBuild = async function() {
const localOutput = await runLocal(`
# mkdir -p copy
# rsync -az --exclude="copy" . copy
# cd copy
. ~/.nvm/nvm.sh
npm pack interactive-landscape@${LANDSCAPEAPP}
tar xzf interactive*
cd package
cp _yarn.lock yarn.lock
echo 0
nvm install
echo 1
nvm use
echo 2
npm install -g agentkeepalive --save
echo 3
npm install -g npm --no-progress
echo 4
npm install -g yarn@latest
echo 5
yarn >/dev/null
export NODE_OPTIONS="--unhandled-rejections=strict"
export JEST_OPTIONS="-i"
export USE_OLD_PUPPETEER=1
PROJECT_PATH=.. yarn build
`, { assignFn: (x) => localPid = x, showOutputFn: () => remoteFailed });
if (!buildDone) {
console.info('Local build finished, exit code:', localOutput.exitCode);
if (localOutput.exitCode !== 0) {
console.info(localOutput.text);
localFailed = true;
if (!remoteFailed) {
return;
} else {
process.exit(1);
}
}
buildDone = true;
await runLocalWithoutErrors(`
rm -rf netlify/dist || true
cp -r dist netlify
mv netlify/dist/functions netlify/functions
cp -r netlify/functions functions # Fix netlify bug
`);
process.exit(0);
} else {
console.info('Ignore local build');
}
}
const key = `
-----BEGIN OPENSSH PRIVATE KEY-----
${(process.env.BUILDBOT_KEY || '').replace(/\s/g,'\n')}
@ -166,7 +84,7 @@ ${(process.env.BUILDBOT_KEY || '').replace(/\s/g,'\n')}
require('fs').writeFileSync('/tmp/buildbot', key);
require('fs').chmodSync('/tmp/buildbot', 0o600);
const runRemote = async function(command, options) {
const runRemote = async function(command, count = 3) {
const bashCommand = `
nocheck=" -o StrictHostKeyChecking=no "
ssh -i /tmp/buildbot $nocheck ${remote} << 'EOSSH'
@ -174,8 +92,14 @@ const runRemote = async function(command, options) {
${command}
EOSSH
`
return await runLocal(bashCommand, options);
const result = await runLocal(bashCommand, true);
if (result.exitCode === 255 && count > 0) {
console.info(`Attempts to retry more: ${count}`);
return await runRemote(command, count - 1);
}
return result;
};
const runRemoteWithoutErrors = async function(command) {
const result = await runRemote(command);
console.info(result.text.trim());
@ -186,15 +110,8 @@ const runRemoteWithoutErrors = async function(command) {
const makeRemoteBuildWithCache = async function() {
await runLocalWithoutErrors(`
echo extracting
mkdir tmpRemote
cd tmpRemote
rm -rf package || true
npm pack interactive-landscape@${LANDSCAPEAPP}
tar xzf interactive*.tgz
cd ..
mv tmpRemote/package packageRemote
cp packageRemote/_yarn.lock packageRemote/yarn.lock
rm -rf packageRemote || true
git clone -b deploy --single-branch https://github.com/cncf/landscapeapp packageRemote
`);
//how to get a hash based on our files
@ -227,56 +144,6 @@ const makeRemoteBuildWithCache = async function() {
const hash = getHash();
const tmpHash = require('crypto').createHash('sha256').update(getTmpFile()).digest('hex');
// lets guarantee npm install for this folder first
{
const buildCommand = [
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
". ~/.nvm/nvm.sh",
`nvm install ${nvmrc}`,
`echo 0`,
`nvm use ${nvmrc}`,
`echo 1`,
`npm install -g agentkeepalive --save`,
`npm install -g npm --no-progress`,
`npm install -g yarn@latest`,
`cd /opt/repo/packageRemote`,
`yarn >/dev/null`
].join(' && ');
const npmInstallCommand = `
mkdir -p /root/builds/node_cache
ls -l /root/builds/node_cache/${hash}/yarnLocal/unplugged 2>/dev/null || (
mkdir -p /root/builds/node_cache/${tmpHash}/{yarnLocal,nvm,yarnGlobal}
cp -r /root/builds/${folder}/packageRemote/.yarn/* /root/builds/node_cache/${tmpHash}/yarnLocal
chmod -R 777 /root/builds/node_cache/${tmpHash}
docker run --shm-size 1G --rm -t \
-v /root/builds/node_cache/${tmpHash}/yarnLocal:/opt/repo/packageRemote/.yarn \
-v /root/builds/node_cache/${tmpHash}/nvm:${dockerHome}/.nvm \
-v /root/builds/node_cache/${tmpHash}/yarnGlobal:${dockerHome}/.yarn \
-v /root/builds/${folder}:/opt/repo \
${dockerImage} /bin/bash -lc "${buildCommand}"
ln -s /root/builds/node_cache/${tmpHash} /root/builds/node_cache/${hash} || (
rm -rf /root/builds/node_cache/${tmpHash}
)
echo "packages for ${hash} had been installed"
)
chmod -R 777 /root/builds/node_cache/${hash}
`;
debug(npmInstallCommand);
console.info(`Remote with cache: Installing npm packages if required`);
const output = await runRemote(npmInstallCommand);
console.info(`Remote with cache: Output from npm install: exit code: ${output.exitCode}`);
if (output.exitCode !== 0) {
console.info(output.text);
throw new Error('Remote with cahce: npm install failed');
}
const lines = output.text.split('\n');
const index = lines.indexOf(lines.filter( (line) => line.match(/added \d+ packages in/))[0]);
const filteredLines = lines.slice(index !== -1 ? index : 0).join('\n');
console.info(filteredLines || 'Reusing an existing folder for node');
}
// do not pass REVIEW_ID because on failure we will run it locally and report
// from there
const vars = [
@ -292,10 +159,16 @@ const makeRemoteBuildWithCache = async function() {
const outputFolder = 'landscape' + getTmpFile();
const buildCommand = [
`cd /opt/repo/packageRemote`,
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
`. ~/.nvm/nvm.sh`,
`cat .nvmrc`,
`nvm install ${nvmrc}`,
`nvm use ${nvmrc}`,
`npm install -g agentkeepalive --save`,
`npm install -g npm@9 --no-progress`,
`npm install -g yarn@latest`,
`yarn`,
`git config --global --add safe.directory /opt/repo`,
`export NODE_OPTIONS="--unhandled-rejections=strict"`,
`PROJECT_PATH=.. yarn run build`,
`cp -r /opt/repo/dist /dist`
@ -305,16 +178,12 @@ const makeRemoteBuildWithCache = async function() {
mkdir -p /root/builds/${outputFolder}
chmod -R 777 /root/builds/${outputFolder}
chmod -R 777 /root/builds/${folder}
chmod -R 777 /root/builds/node_cache/${hash}
docker run --shm-size 1G --rm -t \
${vars.map( (v) => ` -e ${v}="${process.env[v]}" `).join(' ')} \
-e NVM_NO_PROGRESS=1 \
-e NETLIFY=1 \
-e PARALLEL=TRUE \
-v /root/builds/node_cache/${hash}/yarnLocal:/opt/repo/packageRemote/.yarn \
-v /root/builds/node_cache/${hash}/nvm:${dockerHome}/.nvm \
-v /root/builds/node_cache/${hash}/yarnGlobal:${dockerHome}/.yarn \
-v /root/builds/${folder}:/opt/repo \
-v /root/builds/${outputFolder}:/dist \
${dockerImage} /bin/bash -lc "${buildCommand}"
@ -346,27 +215,12 @@ const makeRemoteBuildWithCache = async function() {
rsync -az --chmod=a+r -p -e "ssh -i /tmp/buildbot -o StrictHostKeyChecking=no " ${remote}:/root/builds/${outputFolder}/dist/* distRemote
`
));
await runRemoteWithoutErrors(
await runRemote(
`
rm -rf /root/builds/${folder}
rm -rf /root/builds/${outputFolder}
`
)
if (!buildDone) {
buildDone = true;
const newPids = await getPids();
const pidsToKill = newPids.filter( (x) => !initialPids.includes(x));
console.info(await runLocal(`kill -9 ${pidsToKill.join(' ')}`));
localPid.kill();
const pause = function(i) {
return new Promise(function(resolve) {
setTimeout(resolve, 5 * 1000);
})
};
await pause(); // allow the previous process to be killed
await runLocalWithoutErrors(`ps`);
console.info('Remote build done!');
console.info(output.text);
@ -381,32 +235,23 @@ const makeRemoteBuildWithCache = async function() {
cp -r netlify/functions functions # Fix netlify bug
`);
process.exit(0);
}
}
async function main() {
const path = require('path');
console.info('starting', process.cwd());
process.chdir('..');
await runLocal('rm package*.json');
initialPids = await getPids();
const cleanPromise = runRemoteWithoutErrors(`
find builds/node_cache -maxdepth 1 -mtime +1 -exec rm -rf {} +;
find builds/ -maxdepth 1 -not -path "builds/node_cache" -mtime +1 -exec rm -rf {} +;
`).catch(function(ex) {
`).catch(function() {
console.info('Failed to clean up a builds folder');
});
await Promise.all([makeRemoteBuildWithCache().catch(function(ex) {
console.info('Remote build failed! Continuing with a local build', ex);
remoteFailed = true;
if (localFailed) {
console.info('build failed', ex);
process.exit(1);
}
}), makeLocalBuild(), cleanPromise]);
}), cleanPromise]);
}
main().catch(function(ex) {

View File

@ -1,9 +1,17 @@
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 run = function(x) {
console.info(require('child_process').execSync(x).toString())
}
const debug = function() {
if (process.env.DEBUG_BUILD) {
console.info.apply(console, arguments);
@ -16,18 +24,14 @@ const pause = function(i) {
})
};
console.info('starting', process.cwd());
run('npm init -y');
console.info('installing js-yaml', process.cwd());
run('npm install js-yaml@4.0.0');
const yaml = require('js-yaml');
const yaml = require('./jsyaml');
process.chdir('..');
console.info('starting real script', process.cwd());
const landscapesInfo = yaml.load(require('fs').readFileSync('landscapes.yml'));
const dockerImage = 'netlify/build:xenial';
const dockerImage = 'netlify/build:focal';
const dockerHome = '/opt/buildhome';
async function main() {
const nvmrc = require('fs').readFileSync('.nvmrc', 'utf-8').trim();
const secrets = [
@ -61,7 +65,7 @@ ${process.env.BUILDBOT_KEY.replace(/\s/g,'\n')}
// now our goal is to run this on a remote server. Step 1 - xcopy the repo
const folder = new Date().getTime();
const remote = 'root@147.75.76.177';
const remote = 'root@147.75.199.15';
const runRemote = async function(command) {
const bashCommand = `
@ -74,6 +78,24 @@ EOSSH
const result = await runLocal(bashCommand);
let newOutput = [];
for (var l of result.text.split('\n')) {
if (l.match(/Counting objects: /)) {
continue;
}
if (l.match(/ExperimentalWarning: Custom ESM Loaders is an experimental feature./)) {
continue
}
if (l.match(/Compressing objects: /)) {
continue;
}
if (l.match(/Receiving objects: /)) {
continue;
}
if (l.match(/Resolving deltas: /)) {
continue;
}
if (l.match(/Could not resolve ".*?" in file/)) {
continue;
}
newOutput.push(l);
if (l.includes('mesg: ttyname failed: Inappropriate ioctl for device')) {
newOutput = [];
@ -154,7 +176,6 @@ EOSSH
await runRemoteWithoutErrors(`chmod -R 777 /root/builds/${folder}`);
// lets guarantee npm install for this folder first
const branch = process.env.BRANCH;
{
const buildCommand = [
"(ls . ~/.nvm/nvm.sh || (curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash >/dev/null))",
@ -163,7 +184,8 @@ EOSSH
`nvm use ${nvmrc}`,
`npm install -g yarn --no-progress --silent`,
`cd /opt/repo`,
`yarn >/dev/null`
`yarn >/dev/null`,
`yarn eslint`
].join(' && ');
const npmInstallCommand = `
mkdir -p /root/builds/${folder}_node
@ -194,6 +216,7 @@ EOSSH
const outputFolder = landscape.name + new Date().getTime();
const buildCommand = [
`cd /opt/repo`,
`git config --global --add safe.directory /opt/repo`,
`. ~/.nvm/nvm.sh`,
`nvm use`,
`export NODE_OPTIONS="--unhandled-rejections=strict"`,
@ -234,8 +257,12 @@ EOSSH
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;
@ -297,24 +324,18 @@ EOSSH
await runLocalWithoutErrors('cp -r dist netlify');
if (process.env.BRANCH === 'master') {
console.info(await runLocal('git remote -v'));
await runLocalWithoutErrors(`
git config --global user.email "info@cncf.io"
git config --global user.name "CNCF-bot"
git remote rm github 2>/dev/null || true
git remote add github "https://$GITHUB_USER:$GITHUB_TOKEN@github.com/cncf/landscapeapp"
git fetch github
# git diff # Need to comment this when a diff is too large
git checkout -- .
npm version patch || npm version patch || npm version patch
git commit -m 'Update to a new version [skip ci]' --allow-empty --amend
git branch -D tmp || true
git checkout -b tmp
git push github HEAD:master || true
git push github HEAD:master --tags --force
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
git diff
npm -q publish || (sleep 5 && npm -q publish) || (sleep 30 && npm -q publish)
echo 'Npm package published'
git remote add github "git@github.com:cncf/landscapeapp.git"
echo 1
GIT_SSH_COMMAND='ssh -i ~/.ssh/bot3 -o IdentitiesOnly=yes' git fetch github
echo 2
git --no-pager show HEAD
echo 3
GIT_SSH_COMMAND='ssh -i ~/.ssh/bot3 -o IdentitiesOnly=yes' git push github github/master:deploy
`);
// just for debug purpose
//now we have a different hash, because we updated a version, but for build purposes we have exactly same npm modules

176
netlify/server.js Normal file
View File

@ -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?

View File

@ -1,37 +0,0 @@
const path = require('path')
const { readFileSync } = require('fs')
const { load } = require('js-yaml')
const bundleAnalyzerPlugin = require('@next/bundle-analyzer')
const getBasePath = require('./tools/getBasePath')
const withBundleAnalyzer = bundleAnalyzerPlugin({ enabled: !!process.env.ANALYZE })
const projectPath = process.env.PROJECT_PATH
const lastUpdated = new Date().toISOString().substring(0, 19).replace('T', ' ') + 'Z'
const processedLandscape = load(readFileSync(path.resolve(projectPath, 'processed_landscape.yml')));
const tweets = (processedLandscape.twitter_options || {}).count || 0
const GA = process.env.GA
const basePath = getBasePath()
module.exports = withBundleAnalyzer({
env: { lastUpdated, tweets, GA, PROJECT_NAME: process.env.PROJECT_NAME },
basePath,
webpack: (config, options) => {
// CAREFUL before adding more presets, next/babel already includes some
// see https://nextjs.org/docs/advanced-features/customizing-babel-config
config.externals = [
...config.externals,
{ moment: 'moment' }
]
if (process.env.PREVIEW) {
config.optimization.minimize = false;
}
return config
},
})

View File

@ -1,142 +1,82 @@
{
"name": "interactive-landscape",
"version": "1.0.619",
"version": "1.0.657",
"description": "Visualization tool for building interactive landscapes",
"engines": {
"npm": ">=3",
"node": ">= 10.5"
},
"scripts": {
"autocrop-images": "babel-node tools/autocropImages",
"dev": "yarn yaml2json && yarn watch-landscape",
"open:src": "yarn dev",
"open:dist": "yarn stop-old-ci && babel-node tools/distServer.js",
"get-iframe-resizer-path": "node -e \"console.info(require.resolve('iframe-resizer/js/iframeResizer.min.js'))\" ",
"landscapes": "babel-node tools/landscapes.js",
"lint": "esw webpack.config.* src tools --color",
"lint:watch": "yarn lint -- --watch",
"eslint": "eslint src tools specs netlify",
"autocrop-images": "node tools/autocropImages",
"dev": "yarn server",
"open:src": "yarn server",
"server": "node server.js",
"landscapes": "node tools/landscapes.js",
"update-github-colors": "curl https://raw.githubusercontent.com/Diastro/github-colors/master/github-colors.json > tools/githubColors.json",
"fetch": "babel-node tools/migrate.js && babel-node tools/validateLandscape && babel-node tools/checkWrongCharactersInFilenames && babel-node tools/addExternalInfo.js && yarn yaml2json",
"fetch": "node tools/validateLandscape && node tools/checkWrongCharactersInFilenames && node tools/addExternalInfo.js && yarn yaml2json",
"fetchAll": "LEVEL=complete yarn fetch",
"update": "(rm /tmp/landscape.json || true) && babel-node tools/migrate.js && babel-node tools/validateLandscape && yarn remove-quotes && LEVEL=medium babel-node tools/addExternalInfo.js && yarn prune && yarn check-links && yarn yaml2json && babel-node tools/calculateNumberOfTweets && babel-node tools/updateTimestamps",
"yaml2json": "babel-node tools/generateJson.js",
"remove-quotes": "babel-node tools/removeQuotes",
"prune": "babel-node tools/pruneExtraEntries",
"check-links": "babel-node tools/checkLinks",
"remove-dist": "rimraf \"$PROJECT_PATH\"/dist",
"light-update": "(rm /tmp/landscape.json || true) && node tools/validateLandscape && yarn remove-quotes && LEVEL=crunchbase node tools/addExternalInfo.js && yarn prune && yarn yaml2json && node tools/updateTimestamps",
"update": "(rm /tmp/landscape.json || true) && node tools/validateLandscape && yarn remove-quotes && LEVEL=medium node tools/addExternalInfo.js && yarn prune && yarn check-links && yarn yaml2json && node tools/updateTimestamps",
"yaml2json": "node tools/generateJson.js",
"remove-quotes": "node tools/removeQuotes",
"prune": "node tools/pruneExtraEntries",
"check-links": "node tools/checkLinks",
"precommit": "yarn fetch",
"host-images": "babel-node tools/hostImages.js && yarn fetch",
"start-ci": "yarn exec bash -c \"(yarn run babel-node tools/distServer.js &) && sleep 10\"",
"stop-old-ci": "yarn run babel-node tools/stopOldDistServer.js",
"start-ci": "yarn exec bash -c \"(node tools/distServer.js &) && sleep 10\"",
"stop-old-ci": "node tools/stopOldDistServer.js",
"stop-ci": "yarn exec bash -c \"kill -9 `cat /tmp/ci.pid` >/dev/null 2>/dev/null && rm /tmp/ci.pid \"",
"integration-test": "jest ${JEST_OPTIONS:-} --reporters=./tools/jestReporter.js --reporters=jest-standard-reporter",
"check-landscape": "babel-node tools/checkLandscape",
"render-landscape": "babel-node tools/renderLandscape",
"funding": "babel-node tools/fundingForMasterBranch",
"copy-dist": "cp -r _headers out && ([ -z \"${PROJECT_NAME:-}\" ] || mkdir \"$PROJECT_PATH\"/dist ) && cp -r out/ \"$PROJECT_PATH\"/dist/${PROJECT_NAME:-}",
"prepare-landscape": "babel-node tools/prepareLandscape.js",
"watch-landscape": "babel-node tools/watchLandscape.js",
"setup-robots": "babel-node tools/sitemap && babel-node tools/addRobots",
"quick-build": "yarn build-next && yarn setup-robots && yarn copy-dist ",
"build": "yarn fetch && yarn prepare-landscape && yarn build-next && yarn setup-robots && yarn remove-dist && yarn copy-dist && yarn export-functions && yarn stop-old-ci && yarn start-ci && babel-node tools/parallelWithRetry integration-test check-landscape render-landscape funding && yarn stop-ci",
"build-next": "next build && next export",
"export-functions": "babel-node ./tools/exportFunctions",
"show-report": "open dist/report.html",
"analyze-bundle": "babel-node ./tools/analyzeBundle.js",
"integration-test": "jest --runInBand --reporters=jest-standard-reporter",
"test": "jest",
"check-landscape": "node tools/checkLandscape",
"render-landscape": "node tools/renderLandscape",
"funding": "node tools/fundingForMasterBranch",
"prepare-landscape": "node tools/prepareLandscape.js && node tools/renderItems.js",
"setup-robots": "node tools/sitemap && node tools/addRobots",
"build": "yarn fetch && yarn prepare-landscape && node tools/renderAcquisitions && yarn setup-robots && yarn export-functions && yarn stop-old-ci && yarn start-ci && node tools/parallelWithRetry integration-test check-landscape render-landscape funding && yarn stop-ci",
"export-functions": "node ./tools/exportFunctions",
"latest": "yarn",
"reset-tweet-count": "babel-node tools/resetTweetCount.js",
"reset-tweet-count": "node tools/resetTweetCount.js",
"prepublish": "cp yarn.lock _yarn.lock",
"postpublish": "rm _yarn.lock || true",
"preview": "yarn fetch && yarn prepare-landscape && yarn build-next"
"preview": "yarn fetch && yarn prepare-landscape && yarn export-functions"
},
"author": "CNCF",
"license": "Apache-2.0",
"dependencies": {
"@babel/core": "^7.16.0",
"@babel/node": "^7.16.0",
"@babel/register": "^7.16.0",
"@babel/runtime": "^7.16.3",
"@date-io/date-fns": "^2.11.0",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@material-ui/pickers": "^3.3.10",
"@next/bundle-analyzer": "^11.1.0",
"@vercel/ncc": "^0.33.0",
"@vercel/ncc": "^0.34.0",
"anchorme": "^2.1.2",
"axe-puppeteer": "^1.1.1",
"axios": "^0.24.0",
"babel-plugin-module-resolver": "^4.1.0",
"axios": "^0.27.2",
"bluebird": "3.7.2",
"change-case": "^4.1.2",
"chart.js": "^3.6.2",
"chartjs-adapter-date-fns": "^2.0.0",
"cheerio": "^1.0.0-rc.10",
"chokidar": "^3.5.2",
"classnames": "^2.3.1",
"cheerio": "^1.0.0-rc.11",
"colors": "1.4.0",
"compression": "^1.7.4",
"current-device": "^0.10.2",
"date-fns": "^2.27.0",
"debug": "^4.3.3",
"ejs": "^3.1.6",
"eslint": "^8.4.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-react": "^7.27.1",
"eslint-watch": "^8.0.0",
"debug": "^4.3.4",
"eslint": "latest",
"event-emitter": "0.3.5",
"expect-puppeteer": "^6.0.2",
"express": "^4.17.1",
"extract-zip": "2.0.1",
"favicons-webpack-plugin": "^5.0.2",
"expect-puppeteer": "^6.1.0",
"feed": "^4.2.2",
"fontsource-roboto": "^4.0.0",
"format-number": "3.0.0",
"get-contrast-ratio": "^0.2.1",
"git-branch": "2.0.1",
"iframe-resizer": "^4.3.2",
"jest": "^27.4.3",
"jest-cli": "^27.4.3",
"jest-puppeteer": "^6.0.2",
"jest": "^28.1.0",
"jest-cli": "^28.1.0",
"jest-standard-reporter": "^2.0.0",
"js-yaml": "^4.1.0",
"jsdom": "^19.0.0",
"json2csv": "^5.0.6",
"json2csv": "^5.0.7",
"lodash": "^4.17.21",
"measure-text-width": "0.0.4",
"millify": "^4.0.0",
"next": "^12.0.7",
"node-emoji": "^1.11.0",
"oauth-1.0a": "^2.2.6",
"open": "^8.4.0",
"postcss": "^8.4.4",
"postcss-import": "^14.0.2",
"postcss-loader": "^6.2.1",
"postcss-nested": "4",
"postcss-simple-vars": "^6.0.3",
"prop-types": "15.7.2",
"puppeteer": "^12.0.1",
"query-string": "^7.0.1",
"raf": "3.4.1",
"react": "^17.0.2",
"react-chartjs-2": "^3.0.4",
"react-dom": "^17.0.2",
"react-ga": "^3.3.0",
"react-hot-loader": "^4.13.0",
"react-key-handler": "^1.2.0-beta.3",
"react-test-renderer": "^17.0.2",
"react-twitter-widgets": "^1.10.0",
"recompose": "0.30.0",
"puppeteer": "^14.2.1",
"query-string": "^7.1.1",
"relative-date": "1.1.3",
"reselect": "^4.1.5",
"rimraf": "3.0.2",
"sanitize-html": "^2.6.0",
"serve-static": "1.14.1",
"showdown": "^1.9.1",
"sitemap": "^7.0.0",
"svg-autocrop": "^2.0.40",
"swr": "^0.5.6",
"sanitize-html": "^2.7.0",
"showdown": "^2.1.0",
"sitemap": "^7.1.1",
"svg-autocrop": "^2.0.41",
"traverse": "0.6.6",
"yarn": "^1.22.17"
"yarn": "^1.22.18"
},
"keywords": [
"landscape",
@ -153,25 +93,5 @@
"type": "git",
"url": "https://github.com/cncf/landscapeapp"
},
"jest": {
"haste": {},
"testPathIgnorePatterns": [
"<rootDir>/node_modules"
],
"transformIgnorePatterns": [
"<rootDir>/node_modules"
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/tools/assetsTransformer.js",
"\\.(css)$": "<rootDir>/tools/assetsTransformer.js"
},
"setupFiles": [
"raf/polyfill"
]
},
"dependenciesMeta": {
"open": {
"unplugged": true
}
}
"packageManager": "yarn@3.2.1"
}

119
server.js Normal file
View File

@ -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}/`);

9
specs/.eslintrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
globals: {
jest: true,
test: true,
it: true,
describe: true,
expect: true
}
};

View File

@ -1,47 +0,0 @@
import { existsSync } from 'fs'
import puppeteer from "puppeteer";
const { AxePuppeteer } = require('axe-puppeteer');
import { appUrl } from '../tools/distSettings'
import { projectPath } from '../tools/settings'
const analyzePage = async url => {
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'], defaultViewport: { width: 1600, height: 1200 }});
const page = await browser.newPage();
await page.goto(url);
await page.waitForSelector('.app')
const results = await new AxePuppeteer(page).withTags('wcag2a').analyze()
await browser.close()
if (results.violations.length > 0) {
const output = [
'Encountered the following accessibility issues:',
...results.violations.flatMap(violation => {
return [
'',
`[${violation.impact}] ${violation.help}:`,
` DESCRIPTION: ${violation.helpUrl}`,
' ELEMENTS:',
...violation.nodes.flatMap(node => ` * ${node.html}`),
]
})
].join('\n')
throw output
}
}
describe("Accessibility", () => {
test("Main Landscape", async () => {
await analyzePage(appUrl)
}, 60 * 1000);
test("Card Mode", async () => {
await analyzePage(`${appUrl}/card-mode`)
}, 60 * 1000);
if (existsSync(`${projectPath}/guide`)) {
test("Guide", async () => {
await analyzePage(`${appUrl}/guide`)
}, 60 * 1000);
}
});

View File

@ -1,16 +1,14 @@
import puppeteer from "puppeteer";
const puppeteer = require("puppeteer");
require('expect-puppeteer');
import { paramCase } from 'change-case';
import { settings } from '../tools/settings';
import { projects } from '../tools/loadData';
import { landscapeSettingsList } from "../src/utils/landscapeSettings";
import { appUrl, pathPrefix } from '../tools/distSettings'
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 devicesMap = puppeteer.devices;
const width = 1920;
const height = 1080;
let setup;
let browser;
let page;
let close = () => test('Closing a browser', async () => await browser.close());
@ -26,125 +24,126 @@ expect.extend({
return { pass, message };
},
})
jest.setTimeout(process.env.SHOW_BROWSER ? 40000 : 30000);
jest.setTimeout(process.env.SHOW_BROWSER ? 30000 : 30000);
async function makePage(initialUrl) {
try {
browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'], headless: !process.env.SHOW_BROWSER});
const page = await browser.newPage();
await setup(page);
await page.goto(initialUrl);
await page.setViewport({ width, height });
return page;
} catch(ex) {
try {
console.info('retrying...', ex);
browser.close();
} catch(ex2) {
console.info('failed to close browser', ex2);
}
return await makePage(initialUrl);
}
}
function embedTest() {
describe("Embed test", () => {
describe("I visit an example embed page", () => {
let frame;
test('page is open and has a frame', async function(){
page = await makePage(appUrl + '/embed');
frame = await page.frames()[1];
await frame.waitForXPath(`//h1[contains(text(), 'full interactive landscape')]`);
});
test('Do not see a content from a main mode', async function() {
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 expect(frame).toHaveElement(`.mosaic img`);
await frame.click(`.mosaic img`);
await frame.waitForSelector(".modal-content");
});
close();
}, 6 * 60 * 1000); //give it up to 1 min to execute
});
async function waitForSelector(page, selector) {
await page.waitForFunction(`document.querySelector('${selector}') && document.querySelector('${selector}').clientHeight != 0`);
}
function mainTest() {
describe("Main test", () => {
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');
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 expect(page).toHaveElement(`//a[contains(text(), '${settings.test.section}')]`);
await waitForHeaderText(page, settings.test.section);
});
test('I see a You are viewing text', async function() {
await expect(page).toHaveElement(`//*[contains(text(), 'You are viewing ')]`);
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 page.waitForSelector(".modal-content");
await waitForSelector(page, ".modal-content .product-logo");
});
close();
}, 6 * 60 * 1000); //give it up to 1 min to execute
});
}
});
function landscapeTest() {
describe("Big Picture Test", () => {
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('.big-picture-section');
await page.waitForSelector('.cards-section [data-mode=main]');
});
test('When I click on an item the modal is open', async function() {
await page.click('.big-picture-section img[src]');
await page.waitForSelector(".modal-content");
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");
});
// and check that without redirect it works too
test('If I would straight open the url with a selected id, a modal appears', async function() {
await page.goto(appUrl);
await page.waitForSelector('.big-picture-section');
await page.click('.big-picture-section img[src]');
await page.waitForSelector(".modal-content");
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 }) => {
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 page.waitForSelector('.big-picture-section');
await page.click('.big-picture-section img[src]');
await page.waitForSelector(".modal-content");
await waitForSelector(page, `.cards-section [data-mode=${url}] [data-id]`);
await page.click(`.cards-section [data-mode=${url}] [data-id]`);
await waitForSelector(page, ".modal-content .product-logo");
}, 6 * 60 * 1000); //give it up to 1 min to execute
close();
})
}
});
describe("Normal browser", function() {
beforeAll(async function() {
setup = async (page) => await page.setViewport({ width, height });
})
mainTest();
landscapeTest();
embedTest();
describe("Filtering by organization", () => {
describe("Filtering by organization", () => {
const project = projects[0];
const organizationSlug = paramCase(project.organization);
const otherProject = projects.find(({ organization }) => organization.toLowerCase() !== project.organization.toLowerCase());
@ -153,21 +152,14 @@ describe("Normal browser", function() {
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.goto(`${appUrl}/card-mode?organization=${otherOrganizationSlug}`);
await page.waitForSelector('.cards-section .mosaic');
await expect(page).not.toHaveElement(`//div[contains(@class, 'mosaic')]//*[text()='${project.name}']`);
});
}
close();
}, 6 * 60 * 1000);
});
describe("iPhone simulator", function() {
beforeAll(async function() {
setup = async (page) => await page.emulate(devicesMap['iPhone X'])
})
mainTest();
landscapeTest();
});
}, 6 * 60 * 1000);

View File

@ -1,4 +1,4 @@
import actualTwitter from '../../tools/actualTwitter';
const { actualTwitter } = require('../../tools/actualTwitter');
describe('Twitter URL', () => {
describe('when crunchbase data not set', () => {

View File

@ -1,14 +0,0 @@
import React from 'react'
import settings from 'public/settings.json'
import OutboundLink from './components/OutboundLink'
import assetPath from './utils/assetPath'
const OrganizationLogo = () => {
const { short_name, company_url } = settings.global
return <OutboundLink eventLabel={short_name} to={company_url} className="landscapeapp-logo" title={`${short_name} Home`}>
<img src={assetPath("/images/right-logo.svg")} title={`${short_name} Logo`}/>
</OutboundLink>
}
export default OrganizationLogo

49
src/api/export.js Normal file
View File

@ -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);
}

41
src/api/ids.js Normal file
View File

@ -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));
}

35
src/api/items.js Normal file
View File

@ -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 }
}

View File

@ -1,263 +0,0 @@
import React, { useState } from 'react'
import Container from '@material-ui/core/Container'
import Table from '@material-ui/core/Table'
import TableBody from '@material-ui/core/TableBody'
import TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer'
import TableHead from '@material-ui/core/TableHead'
import TablePagination from '@material-ui/core/TablePagination'
import TableRow from '@material-ui/core/TableRow'
import Paper from '@material-ui/core/Paper'
import Typography from "@material-ui/core/Typography"
import Box from "@material-ui/core/Box"
import Toolbar from "@material-ui/core/Toolbar"
import TextField from "@material-ui/core/TextField"
import FormControl from "@material-ui/core/FormControl"
import InputLabel from "@material-ui/core/InputLabel"
import Select from "@material-ui/core/Select"
import MenuItem from "@material-ui/core/MenuItem"
import Button from "@material-ui/core/Button"
import Dialog from "@material-ui/core/Dialog"
import DialogTitle from "@material-ui/core/DialogTitle"
import DialogContent from "@material-ui/core/DialogContent"
import DialogActions from "@material-ui/core/DialogActions"
import FilterListIcon from "@material-ui/icons/FilterList"
import Autocomplete from "@material-ui/lab/Autocomplete"
import { DatePicker, MuiPickersUtilsProvider } from '@material-ui/pickers'
import DateFnsUtils from '@date-io/date-fns'
import millify from 'millify'
import OutboundLink from './OutboundLink'
import { sortBy, uniq } from 'lodash'
import { stringifyParams } from '../utils/routing'
const makeOptions = (arr) => sortBy(uniq(arr), name => name.toLowerCase())
const Acquistions = ({ ...props }) => {
const acquisitions = props.acquisitions.map(data => ({ ...data, date: new Date(data.date) }))
.sort((a, b) => b.date - a.date)
const members = new Set(props.members)
const acquirers = makeOptions(acquisitions.map(a => a.acquirer))
const acquirees = makeOptions(acquisitions.map(a => a.acquiree).filter(a => a))
const linkToOrg = organization => {
if (!members.has(organization)) {
return organization
}
const url = stringifyParams({ mainContentMode: 'landscape', filters: { organization }})
return <OutboundLink to={url}>{organization}</OutboundLink>
}
const rowKey = ({ acquirer, acquiree, date }) => {
return `${acquirer}-${acquiree}-${date.toString()}`
}
const rowsPerPage = 20
const [page, setPage] = useState(0)
const [dialogOpen, setDialogOpen] = useState(false)
const closeDialog = () => setDialogOpen(false)
const defaultFilters = {
acquirers: [],
acquirees: [],
min_date: null,
max_date: null,
min_price: '',
max_price: ''
}
const priceOptions = [
{ label: '', value: '' },
{ label: '$100M', value: 100000000 },
{ label: '$250M', value: 250000000 },
{ label: '$500M', value: 500000000 },
{ label: '$1B', value: 1000000000 },
{ label: '$5B', value: 5000000000 },
{ label: '$10B', value: 10000000000 },
{ label: '$20B', value: 20000000000 },
{ label: '$50B', value: 50000000000 },
{ label: '$100B', value: 100000000000 }
]
const [filters, setFilters] = useState(defaultFilters)
const filterFn = (acquisition) => {
if (filters.acquirers.length > 0 && !filters.acquirers.includes(acquisition.acquirer)) {
return false
}
if (filters.acquirees.length > 0 && !filters.acquirees.includes(acquisition.acquiree)) {
return false
}
if (filters.min_date && filters.min_date > acquisition.date) {
return false
}
if (filters.max_date && filters.max_date < acquisition.date) {
return false
}
if (filters.min_price && (!acquisition.price || filters.min_price > acquisition.price)) {
return false
}
if (filters.max_price && (!acquisition.price || filters.max_price < acquisition.price)) {
return false
}
return true
}
const resetFilters = () => {
setPage(0)
setFilters(defaultFilters)
}
const setFilter = (name, value) => {
setPage(0)
setFilters({ ...filters, [name]: value })
}
const hasFilters = () => {
const { acquirers, acquirees, min_date, max_date, min_price, max_price } = filters
return acquirers.length > 0 || acquirees.length > 0 || min_date || max_date || min_price || max_price
}
const filteredAcquisitions = acquisitions.filter(filterFn)
const data = filteredAcquisitions.slice(page * rowsPerPage, (page + 1) * rowsPerPage)
const filterDialog = () => {
return <Dialog onClose={closeDialog} open={dialogOpen} maxWidth='sm' fullWidth={true}>
<DialogTitle>Filter Acquisitions</DialogTitle>
<DialogContent>
<Box mb={3}>
<Autocomplete
multiple
options={acquirers}
value={filters.acquirers}
renderInput={params => <TextField {...params} label="Acquirer"/>}
onChange={(_, value) => setFilter('acquirers', value)}
/>
</Box>
<Box mb={3}>
<Autocomplete
multiple
options={acquirees}
value={filters.acquirees}
renderInput={params => <TextField {...params} label="Acquiree" />}
onChange={(_, value) => setFilter('acquirees', value)}
/>
</Box>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Box mb={3} flexDirection="row" display={"flex"}>
<Box mr={2} flexGrow="1">
<DatePicker
value={filters.min_date}
onChange={value => setFilter('min_date', value)}
label="From Date"
autoOk
fullWidth
disableFuture
labelFunc={(date) => date ? date.toLocaleDateString() : ''}
clearable={true}
/>
</Box>
<Box ml={2} flexGrow="1">
<DatePicker
value={filters.max_date}
onChange={value => setFilter('max_date', value)}
label="To Date"
autoOk
fullWidth
disableFuture
labelFunc={(date) => date ? date.toLocaleDateString() : ''}
clearable={true}
/>
</Box>
</Box>
</MuiPickersUtilsProvider>
<Box mb={5} flexDirection="row" display={"flex"}>
<Box mr={2} flexGrow="1">
<FormControl fullWidth={true}>
<InputLabel>Min Price</InputLabel>
<Select
value={filters.min_price}
onChange={e => setFilter('min_price', e.target.value)}
>
{priceOptions.map(({ value, label }) => <MenuItem key={value} value={value}>{label}</MenuItem>)}
</Select>
</FormControl>
</Box>
<Box ml={2} flexGrow="1">
<FormControl fullWidth={true}>
<InputLabel>Max Price</InputLabel>
<Select
value={filters.max_price}
onChange={e => setFilter('max_price', e.target.value)}
>
{priceOptions.map(({ value, label }) => <MenuItem key={value} value={value}>{label}</MenuItem>)}
</Select>
</FormControl>
</Box>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={resetFilters} color="primary">
Reset
</Button>
<Button onClick={closeDialog}>
Close
</Button>
</DialogActions>
</Dialog>
}
return <Container maxWidth="lg" disableGutters={true}>
<Paper>
<Toolbar variant="dense">
<Box flexGrow="1">
<Typography variant="h6">
Acquisitions
</Typography>
</Box>
{ hasFilters() ?
<Button color="primary" onClick={resetFilters}>Reset</Button>
: null
}
<Button
aria-label="filter list"
onClick={() => setDialogOpen(true)}
endIcon={<FilterListIcon>filter</FilterListIcon>}
>
Filter
</Button>
{filterDialog()}
</Toolbar>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Acquirer</TableCell>
<TableCell>Acquiree</TableCell>
<TableCell align="right">Price</TableCell>
<TableCell align="right">Date</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.map(acquisition => {
return <TableRow key={rowKey(acquisition)}>
<TableCell>{linkToOrg(acquisition.acquirer)}</TableCell>
<TableCell>{acquisition.acquiree && linkToOrg(acquisition.acquiree)}</TableCell>
<TableCell align="right">{acquisition.price && `$${millify(acquisition.price)}`}</TableCell>
<TableCell align="right">{acquisition.date.toLocaleDateString()}</TableCell>
</TableRow>
})}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={filteredAcquisitions.length}
rowsPerPage={rowsPerPage}
page={page}
rowsPerPageOptions={[]}
onChangePage={(_, page) => setPage(page)}
/>
</Paper>
</Container>
}
export default Acquistions

View File

@ -1,18 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import OutboundLink from './OutboundLink';
import settings from 'public/settings.json';
import assetPath from '../utils/assetPath'
const Ad = () => {
const entries = settings.ads || [];
return <div id="kubecon">
{ entries.map( (entry) => (
<OutboundLink className="sidebar-event" key={entry.image} to={entry.url} title={entry.title}>
<img src={assetPath(entry.image)} alt={entry.title} />
</OutboundLink>
)) }
</div>
}
export default pure(Ad);

View File

@ -1,14 +0,0 @@
import { useContext } from 'react'
import ComboboxMultiSelector from './ComboboxMultiSelector'
import { options } from '../types/fields';
import LandscapeContext from '../contexts/LandscapeContext'
const ArrayFilterContainer = ({ name }) => {
const { navigate, params } = useContext(LandscapeContext)
const value = params.filters[name]
const _options = options(name)
const onChange = value => navigate({ filters: { [name]: value } })
return <ComboboxMultiSelector onChange={onChange} value={value} options={_options} />
}
export default ArrayFilterContainer

View File

@ -1,30 +0,0 @@
// locate zoom buttons
import IconButton from '@material-ui/core/IconButton';
import { pure } from 'recompose';
import FullscreenExitIcon from '@material-ui/icons/FullscreenExit';
import FullscreenIcon from '@material-ui/icons/Fullscreen';
import React, { useContext } from 'react';
import LandscapeContext from '../../contexts/LandscapeContext'
const FullscreenButton = _ => {
const { navigate, params } = useContext(LandscapeContext)
const isBigPicture = params.mainContentMode !== 'card-mode'
const { isFullscreen } = params
if (!isBigPicture) {
return null;
}
return <div className="fullscreen-button">
{ isFullscreen ?
<IconButton onClick={_ => navigate({ isFullscreen: false })} title="Exit fullscreen" size="small">
<FullscreenExitIcon />
</IconButton>
:
<IconButton onClick={_ => navigate({ isFullscreen: true })} title="Enter fullscreen" size="small">
<FullscreenIcon />
</IconButton>
}
</div>
}
export default pure(FullscreenButton);

View File

@ -1,142 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import LandscapeContent from './LandscapeContent';
import useCurrentDevice from '../../utils/useCurrentDevice'
import { useRouter } from 'next/router'
import LandscapeContext from '../../contexts/LandscapeContext'
import { headerHeight} from '../../utils/landscapeCalculations'
const _calculateZoom = (fullscreenWidth, fullscreenHeight, zoomedIn, currentDevice) => {
const isFirefox = navigator.userAgent.indexOf('Firefox') > -1
const aspectRatio = innerWidth / innerHeight
const adjustedWidth = outerWidth
const adjustedHeight = adjustedWidth / aspectRatio
let baseZoom = Math.min(adjustedHeight / fullscreenHeight, adjustedWidth / fullscreenWidth, 2).toPrecision(4)
let wrapperWidth, wrapperHeight
if (isFirefox || location.search.indexOf('scale=false') > -1) {
wrapperWidth = Math.max(fullscreenWidth, innerWidth)
wrapperHeight = Math.max(fullscreenHeight, innerHeight)
baseZoom = 1
} else {
wrapperWidth = adjustedWidth / baseZoom
wrapperHeight = adjustedHeight / baseZoom
}
return { zoom: Math.min(baseZoom * (zoomedIn ? 3 : 1), 3), wrapperWidth, wrapperHeight }
}
const Fullscreen = _ => {
const { version } = useRouter().query
const { fullscreenWidth, fullscreenHeight, landscapeSettings } = useContext(LandscapeContext)
const currentDevice = useCurrentDevice()
const [zoomState, setZoomState] = useState({
zoom: 1,
wrapperHeight: fullscreenHeight,
wrapperWidth: fullscreenWidth,
zoomedIn: false,
zoomedAt: {}
})
const { zoom, wrapperHeight, wrapperWidth, zoomedIn, zoomedAt } = zoomState
const calculateZoom = (zoomedIn = false, zoomedAt = {}) => {
const zoomAttrs = _calculateZoom(fullscreenWidth, fullscreenHeight, zoomedIn, currentDevice)
setZoomState({ zoomedIn, zoomedAt, ...zoomAttrs })
}
const onZoom = e => {
const zoomedAt = { x: e.pageX / zoom, y: e.pageY / zoom }
calculateZoom(!zoomedIn, zoomedAt)
}
useEffect(() => {
if (currentDevice.ready && currentDevice.desktop()) {
calculateZoom()
const onResize = _ => calculateZoom()
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}
}, [currentDevice]);
useEffect(() => {
window.scrollTo((zoomedAt.x * zoom - innerWidth / 2), (zoomedAt.y * zoom - innerHeight / 2))
}, [zoomedAt, zoomedIn])
return (
<div className="gradient-bg" style={{
width: wrapperWidth * zoom,
height: wrapperHeight * zoom,
overflow: 'hidden'
}}>
<div style={{
transform: `scale(${zoom})`,
width: wrapperWidth,
height: wrapperHeight,
transformOrigin: '0 0',
paddingTop: headerHeight,
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div
onClick={e => currentDevice.desktop() && onZoom(e)}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
cursor: zoomedIn ? 'zoom-out' : 'zoom-in',
zIndex: 100000
}}>
</div>
<LandscapeContent padding={0} />
<div style={{
position: 'absolute',
top: 10,
left: '50%',
transform: 'translateX(-50%)',
fontSize: 18,
background: 'rgb(64,89,163)',
color: 'white',
paddingLeft: 20,
paddingRight: 20,
paddingTop: 3,
paddingBottom: 3,
borderRadius: 5
}}>{landscapeSettings.fullscreen_header}</div>
{ !landscapeSettings.fullscreen_hide_grey_logos && <div style={{
position: 'absolute',
top: 15,
right: 12,
fontSize: 11,
background: '#eee',
color: 'rgb(100,100,100)',
paddingLeft: 20,
paddingRight: 20,
paddingTop: 3,
paddingBottom: 3,
borderRadius: 5
}}>Greyed logos are not open source</div> }
<div style={{
position: 'absolute',
top: 10,
left: 15,
fontSize: 14,
color: 'white',
}}>{landscapeSettings.title} </div>
<div style={{
position: 'absolute',
top: 30,
left: 15,
fontSize: 12,
color: '#eee',
}}>{version}</div>
</div>
</div>
);
};
export default Fullscreen

View File

@ -1,120 +0,0 @@
import React, { Fragment, useContext } from "react";
import Item from "./Item";
import InternalLink from "../InternalLink";
import {
calculateHorizontalCategory,
categoryBorder,
categoryTitleHeight,
dividerWidth,
itemMargin,
smallItemWidth,
smallItemHeight,
subcategoryMargin,
subcategoryTitleHeight
} from "../../utils/landscapeCalculations";
import LandscapeContext from '../../contexts/LandscapeContext'
import SubcategoryInfo from '../SubcategoryInfo'
import CategoryHeader from '../CategoryHeader'
const Divider = ({ color }) => {
const width = dividerWidth
const marginTop = 2 * subcategoryMargin
const height = `calc(100% - ${2 * marginTop}px)`
return <div style={{ width, marginTop, height, borderLeft: `${width}px solid ${color}` }}/>
}
const HorizontalCategory = ({ header, subcategories, width, height, top, left, color, href, fitWidth }) => {
const { guideIndex } = useContext(LandscapeContext)
const addInfoIcon = Object.keys(guideIndex).length > 0
const subcategoriesWithCalculations = calculateHorizontalCategory({ height, width, subcategories, fitWidth, addInfoIcon })
const totalRows = Math.max(...subcategoriesWithCalculations.map(({ rows }) => rows))
return (
<div style={{ width, left, height, top, position: 'absolute' }} className="big-picture-section">
<div
style={{
position: 'absolute',
background: color,
top: subcategoryTitleHeight,
bottom: 0,
left: 0,
right: 0,
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
padding: categoryBorder
}}
>
<div style={{
top: 0,
bottom: 0,
left: 0,
width: categoryTitleHeight,
position: 'absolute',
writingMode: 'vertical-rl',
transform: 'rotate(180deg)',
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<CategoryHeader href={href} label={header} guideAnchor={guideIndex[header]} background={color} rotate={true} />
</div>
<div style={{
marginLeft: 30,
height: '100%',
display: 'flex',
justifyContent: 'space-evenly',
background: 'white'
}}>
{subcategoriesWithCalculations.map((subcategory, index) => {
const lastSubcategory = index !== subcategories.length - 1
const { allItems, columns, width, name, href } = subcategory
const padding = fitWidth ? 0 : `${subcategoryMargin}px 0`
const style = {
display: 'grid',
height: '100%',
gridTemplateColumns: `repeat(${columns}, ${smallItemWidth}px)`,
gridAutoRows: `${smallItemHeight}px`
}
const extraStyle = fitWidth ? { justifyContent: 'space-evenly', alignContent: 'space-evenly' } : { gridGap: itemMargin }
const path = [header, name].join(' / ')
return <Fragment key={name}>
<div style={{
width,
position: 'relative',
overflow: 'visible',
padding,
boxSizing: 'border-box'
}}>
<div style={{
position: 'absolute',
top: -1 * categoryTitleHeight,
left: 0,
right: 0,
height: categoryTitleHeight,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center'
}}>
<InternalLink to={href} className="white-link">{name}</InternalLink>
</div>
<div style={{...style, ...extraStyle}}>
{
allItems.map(item => <Item item={item} key={item.name}/>)
}
{ guideIndex[path] && <SubcategoryInfo label={name} anchor={guideIndex[path]} column={columns} row={totalRows}/> }
</div>
</div>
{lastSubcategory && <Divider color={color}/>}
</Fragment>
})}
</div>
</div>
</div>);
}
export default HorizontalCategory

View File

@ -1,97 +0,0 @@
import { useContext } from 'react'
import settings from 'public/settings.json'
import fields from "../../types/fields";
import {
largeItemHeight,
largeItemWidth,
smallItemHeight,
smallItemWidth
} from "../../utils/landscapeCalculations";
import LandscapeContext from '../../contexts/LandscapeContext'
import assetPath from '../../utils/assetPath'
const LargeItem = ({ item, onClick }) => {
const relationInfo = fields.relation.valuesMap[item.relation]
const color = relationInfo.big_picture_color;
const label = relationInfo.big_picture_label;
const textHeight = label ? 10 : 0
const padding = 2
return <div className="large-item item" onClick={onClick}>
<style jsx>{`
.large-item {
cursor: pointer;
position: relative;
background: ${color};
visibility: ${item.isVisible ? 'visible' : 'hidden'};
width: ${largeItemWidth}px;
height: ${largeItemHeight}px;
}
.large-item img {
width: calc(100% - ${2 * padding}px);
height: calc(100% - ${2 * padding + textHeight}px);
padding: 5px;
margin: ${padding}px ${padding}px 0 ${padding}px;
}
.large-item .label {
position: absolute;
bottom: 0;
width: 100%;
height: ${textHeight + padding}px;
text-align: center;
vertical-align: middle;
background: ${color};
color: white;
font-size: 6.7px;
line-height: 13px;
}
`}</style>
<img loading="lazy" src={assetPath(item.href)} data-href={item.id} alt={item.name} />
<div className="label">{label}</div>
</div>;
}
const SmallItem = ({ item, onClick }) => {
const isMember = item.category === settings.global.membership;
return <>
<style jsx>{`
img {
cursor: pointer;
width: ${smallItemWidth}px;
height: ${smallItemHeight}px;
border: 1px solid ${isMember ? 'white' : 'grey'};
border-radius: 2px;
padding: 1px;
visibility: ${item.isVisible ? 'visible' : 'hidden'};
}
`}</style>
<img data-href={item.id} loading="lazy" className="item" src={assetPath(item.href)} onClick={onClick} alt={item.name} />
</>
}
const Item = props => {
const { isLarge, category, oss, categoryAttrs } = props.item
const isMember = category === settings.global.membership;
const { navigate } = useContext(LandscapeContext)
const onClick = _ => navigate({ selectedItemId: props.item.id }, { scroll: false })
const newProps = { ...props, onClick }
return <div className={isMember || oss || categoryAttrs.isLarge ? 'oss' : 'nonoss'}>
<style jsx>{`
div {
display: flex;
justify-content: center;
align-items: center;
grid-column-end: span ${isLarge ? 2 : 1};
grid-row-end: span ${isLarge ? 2 : 1};
}
`}</style>
{isLarge ? <LargeItem {...newProps} isMember={isMember} /> : <SmallItem {...newProps} />}
</div>
}
export default Item

View File

@ -1,56 +0,0 @@
import React, { useContext } from 'react';
import { pure } from 'recompose';
import _ from 'lodash';
import HorizontalCategory from './HorizontalCategory'
import VerticalCategory from './VerticalCategory'
import LandscapeInfo from './LandscapeInfo';
import OtherLandscapeLink from './OtherLandscapeLink';
import LandscapeContext from '../../contexts/LandscapeContext'
const extractKeys = (obj, keys) => {
const attributes = _.pick(obj, keys)
return _.mapKeys(attributes, (value, key) => _.camelCase(key))
}
const LandscapeContent = ({zoom, padding = 10 }) => {
const { groupedItems, landscapeSettings, width, height } = useContext(LandscapeContext)
const elements = landscapeSettings.elements.map(element => {
if (element.type === 'LandscapeLink') {
return <OtherLandscapeLink {..._.pick(element, ['width','height','top','left','color', 'layout', 'title', 'url', 'image']) }
key={element.url}
/>
}
if (element.type === 'LandscapeInfo') {
return <LandscapeInfo {..._.pick(element, ['width', 'height', 'top', 'left']) } childrenInfo={element.children}
key='landscape-info'
/>
}
const category = groupedItems.find(c => c.key === element.category) || {}
const attributes = extractKeys(element, ['width', 'height', 'top', 'left', 'color', 'fit_width', 'is_large'])
const subcategories = category.subcategories.map(subcategory => {
const allItems = subcategory.allItems.map(item => ({ ...item, categoryAttrs: attributes }))
return { ...subcategory, allItems }
})
const Component = element.type === 'HorizontalCategory' ? HorizontalCategory : VerticalCategory
return <Component {...category} subcategories={subcategories} {...attributes} />
});
const style = {
padding,
width: width + 2 * padding,
height: height + 2 * padding,
transform: `scale(${zoom})`,
transformOrigin: '0 0'
}
return <div className="inner-landscape" style={style}>
<div style={{ position: 'relative' }}>
{elements}
</div>
</div>
};
export default pure(LandscapeContent);

View File

@ -1,76 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import _ from 'lodash';
import assetPath from '../../utils/assetPath'
import { useRouter } from 'next/router'
const LandscapeInfo = ({width, height, top, left, childrenInfo}) => {
const { query } = useRouter()
const children = childrenInfo.map(function(info) {
const positionProps = {
position: 'absolute',
top: _.isUndefined(info.top) ? null : info.top,
left: _.isUndefined(info.left) ? null : info.left,
right: _.isUndefined(info.right) ? null : info.right,
bottom: _.isUndefined(info.bottom) ? null : info.bottom,
width: _.isUndefined(info.width) ? null : info.width,
height: _.isUndefined(info.height) ? null : info.height
};
if (info.type === 'text') {
// pdf requires a normal version without a zoom trick
if (query.hasOwnProperty('pdf')) {
return <div key='text' style={{
...positionProps,
fontSize: info.font_size,
fontStyle: 'italic',
textAlign: 'justify',
zIndex: 1
}}>{info.text}</div>
// while in a browser we use a special version which renders fonts
// properly on a small zoom
} else {
return <div key='text' style={{
...positionProps,
fontSize: info.font_size * 4,
fontStyle: 'italic',
textAlign: 'justify',
zIndex: 1
}}><div style={{
position: 'absolute',
left: 0,
top: 0,
width: '400%',
height: '100%',
transform: 'scale(0.25)',
transformOrigin: 'left'
}}> {info.text} </div></div>
}
}
if (info.type === 'title') {
return <div key='title' style= {{
...positionProps,
fontSize: info.font_size,
color: '#666'
}}>{info.title}</div>
}
if (info.type === 'image') {
return <img src={assetPath(`/images/${info.image}`)} style={{...positionProps}} key={info.image} alt={info.title || info.image} />
}
});
return <div style={{
position: 'absolute',
width: width,
height: height - 20,
top: top,
left: left,
border: '1px solid black',
background: 'white',
borderRadius: 10,
marginTop: 20,
boxShadow: `0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)`
}}>{children}</div>
}
export default pure(LandscapeInfo);

View File

@ -1,74 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import assetPath from '../../utils/assetPath'
import OutboundLink from '../OutboundLink'
import InternalLink from '../InternalLink'
import { stringifyParams } from '../../utils/routing'
import { categoryBorder, categoryTitleHeight, subcategoryTitleHeight } from '../../utils/landscapeCalculations'
const CardLink = ({ url, children }) => {
const Component = url.indexOf('http') === 0 ? OutboundLink : InternalLink
const to = url.indexOf('http') === 0 ? url : stringifyParams({ mainContentMode: url })
return <Component to={to} style={{ display: 'flex', flexDirection: 'column' }}>{children}</Component>
}
const OtherLandscapeLink = function({top, left, height, width, color, title, image, url, layout}) {
const imageSrc = image || assetPath(`/images/${url}_preview.png`)
if (layout === 'category') {
return <div style={{
position: 'absolute', top, left, height, width, background: color,
cursor: 'pointer',
boxShadow: `0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)`,
padding: 1,
display: 'flex'
}}>
<CardLink url={url}>
<div style={{ width, height: 30, lineHeight: '28px', textAlign: 'center', color: 'white', fontSize: 12}}>{title}</div>
<div style={{ flex: 1, background: 'white', position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<img loading="lazy" src={imageSrc} style={{ width: width - 12, height: height - 42,
objectFit: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }} alt={title} />
</div>
</CardLink>
</div>
}
if (layout === 'subcategory') {
return <div style={{ width, left, height, top, position: 'absolute' }}>
<CardLink url={url}>
<div
style={{
position: 'absolute',
background: color,
top: subcategoryTitleHeight,
bottom: 0,
left: 0,
right: 0,
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
padding: categoryBorder,
display: 'flex'
}}
>
<div style={{
width: categoryTitleHeight,
writingMode: 'vertical-rl',
transform: 'rotate(180deg)',
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 12,
lineHeight: '13px',
color: 'white'
}}>
{title}
</div>
<div style={{ display: 'flex', flex: 1, background: 'white', justifyContent: 'center', alignItems: 'center' }}>
<img loading="lazy" src={imageSrc} alt={title}
style={{ width: width - 42, height: height - 32, objectFit: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }} />
</div>
</div>
</CardLink>
</div>;
}
}
export default pure(OtherLandscapeLink);

View File

@ -1,48 +0,0 @@
import React, { useContext } from 'react';
import { pure, withProps, toClass } from 'recompose';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import InternalLink from '../InternalLink';
import LandscapeContext from '../../contexts/LandscapeContext'
import _ from 'lodash'
import settings from 'public/settings.json';
import { stringifyParams } from '../../utils/routing'
const mainCard = [{shortTitle: 'Card', title: 'Card Mode', mode: 'card-mode', tabIndex: 0}]
const landscapes = _.map(settings.big_picture, function(section) {
return {
title: section.name,
shortTitle: section.short_name,
mode: section.url,
tabIndex: section.tab_index
}
})
const _cards = _.orderBy(mainCard.concat(landscapes), 'tabIndex').map( item => _.pick(item, ['title', 'mode', 'shortTitle']))
const SwitchButton = _ => {
const { navigate, params } = useContext(LandscapeContext)
const { mainContentMode, isEmbed } = params
const cards = _cards.map(card => ({ ...card, url: stringifyParams({ ...params, mainContentMode: card.mode })}))
if (isEmbed) {
return null;
}
return [
<Tabs
className="big-picture-switch big-picture-switch-normal"
value={mainContentMode}
onChange={(_event, mainContentMode) => navigate({ mainContentMode })}
key='tabs'
>
{ cards.map(({ mode, title, url}) => {
const link = toClass(withProps(props => { return { to: url } })(InternalLink));
return <Tab key={mode} label={title} component={link} value={mode} />
})}
</Tabs>
]
}
export default pure(SwitchButton);

View File

@ -1,53 +0,0 @@
import React, { useContext } from "react";
import Item from "./Item";
import InternalLink from "../InternalLink";
import {
calculateVerticalCategory,
categoryTitleHeight,
itemMargin, smallItemWidth,
subcategoryMargin
} from "../../utils/landscapeCalculations";
import LandscapeContext from '../../contexts/LandscapeContext'
import SubcategoryInfo from '../SubcategoryInfo'
import CategoryHeader from '../CategoryHeader'
const VerticalCategory = ({header, subcategories, top, left, width, height, color, href, fitWidth}) => {
const subcategoriesWithCalculations = calculateVerticalCategory({ subcategories, fitWidth, width })
const { guideIndex } = useContext(LandscapeContext)
return <div>
<div style={{
position: 'absolute', top, left, height, width, background: color,
boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2)',
padding: 0,
display: 'flex',
flexDirection: 'column'
}} className="big-picture-section">
<div style={{ height: categoryTitleHeight, width: '100%', display: 'flex' }}>
<CategoryHeader href={href} label={header} guideAnchor={guideIndex[header]} background={color} />
</div>
<div style={{ width: '100%', position: 'relative', flex: 1, padding: `${subcategoryMargin}px 0`, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', background: 'white' }}>
{subcategoriesWithCalculations.map(subcategory => {
const { width, columns, name } = subcategory
const style = { display: 'grid', gridTemplateColumns: `repeat(${columns}, ${smallItemWidth}px)` }
const extraStyle = fitWidth ? { justifyContent: 'space-evenly', flex: 1 } : { gridGap: itemMargin }
const path = [header, name].join(' / ')
return <div key={subcategory.name} style={{position: 'relative', flexGrow: subcategory.rows, display: 'flex', flexDirection: 'column' }}>
<div style={{ lineHeight: '15px', textAlign: 'center'}}>
<InternalLink to={subcategory.href}>{name}</InternalLink>
</div>
<div style={{width, overflow: 'hidden', margin: '0 auto', ...style, ...extraStyle}}>
{subcategory.allItems.map(item => <Item item={item} key={item.name} fitWidth={fitWidth} />)}
{ guideIndex[path] && <SubcategoryInfo label={name} anchor={guideIndex[path]} column={columns}/> }
</div>
</div>
})}
</div>
</div>
</div>
}
export default VerticalCategory

View File

@ -1,11 +0,0 @@
// locate zoom buttons
import React from 'react';
import { pure } from 'recompose';
const Zoom = function({zoom, children}) {
return <div style={{position:'relative', zoom: zoom}}>
{children}
</div>
}
export default pure(Zoom);

View File

@ -1,39 +0,0 @@
// locate zoom buttons
import { pure } from 'recompose';
import IconButton from '@material-ui/core/IconButton';
import Button from '@material-ui/core/Button';
import RemoveCircleIcon from '@material-ui/icons/RemoveCircle';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import React, { useContext } from 'react';
import { zoomLevels } from '../../utils/zoom'
import LandscapeContext from '../../contexts/LandscapeContext'
const ZoomButtons = _ => {
const { navigate, params } = useContext(LandscapeContext)
const { zoom } = params
const minZoom = zoomLevels[0]
const maxZoom = zoomLevels.slice(-1)[0]
const zoomIndex = zoomLevels.indexOf(zoom)
const canZoomOut = zoom !== minZoom
const canZoomIn = zoom !== maxZoom
const zoomText = Math.round(zoom * 100) + '%'
const setZoom = zoom => navigate({ zoom: zoom })
const zoomIn = _ => setZoom(zoomLevels[zoomIndex + 1] || maxZoom)
const zoomOut = _ => setZoom(zoomLevels[zoomIndex - 1] || minZoom)
const resetZoom = _ => setZoom()
return <div className="zoom-buttons">
<IconButton disabled={!canZoomOut} onClick={zoomOut} className='zoom-change' title="Zoom out" size="small">
<RemoveCircleIcon />
</IconButton>
<Button onClick={resetZoom} className='zoom-reset' title="Reset zoom" size="small">{zoomText}</Button>
<IconButton disabled={!canZoomIn} onClick={zoomIn} className='zoom-change' title="Zoom in" size="small">
<AddCircleIcon />
</IconButton>
</div>
}
export default pure(ZoomButtons);

View File

@ -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>
`;
}

View File

@ -1,33 +0,0 @@
import React, { useContext } from 'react';
import { pure } from 'recompose';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import TreeSelector from './TreeSelector';
import LandscapeContext from '../contexts/LandscapeContext'
import { options } from '../types/fields'
const CategoryFilter = _ => {
const { navigate, params } = useContext(LandscapeContext)
const { mainContentMode, filters } = params
const isBigPicture = mainContentMode !== 'card-mode'
const value = filters.landscape
const _options = options('landscape')
const onChange = landscape => navigate({ filters: { landscape }})
if (!isBigPicture) {
return <TreeSelector value={value} options={_options} onChange={onChange} />;
} else {
return (
<Select
disabled
value="empty"
style={{width:175 ,fontSize:'0.8em'}}
>
<MenuItem value="empty">
<em>N/A</em>
</MenuItem>
</Select>
);
}
};
export default pure(CategoryFilter);

View File

@ -1,16 +1,30 @@
import React from 'react'
import css from 'styled-jsx/css'
import GuideLink from './GuideLink'
import { categoryTitleHeight } from '../utils/landscapeCalculations'
import InternalLink from './InternalLink'
import { getContrastRatio } from '@material-ui/core/styles'
const getContrastRatio = require('get-contrast-ratio').default;
const CategoryHeader = ({ href, label, guideAnchor, background, rotate = false }) => {
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)'
const infoEl = css.resolve`
a {
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;
@ -20,45 +34,7 @@ const CategoryHeader = ({ href, label, guideAnchor, background, rotate = false }
font-size: 18px;
color: ${color};
background: ${backgroundColor};
transform: ${rotate ? 'rotate(180deg)' : 'none'};
}
a:hover {
color: ${color};
background: none;
}
a :global(svg) {
stroke: ${color};
}
`
const linkEl = css.resolve`
a {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
flex: 1;
font-size: 12px;
color: ${color};
background: none;
}
a:hover {
font-weight: bold;
background: ${backgroundColor};
}
`
return <>
{infoEl.styles}
{linkEl.styles}
<InternalLink to={href} className={linkEl.className}>{label}</InternalLink>
{ guideAnchor && <GuideLink label={label} anchor={guideAnchor} className={infoEl.className} /> }
</>
transform: ${rotate ? 'rotate(180deg)' : 'none' };
`}) : '' }
`;
}
export default CategoryHeader

View File

@ -1,32 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
const CheckboxSelector = ({value, options, onChange}) => {
const valueOf = function(checkbox) {
return value.indexOf(checkbox) !== -1;
};
const handleCheckboxChange = function(checkbox, checked) {
if (checked) {
onChange(value.concat([checkbox]));
} else {
onChange(value.filter(function(x) { return x !== checkbox; }))
}
};
return <FormGroup>
{ options.map( (el) => (
<FormControlLabel key={el.id} control={
<Checkbox onClick={function() {
handleCheckboxChange(el.id, !valueOf(el.id));
}}
checked={valueOf(el.id)}
/>
} label={el.label}
/>
)) }
</FormGroup>
};
export default pure(CheckboxSelector);

View File

@ -1,40 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import Checkbox from '@material-ui/core/Checkbox';
import ListItemText from '@material-ui/core/ListItemText';
const idToValue = (id) => id !== null ? id : 'any';
const valueToId = (value) => value === 'any' ? null : value;
const ComboboxSelector = ({value, options, onChange}) => {
const renderValue = function(selected) {
if (selected.length === 0) {
return 'Any';
}
return selected.join(', ');
}
return <Select
multiple
style={{width:175, fontSize:'0.8em'}}
value={idToValue(value)}
onChange={(e) => onChange(valueToId(e.target.value))}
renderValue={renderValue }
displayEmpty
>
{ options.map( (el) => (
<MenuItem key={idToValue(el.id)}
value={idToValue(el.id)}
style={{height:5}}
>
<Checkbox color="primary" disableRipple checked={value.indexOf(el.id) !== -1} />
<ListItemText disableTypography style={{fontSize:'0.8em'}} primary={el.label}/>
</MenuItem>
)) }
</Select>
};
export default pure(ComboboxSelector);

View File

@ -1,24 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
const idToValue = (id) => id !== null ? id : 'any';
const valueToId = (value) => value === 'any' ? null : value;
const ComboboxSelector = ({value, options, onChange}) => {
return <Select
style={{width:175, fontSize:'0.8em'}}
value={idToValue(value)}
onChange={(e) => onChange(valueToId(e.target.value))}
>
{ options.map( (el) => (
<MenuItem key={idToValue(el.id)}
value={idToValue(el.id)}
style={{height:5, fontSize:'0.8em'}}>{el.label}</MenuItem>
)) }
</Select>
};
export default pure(ComboboxSelector);

View File

@ -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">&nbsp;(${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;
};

View File

@ -1,14 +0,0 @@
import React, { useContext } from 'react';
import { pure } from 'recompose';
import OutboundLink from './OutboundLink';
import LandscapeContext from '../contexts/LandscapeContext'
import { stringifyParams } from '../utils/routing'
const EmbeddedFooter = () => {
const { params } = useContext(LandscapeContext)
const url = stringifyParams({ ...params, isEmbed: null })
return <h1 style={{ marginTop: 20, width: '100%', textAlign: 'center' }}>
<OutboundLink to={url}>View</OutboundLink> the full interactive landscape
</h1>
}
export default pure(EmbeddedFooter);

View File

@ -1,55 +0,0 @@
import SystemUpdateIcon from '@material-ui/icons/SystemUpdate'
import useSWR from 'swr'
import assetPath from '../utils/assetPath'
import { useContext, useEffect, useState } from 'react'
import LandscapeContext from '../contexts/LandscapeContext'
import Parser from 'json2csv/lib/JSON2CSVParser'
import { flattenItems } from '../utils/itemsCalculator'
const fetchItems = shouldFetch => useSWR(shouldFetch ? assetPath(`/data/items-export.json`) : null)
const _downloadCSV = (allItems, selectedItems) => {
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 });
const filename = 'interactive_landscape.csv'
const data = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv)
const link = document.createElement('a');
link.setAttribute('href', data);
link.setAttribute('download', filename);
link.click();
}
const ExportCsv = _ => {
const { groupedItems } = useContext(LandscapeContext)
const [shouldFetch, setShouldFetch] = useState(false)
const { data: itemsForExport } = fetchItems(!!shouldFetch)
const fetched = !!itemsForExport
const selectedItems = flattenItems(groupedItems)
.reduce((acc, item) => ({ ...acc, [item.id]: true }), {})
const downloadCSV = () => _downloadCSV(itemsForExport, selectedItems)
const onClick = _ => {
if (!fetched) {
setShouldFetch(true)
} else {
downloadCSV()
}
}
useEffect(() => {
if (fetched) {
downloadCSV()
}
}, [fetched])
return <a className="filters-action" onClick={onClick} aria-label="Download as CSV">
<SystemUpdateIcon/><span>Download as CSV</span>
</a>
};
export default ExportCsv

View File

@ -1,66 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import FormGroup from '@material-ui/core/FormGroup';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import ProjectFilterContainer from './ProjectFilterContainer';
import LicenseFilterContainer from './LicenseFilterContainer';
import OrganizationFilterContainer from './OrganizationFilterContainer';
import HeadquartersFilterContainer from './HeadquartersFilterContainer';
import ArrayFilterContainer from './ArrayFilterContainer';
import fields from '../types/fields';
import CategoryFilter from './CategoryFilter'
const Filters = () => {
return <div>
<FormGroup row>
<FormControl component="fieldset">
<FormLabel component="legend">{fields.landscape.label}</FormLabel>
<CategoryFilter/>
</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>
<FormGroup row>
<FormControl component="fieldset">
<FormLabel component="legend">{fields.companyType.label}</FormLabel>
<ArrayFilterContainer name="companyType" />
</FormControl>
</FormGroup>
<FormGroup row>
<FormControl component="fieldset">
<FormLabel component="legend">{fields.industries.label}</FormLabel>
<ArrayFilterContainer name="industries" />
</FormControl>
</FormGroup>
</div>;
}
export default pure(Filters);

View File

@ -1,14 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import OutboundLink from './OutboundLink';
import settings from 'public/settings.json'
const Footer = () => {
return <div style={{ marginTop: 10, fontSize:'9pt', width: '100%', textAlign: 'center' }}>
{settings.home.footer} For more information, please see the&nbsp;
<OutboundLink eventLabel="crunchbase-terms" to={`https://github.com/${settings.global.repo}/blob/HEAD/README.md#license`}>
license
</OutboundLink> info.
</div>
}
export default pure(Footer);

View File

@ -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>`;
}

View File

@ -1,15 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import FormGroup from '@material-ui/core/FormGroup';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import GroupingSelector from './GroupingSelector';
const Grouping = () => {
return <FormGroup row>
<FormControl component="fieldset">
<FormLabel component="legend">Grouping</FormLabel>
<GroupingSelector />
</FormControl>
</FormGroup>;
};
export default pure(Grouping);

View File

@ -1,38 +0,0 @@
import React, { useContext } from 'react';
import { pure } from 'recompose';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import ComboboxSelector from './ComboboxSelector';
import fields from '../types/fields'
import LandscapeContext from '../contexts/LandscapeContext'
const groupingFields = ['landscape', 'relation', 'license', 'organization', 'headquarters'];
const options = [{
id: 'no',
label: 'No Grouping',
url: 'no'
}].concat(groupingFields.map(id => ({ id, label: fields[id].groupingLabel })))
const GroupingSelector = _ => {
const { navigate, params } = useContext(LandscapeContext)
const { grouping, mainContentMode } = params
const isBigPicture = mainContentMode !== 'card-mode'
const onChange = grouping => navigate({ grouping })
if (!isBigPicture) {
return <ComboboxSelector value={grouping} options={options} onChange={onChange} />;
} else {
return (
<Select
disabled
value="empty"
style={{width:175 ,fontSize:'0.8em'}}
>
<MenuItem value="empty">
<em>N/A</em>
</MenuItem>
</Select>
);
}
};
export default pure(GroupingSelector);

View File

@ -1,27 +1,10 @@
import React from 'react'
import css from 'styled-jsx/css'
import InfoIcon from '@material-ui/icons/InfoOutlined'
import OutboundLink from './OutboundLink'
import assetPath from '../utils/assetPath'
const GuideLink = ({ anchor, label, className="" }) => {
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 = assetPath(`/guide#${anchor}`)
const to = h(`guide#${anchor}`);
const svgEl = css.resolve`
svg {
stroke-width: 0;
}
svg:hover {
stroke-width: 0.5;
}
`
return <OutboundLink className={className} to={to} aria-label={ariaLabel}>
{svgEl.styles}
<InfoIcon style={{ fontSize: 'inherit' }} className={svgEl.className}/>
</OutboundLink>
return `<a data-type="external" target="_blank" style="${style}" aria-label="${h(ariaLabel)}" href="${to}">
${guideLink}
</a>`;
}
export default GuideLink

View File

@ -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>
`;
}

View File

@ -1,61 +0,0 @@
import React, { useContext } from 'react'
import Link from 'next/link'
import LandscapeContext from '../contexts/LandscapeContext'
const ToggleItem = ({ isActive, title, to }) => {
return <span className={`toggle-item ${isActive ? 'active' : ''}`}>
<style jsx>{`
.toggle-item {
width: 50%;
text-align: center;
padding: 2px;
color: #2E67BF;
background: white;
display: inline-block;
}
.toggle-item.active {
background: #2E67BF;
color: white;
}
a {
transition: none;
}
a:hover {
color: #2E67BF;
}
`}</style>
{isActive ? title : <Link href={to} prefetch={false}>
<a>{title}</a>
</Link>}
</span>
}
const GuideToggle = ({ active }) => {
const { guideIndex } = useContext(LandscapeContext)
if (active === 'landscape' && Object.keys(guideIndex).length === 0) {
return null
}
return <div className="guide-toggle">
<style jsx>{`
.guide-toggle {
border: 2px solid #2E67BF;
background: #2E67BF;
border-radius: 4px;
max-width: 400px;
margin: 0;
font-size: 14px;
margin: 15px 0;
max-width: 165px;
}
`}</style>
<ToggleItem isActive={active === 'landscape'} title="Landscape" to="/" />
<ToggleItem isActive={active === 'guide'} title="Guide" to="/guide" />
</div>
}
export default GuideToggle

View File

@ -1,17 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import LandscapeLogo from './LandscapeLogo'
import OrganizationLogo from '../OrganizationLogo'
const Header = _ => {
return (
<div className="header_container">
<div className="header">
<LandscapeLogo />
<OrganizationLogo />
</div>
</div>
);
};
export default pure(Header);

View File

@ -1,14 +0,0 @@
import TreeSelector from './TreeSelector';
import { options } from '../types/fields';
import { useContext } from 'react'
import LandscapeContext from '../contexts/LandscapeContext'
const HeadquartersFilterContainer = () => {
const { navigate, params } = useContext(LandscapeContext)
const value = params.filters.headquarters
const _options = options('headquarters')
const onChange = value => navigate({ filters: { headquarters: value }})
return <TreeSelector onChange={onChange} value={value} options={_options} />
}
export default HeadquartersFilterContainer

View File

@ -1,210 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import Head from 'next/head'
import { pure } from 'recompose';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import MenuIcon from '@material-ui/icons/Menu';
import classNames from 'classnames'
import Filters from './Filters';
import Grouping from './Grouping';
import Sorting from './Sorting';
import Ad from './Ad';
import OutboundLink from './OutboundLink';
import TweetButton from './TweetButton';
import Footer from './Footer';
import EmbeddedFooter from './EmbeddedFooter';
import isGoogle from '../utils/isGoogle';
import settings from 'public/settings.json'
import useCurrentDevice from '../utils/useCurrentDevice'
import LandscapeContent from './BigPicture/LandscapeContent'
import LandscapeContext from '../contexts/LandscapeContext'
import ResetFilters from './ResetFilters'
import ItemDialog from './ItemDialog'
import ZoomButtons from './BigPicture/ZoomButtons'
import Summary from './Summary'
import FullscreenButton from './BigPicture/FullscreenButton'
import Header from './Header'
import SwitchButton from './BigPicture/SwitchButton'
import ExportCsv from './ExportCsv'
import MainContent from './MainContent'
import Presets from './Presets'
import GuideToggle from './GuideToggle'
function preventDefault(e){
const modal = e.srcElement.closest('.modal-body');
if (!modal) {
e.preventDefault();
}
}
function disableScroll(){
const shadow = document.querySelector('.MuiBackdrop-root');
if (shadow) {
shadow.addEventListener('touchmove', preventDefault, { passive: false });
}
document.body.addEventListener('touchmove', preventDefault, { passive: false });
}
function enableScroll(){
const shadow = document.querySelector('.MuiBackdrop-root');
if (shadow) {
shadow.removeEventListener('touchmove', preventDefault);
}
document.body.removeEventListener('touchmove', preventDefault);
}
const HomePage = _ => {
const { params, landscapeSettings } = useContext(LandscapeContext)
const { mainContentMode, zoom, isFullscreen, isEmbed, onlyModal, selectedItemId } = params
const isBigPicture = mainContentMode !== 'card-mode';
const [sidebarVisible, setSidebarVisible] = useState(false)
const showSidebar = _ => setSidebarVisible(true)
const hideSidebar = _ => setSidebarVisible(false)
const [lastScrollPosition, setLastScrollPosition] = useState(0)
const currentDevice = useCurrentDevice()
if (onlyModal) {
document.querySelector('body').classList.add('popup');
}
useEffect(() => {
const { classList } = document.querySelector('html')
isFullscreen ? classList.add('fullscreen') : classList.remove('fullscreen')
}, [isFullscreen])
useEffect(() => {
if (currentDevice.ios()) {
if (selectedItemId) {
if (!document.querySelector('.iphone-scroller')) {
setLastScrollPosition((document.scrollingElement || document.body).scrollTop)
}
document.querySelector('html').classList.add('has-selected-item');
(document.scrollingElement || document.body).scrollTop = 0;
disableScroll();
} else {
document.querySelector('html').classList.remove('has-selected-item');
if (document.querySelector('.iphone-scroller')) {
(document.scrollingElement || document.body).scrollTop = lastScrollPosition;
}
enableScroll();
}
}
}, [currentDevice, selectedItemId])
useEffect(() => {
if (isEmbed) {
if (window.parentIFrame) {
if (selectedItemId) {
window.parentIFrame.sendMessage({type: 'showModal'})
} else {
window.parentIFrame.sendMessage({type: 'hideModal'})
}
if (selectedItemId) {
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);
});
}
}
const { classList } = document.documentElement
if (!classList.contains('embed')) {
classList.add('embed');
}
}
}, [selectedItemId])
if ((isGoogle() || onlyModal) && selectedItemId) {
return <ItemDialog />;
}
const isIphone = currentDevice.ios()
const titlePrefix = isBigPicture ? (landscapeSettings.isMain ? '' : landscapeSettings.name) : 'Card Mode'
const title = [titlePrefix, settings.global.meta.title].filter(_ => _).join(' - ')
return <>
{selectedItemId && <ItemDialog/>}
<Head>
<title>{title}</title>
<meta property="og:title" content={title}/>
</Head>
<div id="home" className={classNames('app',{'filters-opened' : sidebarVisible, 'big-picture': isBigPicture })}>
<div style={{marginTop: isIphone && selectedItemId ? -lastScrollPosition : 0}} className={classNames({"iphone-scroller": isIphone && selectedItemId}, 'main-parent')} >
{ !isEmbed && !isFullscreen && <>
<Header />
<IconButton className="sidebar-show" title="Show sidebar" onClick={showSidebar}><MenuIcon /></IconButton>
<div className="sidebar">
<div className="sidebar-scroll">
<IconButton className="sidebar-collapse" title="Hide sidebar" size="small" onClick={hideSidebar}><CloseIcon /></IconButton>
<GuideToggle active="landscape"/>
<ResetFilters />
<Grouping/>
<Sorting/>
<Filters />
<Presets />
<ExportCsv />
<Ad />
</div>
</div>
</>}
{sidebarVisible && <div className="app-overlay" onClick={hideSidebar}></div>}
<div className={classNames('main', {'embed': isEmbed})}>
<div className="disclaimer">
<span dangerouslySetInnerHTML={{__html: settings.home.header}} />
Please <OutboundLink to={`https://github.com/${settings.global.repo}`}>open</OutboundLink> a pull request to
correct any issues. Greyed logos are not open source. Last Updated: {process.env.lastUpdated}
</div>
<Summary />
<div className="cards-section">
<SwitchButton />
<div className="right-buttons">
<TweetButton cls="tweet-button-main"/>
<FullscreenButton/>
<ZoomButtons/>
</div>
{ isBigPicture &&
<div className="landscape-flex">
<div className="landscape-wrapper">
<LandscapeContent zoom={zoom} />
</div>
</div>
}
{ !isBigPicture && <MainContent /> }
</div>
{ !isEmbed && !isBigPicture && <Footer/> }
{ isEmbed && <EmbeddedFooter/> }
</div>
</div>
</div>
</>
};
export default pure(HomePage);

View File

@ -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&nbsp;
<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>
`;
}

View File

@ -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>`;
}

View File

@ -1,18 +0,0 @@
import React, { useContext } from 'react';
import Link from 'next/link'
import isGoogle from '../utils/isGoogle';
import LandscapeContext from '../contexts/LandscapeContext'
const InternalLink = ({to, children, onClick, className, ...other}) => {
const { params } = useContext(LandscapeContext)
if (params.isEmbed || isGoogle() || params.onlyModal || !to) {
return <span className={`${className}`} {...other}>{children}</span>;
} else {
return <Link href={to} prefetch={false}>
<a className={`${className} nav-link`} {...other}>{children}</a>
</Link>
}
}
export default InternalLink

78
src/components/Item.js Normal file
View File

@ -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>`;
}

View File

@ -1,31 +0,0 @@
import React, { useContext } from 'react';
import { pure } from 'recompose';
import Dialog from '@material-ui/core/Dialog';
import classNames from 'classnames'
import ItemDialogContent from './ItemDialogContent';
import ItemDialogButtons from './ItemDialogButtons'
import LandscapeContext from '../contexts/LandscapeContext'
import useSWR from 'swr'
import assetPath from '../utils/assetPath'
const fetchItem = itemId => useSWR(itemId ? assetPath(`/data/items/${itemId}.json`) : null)
const ItemDialog = _ => {
const { navigate, params, entries } = useContext(LandscapeContext)
const { onlyModal, selectedItemId } = params
const { data: selectedItem } = fetchItem(selectedItemId)
const closeDialog = _ => onlyModal ? _ : navigate({ selectedItemId: null }, { scroll: false })
const nonoss = selectedItem && selectedItem.oss === false
const loading = selectedItemId && !selectedItem
const itemInfo = selectedItem || entries.find(({ id }) => id === selectedItemId)
return (
<Dialog open={!!selectedItemId} onClose={closeDialog} transitionDuration={400}
classes={{paper:'modal-body'}}
className={classNames('modal', 'product', {nonoss})}>
{ !onlyModal && <ItemDialogButtons closeDialog={closeDialog} /> }
<ItemDialogContent itemInfo={itemInfo} loading={loading}/>
</Dialog>
);
}
export default pure(ItemDialog);

View File

@ -1,26 +0,0 @@
import React, { useContext } from 'react';
import { pure } from 'recompose';
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import KeyHandler from 'react-key-handler';
import LandscapeContext from '../contexts/LandscapeContext'
const ItemDialogButtons = ({ closeDialog }) => {
const { navigate, nextItemId, previousItemId } = useContext(LandscapeContext)
const onSelectItem = selectedItemId => navigate({ selectedItemId }, { scroll: false })
return (
<div className='modal-buttons'>
{ nextItemId && <KeyHandler keyValue="ArrowRight" onKeyHandle={() => onSelectItem(nextItemId)} /> }
{ previousItemId && <KeyHandler keyValue="ArrowLeft" onKeyHandle={() => onSelectItem(previousItemId)} /> }
<a className="modal-close" onClick={closeDialog}>×</a>
<span className="modal-prev" disabled={!previousItemId} onClick={(e) => {e.stopPropagation(); onSelectItem(previousItemId)}}>
<ChevronLeftIcon style={{ fontSize:'1.2em'}} />
</span>
<span className="modal-next" disabled={!nextItemId} onClick={(e) => {e.stopPropagation(); onSelectItem(nextItemId)}}>
<ChevronRightIcon style={{ fontSize:'1.2em'}} />
</span>
</div>
);
}
export default pure(ItemDialogButtons);

View File

@ -1,661 +0,0 @@
import React, { Fragment, useContext, useEffect, useState } from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
import StarIcon from '@material-ui/icons/Star';
import KeyHandler from 'react-key-handler';
import _ from 'lodash';
import OutboundLink from './OutboundLink';
import millify from 'millify';
import relativeDate from 'relative-date';
import formatNumber from '../utils/formatNumber';
import isParent from '../utils/isParent';
import InternalLink from './InternalLink';
import fields from '../types/fields';
import isGoogle from '../utils/isGoogle';
import settings from 'public/settings.json';
import TweetButton from './TweetButton';
import TwitterTimeline from "./TwitterTimeline";
import {Bar, Pie, defaults} from 'react-chartjs-2';
import 'chartjs-adapter-date-fns';
import classNames from 'classnames'
import CreateWidthMeasurer from 'measure-text-width';
import assetPath from '../utils/assetPath'
import { stringifyParams } from '../utils/routing'
import LandscapeContext from '../contexts/LandscapeContext'
import Head from 'next/head'
import useWindowSize from '../utils/useWindowSize'
const closeUrl = params => stringifyParams({ mainContentMode: 'card-mode', selectedItemId: null, ...params })
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 = fields.relation.valuesMap[relation]
if (relationInfo && relationInfo.color) {
return {
border: '4px solid ' + relationInfo.color
};
} else {
return {};
}
}
const showTwitter = !isGoogle();
const iconGithub = <svg viewBox="0 0 24 24">
<path d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58
9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81
5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18
9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5
6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84
13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39
18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68
14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
</svg>;
const linkTag = (label, { name, url = null, color = 'blue', multiline = false }) => {
return (<InternalLink to={url || '/'} className={`tag tag-${color} ${multiline ? 'multiline' : ''}`}>
{(name ? <span className="tag-name">{name}</span> : null)}
<span className="tag-value">{label}</span>
</InternalLink>)
}
const parentTag = (project) => {
const membership = Object.values(settings.membership).find(({ crunchbase_and_children }) => {
return isParent(crunchbase_and_children, project)
});
if (membership) {
const { label, name, crunchbase_and_children } = membership;
const slug = crunchbase_and_children.split("/").pop();
const url = closeUrl({ grouping: 'organization', filters: {parents: slug}})
return linkTag(label, {name, url});
}
}
const projectTag = function({relation, isSubsidiaryProject, project, ...item}) {
if (relation === false) {
return null;
}
const { prefix, tag } = fields.relation.valuesMap[project] || {};
if (prefix && tag) {
const url = closeUrl({ filters: { relation: project }})
return linkTag(tag, {name: prefix, url })
}
if (isSubsidiaryProject) {
const url = closeUrl({ mainContentMode: 'card-mode', filters: { relation: 'member', organization: item.organization }})
return linkTag("Subsidiary Project", { name: settings.global.short_name, url });
}
return null;
};
const memberTag = function({relation, member, enduser}) {
if (relation === 'member' || relation === 'company') {
const info = settings.membership[member];
if (!info) {
return null;
}
const name = info.name;
const label = enduser ? (info.end_user_label || info.label) : info.label ;
if (!label) {
return null;
}
const url = closeUrl({ filters: { relation }})
return linkTag(label, {name: name, url });
}
return null;
}
const openSourceTag = function(oss) {
if (oss) {
const url = closeUrl({ grouping: 'license', filters: {license: 'Open Source'}})
return linkTag("Open Source Software", { url, color: "orange" });
}
};
const licenseTag = function({relation, license, hideLicense}) {
const { label } = _.find(fields.license.values, {id: license});
const [width, setWidth] = useState(0)
useEffect(() => {
const width = CreateWidthMeasurer(window).setFont('0.6rem Roboto');
setWidth(width)
}, [label])
if (relation === 'company' || hideLicense) {
return null;
}
const url = closeUrl({ grouping: 'license', filters: { license }});
return linkTag(label, { name: "License", url, color: "purple", multiline: width > 90 });
}
const badgeTag = function(itemInfo) {
if (settings.global.hide_best_practices) {
return null;
}
if (!itemInfo.bestPracticeBadgeId) {
if (itemInfo.oss) {
const emptyUrl="https://bestpractices.coreinfrastructure.org/";
return (<OutboundLink to={emptyUrl} className="tag tag-grass">
<span className="tag-value">No CII Best Practices </span>
</OutboundLink>);
} else {
return null;
}
}
const url = `https://bestpractices.coreinfrastructure.org/en/projects/${itemInfo.bestPracticeBadgeId}`;
const label = itemInfo.bestPracticePercentage === 100 ? 'passing' : (itemInfo.bestPracticePercentage + '%');
return (<OutboundLink to={url} className="tag tag-grass">
<span className="tag-name">CII Best Practices</span>
<span className="tag-value">{label}</span>
</OutboundLink>);
}
const chart = function(itemInfo) {
const { params } = useContext(LandscapeContext)
if (params.isEmbed || !itemInfo.github_data || !itemInfo.github_data.languages) {
return null;
}
const callbacks = defaults.plugins.tooltip.callbacks;
function percents(v) {
const p = Math.round(v / total * 100);
if (p === 0) {
return '<1%';
} else {
return p + '%';
}
}
const newCallbacks = { label: function(tooltipItem) {
const v = tooltipItem.dataset.data[tooltipItem.dataIndex];
const value = millify(v, {precision: 1});
const language = languages[tooltipItem.dataIndex];
return `${language.name} ${percents(language.value)} (${value})`;
}};
const allLanguages = itemInfo.github_data.languages;
const languages = (function() {
const maxEntries = 7;
if (allLanguages.length <= maxEntries) {
return allLanguages
} else {
return allLanguages.slice(0, maxEntries).concat([{
name: 'Other',
value: _.sum( allLanguages.slice(maxEntries - 1).map( (x) => x.value)),
color: 'Grey'
}]);
}
})();
const data = {
labels: languages.map((x) => x.name),
datasets: [{
data: languages.map( (x) => x.value),
backgroundColor: languages.map( (x) => x.color)
}]
};
const total = _.sumBy(languages, 'value');
function getLegendText(language) {
return `${language.name} ${percents(language.value)}`;
}
const legend = <div style={{position: 'absolute', width: 170, left: 0, top: 0, marginTop: -5, marginBottom: 5, fontSize: '0.8em' }}>
{languages.map(function(language) {
const url = language.name === 'Other' ? null : closeUrl({ grouping: 'no', filters: {language: language.name }});
return <div key={language.name} style = {{
position: 'relative',
marginTop: 2,
height: 12
}} >
<div style={{display: 'inline-block', position: 'absolute', height: 12, width: 12, background: language.color, top: 2, marginRight: 4}} />
<div style={{display: 'inline-block', position: 'relative', width: 125, left: 16, whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden'}}>
<InternalLink to={url}>{ getLegendText(language) } </InternalLink></div>
</div>
})}
</div>
return <div style={{width: 220, height: 120, position: 'relative'}}>
<div style={{marginLeft: 170, width: 100, height: 100}}>
<Pie height={100} width={100} data={data} options={{plugins: {legend: { display: false}, tooltip: {callbacks: newCallbacks}}}} />
</div>
{ legend }
</div>
}
const participation = function(itemInfo) {
const { innerWidth } = useWindowSize();
const { params } = useContext(LandscapeContext)
if (params.isEmbed || !itemInfo.github_data || !itemInfo.github_data.contributions) {
return null;
}
let lastMonth = null;
let lastWeek = null;
const data = {
labels: _.range(0, 51).map(function(week) {
const firstWeek = new Date(itemInfo.github_data.firstWeek.replace('Z', 'T00:00:00Z'));
firstWeek.setDate(firstWeek.getDate() + week * 7);
const m = firstWeek.getMonth();
if (lastMonth === null) {
lastMonth = m;
lastWeek = week;
}
else if (m % 12 === (lastMonth + 2) % 12) {
if (week > lastWeek + 6) {
lastMonth = m;
lastWeek = week;
} else {
return '';
}
} else {
return '';
}
const result = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ')[m];
return result;
}),
datasets: [{
backgroundColor: 'darkblue',
labels: [],
data: itemInfo.github_data.contributions.split(';').map( (x)=> +x).slice(-51)
}]
};
const callbacks = defaults.plugins.tooltip.callbacks;
const newCallbacks = { title: function(data) {
const firstWeek = new Date(itemInfo.github_data.firstWeek.replace('Z', 'T00:00:00Z'));
const week = data[0].dataIndex;
firstWeek.setDate(firstWeek.getDate() + week * 7);
const s = firstWeek.toISOString().substring(0, 10);
return s;
}};
const options = {
plugins: {
legend: { display: false },
tooltip: {callbacks: newCallbacks}
},
scales: {
x: {
grid: { display: false },
ticks: {
padding: -1,
autoSkip: false,
minRotation: 0,
maxRotation: 0
},
title: {
display: false
}
},
y: {
ticks: {
beginAtZero: true,
callback: function (value) { if (Number.isInteger(value)) { return value; } }
}
}
}
};
const width = Math.min(innerWidth - 110, 300);
return <div style={{width: width, height: 150, position: 'relative'}}>
<Bar height={150} width={width} data={data} options={options} />
<div style={{
transform: 'rotate(-90deg)',
position: 'absolute',
left: -24,
fontSize: 10,
top: 59
}}>Commits</div>
</div>;
}
function handleUp() {
productScrollEl.scrollBy({top: -200, behavior: 'smooth'});
}
function handleDown() {
productScrollEl.scrollBy({top: 200, behavior: 'smooth' });
}
const ItemDialogContent = ({ itemInfo, loading }) => {
const { params } = useContext(LandscapeContext)
const { onlyModal } = params
const [showAllRepos, setShowAllRepos] = useState(false)
const { innerWidth, innerHeight } = useWindowSize()
const linkToOrganization = closeUrl({ grouping: 'organization', filters: {organization: itemInfo.organization}});
const itemCategory = function(path) {
var separator = <span className="product-category-separator" key="product-category-separator"></span>;
var subcategory = _.find(fields.landscape.values,{id: path});
var category = _.find(fields.landscape.values, {id: subcategory.parentId});
var categoryMarkup = (
<InternalLink key="category" to={closeUrl({ grouping: 'landscape', filters: {landscape: category.id}})}>{category.label}</InternalLink>
)
var subcategoryMarkup = (
<InternalLink key="subcategory" to={closeUrl({ grouping: 'landscape', filters: {landscape: path}})}>{subcategory.label}</InternalLink>
)
return (<span>{[categoryMarkup, separator, subcategoryMarkup]}</span>);
}
const twitterElement = itemInfo.twitter &&
<div className="product-property row">
<div className="product-property-name col col-40">Twitter</div>
<div className="product-property-value col col-60">
<OutboundLink to={itemInfo.twitter}>{formatTwitter(itemInfo.twitter)}</OutboundLink>
</div>
</div>;
const latestTweetDateElement = itemInfo.twitter && (
<div className="product-property row">
<div className="product-property-name col col-50">Latest Tweet</div>
<div className="product-property-value col col-50">
{ itemInfo.latestTweetDate && (
<OutboundLink to={itemInfo.twitter}>{formatDate(itemInfo.latestTweetDate)}</OutboundLink>
)}
</div>
</div>
);
const firstCommitDateElement = itemInfo.firstCommitDate && (
<div className="product-property row">
<div className="product-property-name col col-40">First Commit</div>
<div className="product-property-value tight-col col-60">
<OutboundLink to={itemInfo.firstCommitLink} >{formatDate(itemInfo.firstCommitDate)}</OutboundLink>
</div>
</div>
);
const contributorsCountElement = itemInfo.contributorsCount ? (
<div className="product-property row">
<div className="product-property-name col col-40">Contributors</div>
<div className="product-property-value tight-col col-60">
<OutboundLink to={itemInfo.contributorsLink}>
{itemInfo.contributorsCount > 500 ? '500+' : itemInfo.contributorsCount }
</OutboundLink>
</div>
</div>
) : null;
const headquartersElement = itemInfo.headquarters && itemInfo.headquarters !== 'N/A' && (
<div className="product-property row">
<div className="product-property-name col col-40">Headquarters</div>
<div className="product-property-value tight-col col-60"><InternalLink to={closeUrl({ grouping: 'headquarters', filters:{headquarters:itemInfo.headquarters}})}>{itemInfo.headquarters}</InternalLink></div>
</div>
);
const amountElement = !loading && !settings.global.hide_funding_and_market_cap && Number.isInteger(itemInfo.amount) && (
<div className="product-property row">
<div className="product-property-name col col-40">{itemInfo.amountKind === 'funding' ? 'Funding' : 'Market Cap'}</div>
{ itemInfo.amountKind === 'funding' &&
<div className="product-property-value tight-col col-60">
<OutboundLink to={itemInfo.crunchbase + '#section-funding-rounds'}>
{'$' + millify(itemInfo.amount)}
</OutboundLink>
</div>
}
{ itemInfo.amountKind !== 'funding' &&
<div className="product-property-value tight-col col-60">
<OutboundLink to={'https://finance.yahoo.com/quote/' + itemInfo.yahoo_finance_data.effective_ticker}>
{'$' + millify(itemInfo.amount)}
</OutboundLink>
</div>
}
</div>
);
const tickerElement = itemInfo.ticker && (
<div className="product-property row">
<div className="product-property-name col col-40">Ticker</div>
<div className="product-property-value tight-col col-60">
<OutboundLink to={"https://finance.yahoo.com/quote/" + itemInfo.yahoo_finance_data.effective_ticker}>
{itemInfo.yahoo_finance_data.effective_ticker}
</OutboundLink>
</div>
</div>
);
const latestCommitDateElement = itemInfo.latestCommitDate && (
<div className="product-property row">
<div className="product-property-name col col-50">Latest Commit</div>
<div className="product-property-value col col-50">
<OutboundLink to={itemInfo.latestCommitLink}>{formatDate(itemInfo.latestCommitDate)}</OutboundLink>
</div>
</div>
);
const releaseDateElement = itemInfo.releaseDate && (
<div className="product-property row">
<div className="product-property-name col col-50">Latest Release</div>
<div className="product-property-value col col-50">
<OutboundLink to={itemInfo.releaseLink}>{formatDate(itemInfo.releaseDate)}</OutboundLink>
</div>
</div>
);
const crunchbaseEmployeesElement = itemInfo.crunchbaseData && itemInfo.crunchbaseData.numEmployeesMin && (
<div className="product-property row">
<div className="product-property-name col col-50">Headcount</div>
<div className="product-property-value col col-50">{formatNumber(itemInfo.crunchbaseData.numEmployeesMin)}-{formatNumber(itemInfo.crunchbaseData.numEmployeesMax)}</div>
</div>
);
const extraElement = ( function() {
if (!itemInfo.extra) {
return null;
}
const items = Object.keys(itemInfo.extra).map( function(key) {
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 relativeDate(new Date(value));
}
if (typeof value === 'string' && (value.indexOf('http://') === 0 || value.indexOf('https://') === 0)) {
return <OutboundLink to={value}>{value}</OutboundLink>;
}
return value;
})();
return <div className="product-property row">
<div className="product-property-name tight-col col-20">{keyText}</div>
<div className="product-proerty-value tight-col col-80">{valueText}</div>
</div>;
});
return items;
})();
const scrollAllContent = innerWidth < 1000 || innerHeight < 630;
const cellStyle = {
width: 146,
marginRight: 4,
height: 26,
display: 'inline-block',
layout: 'relative',
overflow: 'hidden'
};
const productLogoAndTags = <Fragment>
<div className="product-logo" style={getRelationStyle(itemInfo.relation)}>
<img src={assetPath(itemInfo.href)} className='product-logo-img' alt={itemInfo.name}/>
</div>
<div className="product-tags">
<div className="product-badges" style = {{width: Math.min(300, innerWidth - 110)}} >
<div style={cellStyle}>{projectTag(itemInfo)}</div>
<div style={cellStyle}>{parentTag(itemInfo)}</div>
<div style={cellStyle}>{openSourceTag(itemInfo.oss)}</div>
<div style={cellStyle}>{licenseTag(itemInfo)}</div>
<div style={cellStyle}>{badgeTag(itemInfo)}</div>
<div style={cellStyle}><TweetButton/></div>
</div>
</div>
</Fragment>;
const charts = <Fragment>
{chart(itemInfo)}
{participation(itemInfo)}
</Fragment>
const productLogoAndTagsAndCharts = <Fragment>
<div className="product-logo" style={getRelationStyle(itemInfo.relation)}>
<img src={assetPath(itemInfo.href)} className='product-logo-img'/>
</div>
<div className="product-tags">
<div className="product-badges" style = {{width: 300}} >
<div style={cellStyle}>{projectTag(itemInfo)}</div>
<div style={cellStyle}>{parentTag(itemInfo)}</div>
<div style={cellStyle}>{openSourceTag(itemInfo.oss)}</div>
<div style={cellStyle}>{licenseTag(itemInfo)}</div>
<div style={cellStyle}>{badgeTag(itemInfo)}</div>
<div style={cellStyle}><TweetButton/></div>
{chart(itemInfo)}
{participation(itemInfo)}
</div>
</div>
</Fragment>;
const shortenUrl = (url) => url.replace(/http(s)?:\/\/(www\.)?/, "").replace(/\/$/, "");
const productInfo = <Fragment>
<div className="product-main">
{ (isGoogle() || onlyModal) ?
<React.Fragment>
<div className="product-name">{itemInfo.name}</div>
<div className="product-description">{itemInfo.description}</div>
<div className="product-parent"><InternalLink to={linkToOrganization}>{itemInfo.organization}</InternalLink></div>
<div className="product-category">{itemCategory(itemInfo.landscape)}</div>
</React.Fragment> :
<React.Fragment>
<div className="product-name">{itemInfo.name}</div>
<div className="product-parent"><InternalLink to={linkToOrganization}><span>{itemInfo.organization}</span>{memberTag(itemInfo)}</InternalLink></div>
<div className="product-category">{itemCategory(itemInfo.landscape)}</div>
<div className="product-description">{itemInfo.description}</div>
</React.Fragment>
}
</div>
{ !loading && <div className="product-properties">
<div className="product-property row">
<div className="product-property-name col col-20">Website</div>
<div className="product-property-value col col-80">
<OutboundLink to={itemInfo.homepage_url}>{shortenUrl(itemInfo.homepage_url)}</OutboundLink>
</div>
</div>
{ itemInfo.repos && itemInfo.repos.map(({ url, stars }, idx) => {
return <div className={`product-property row ${ idx < 3 || showAllRepos ? '' : 'hidden' }`} key={idx}>
<div className="product-property-name col col-20">
{ idx === 0 && (itemInfo.repos.length > 1 ? 'Repositories' : 'Repository') }
</div>
<div className="product-property-value product-repo col col-80">
<OutboundLink to={url}>{shortenUrl(url)}</OutboundLink>
{ idx === 0 && itemInfo.repos.length > 1 && <span className="primary-repo">(primary)</span> }
{ itemInfo.github_data && <span className="product-repo-stars">
<SvgIcon style={{ color: '#7b7b7b' }}>{iconGithub}</SvgIcon>
<StarIcon style={{ color: '#7b7b7b' }}/>{formatNumber(stars)}
</span> }
</div>
</div>
})}
{itemInfo.repos && (itemInfo.repos.length > 3 || (itemInfo.repos.length > 1 && itemInfo.github_data)) &&
<div className="product-property row">
<div className="product-property-name col col-20"></div>
<div className="product-property-value product-repo col col-80">
{itemInfo.repos && itemInfo.repos.length > 3 &&
<span>
<a href="#" onClick={() => setShowAllRepos(!showAllRepos)}>{ showAllRepos ? 'less...' : 'more...' }</a>
</span>
}
{ itemInfo.github_data && <>
<span className="product-repo-stars-label">
total:
</span>
<span className="product-repo-stars">
<SvgIcon style={{color: '#7b7b7b'}}>{iconGithub}</SvgIcon>
<StarIcon style={{color: '#7b7b7b'}} />
{formatNumber(itemInfo.github_data.stars)}
</span>
</>}
</div>
</div>
}
{itemInfo.crunchbase &&
<div className="product-property row">
<div className="product-property-name col col-20">Crunchbase</div>
<div className="product-property-value col col-80">
<OutboundLink to={itemInfo.crunchbase}>{shortenUrl(itemInfo.crunchbase)}</OutboundLink>
</div>
</div>
}
{itemInfo.crunchbaseData && itemInfo.crunchbaseData.linkedin &&
<div className="product-property row">
<div className="product-property-name col col-20">LinkedIn</div>
<div className="product-property-value col col-80">
<OutboundLink to={itemInfo.crunchbaseData.linkedin}>
{shortenUrl(itemInfo.crunchbaseData.linkedin)}
</OutboundLink>
</div>
</div>
}
<div className="row">
{ innerWidth <= 1000 && <div className="col col-50 single-column">
{ twitterElement }
{ latestTweetDateElement }
{ firstCommitDateElement }
{ latestCommitDateElement }
{ contributorsCountElement }
{ releaseDateElement }
{ headquartersElement }
{ crunchbaseEmployeesElement }
{ amountElement }
{ tickerElement }
</div> }
{ innerWidth > 1000 && <div className="col col-50">
{ twitterElement }
{ firstCommitDateElement }
{ contributorsCountElement }
{ headquartersElement }
{ amountElement }
{ tickerElement }
</div>
}
{ innerWidth > 1000 && <div className="col col-50">
{ latestTweetDateElement }
{ latestCommitDateElement }
{ releaseDateElement }
{ crunchbaseEmployeesElement }
</div>
}
</div>
{ extraElement }
</div> }
</Fragment>;
return (
<div className={classNames("modal-content", {'scroll-all-content': scrollAllContent})} >
<Head>
<title>{`${itemInfo.name} - ${settings.global.meta.title}`}</title>
</Head>
<KeyHandler keyEventName="keydown" keyValue="ArrowUp" onKeyHandle={handleUp} />
<KeyHandler keyEventName="keydown" keyValue="ArrowDown" onKeyHandle={handleDown} />
{ !scrollAllContent && !isGoogle() && productLogoAndTagsAndCharts }
<div className="product-scroll" ref={(x) => productScrollEl = x }>
{ !scrollAllContent && productInfo }
{ scrollAllContent && <div className="landscape-layout">
{productLogoAndTags}
<div className="right-column">{productInfo}</div>
{charts}
</div>
}
{ showTwitter && itemInfo.twitter && <TwitterTimeline twitter={itemInfo.twitter} />}
</div>
{ !scrollAllContent && isGoogle() && productLogoAndTags }
</div>
);
}
export default ItemDialogContent

View File

@ -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;
}

View File

@ -0,0 +1,45 @@
const _ = require('lodash');
// Render all items here!
const { renderHorizontalCategory } = require('./HorizontalCategory');
const { renderVerticalCategory } = require('./VerticalCategory');
const { renderLandscapeInfo } = require('./LandscapeInfo');
const { renderOtherLandscapeLink } = require('./OtherLandscapeLink');
const extractKeys = (obj, keys) => {
const attributes = _.pick(obj, keys)
return _.mapKeys(attributes, (value, key) => _.camelCase(key))
}
module.exports.render = function({landscapeSettings, landscapeItems}) {
const elements = landscapeSettings.elements.map(element => {
if (element.type === 'LandscapeLink') {
return renderOtherLandscapeLink(element)
}
if (element.type === 'LandscapeInfo') {
return renderLandscapeInfo(element)
}
const category = landscapeItems.find(c => c.key === element.category);
if (!category) {
console.info(`Can not find the ${element.category}`);
console.info(`Valid values: ${landscapeItems.map( (x) => x.key).join('; ')}`);
}
const attributes = extractKeys(element, ['width', 'height', 'top', 'left', 'color', 'fit_width', 'is_large'])
const subcategories = category.subcategories.map(subcategory => {
const allItems = subcategory.allItems.map(item => ({ ...item, categoryAttrs: attributes }))
return { ...subcategory, allItems }
})
if (element.type === 'HorizontalCategory') {
return renderHorizontalCategory({...category, ...attributes, subcategories: subcategories});
}
if (element.type === 'VerticalCategory') {
return renderVerticalCategory({...category, ...attributes, subcategories: subcategories});
}
}).join('');
return `<div style="position: relative;">${elements}</div>`;
};

View File

@ -0,0 +1,56 @@
const { h } = require('../utils/format');
const { assetPath } = require('../utils/assetPath');
module.exports.renderLandscapeInfo = function({width, height, top, left, children}) {
children = children.map(function(info) {
const positionStyle = `
position: absolute;
top: ${info.top}px;
left: ${info.left}px;
right: ${info.right}px;
bottom: ${info.bottom}px;
width: ${info.width}px;
height: ${info.height}px;
`;
if (info.type === 'text') {
return `<div key='text' style="
${positionStyle}
font-size: ${info.font_size * 4}px;
font-style: italic;
text-align: justify;
z-index: 1;
"><div style="
position: absolute;
left: 0;
top: 0;
width: 400%;
height: 100%;
transform: scale(0.25);
transform-origin: left;
"> ${h(info.text)} </div></div>`;
}
if (info.type === 'title') {
return `<div key='title' style="
${positionStyle}
font-size: ${info.font_size}px;
color: #666;
">${h(info.title)}</div>`;
}
if (info.type === 'image') {
return `<img src="${assetPath(`images/${info.image}`)}" style="${positionStyle}" alt="${info.title || info.image}" />`;
}
}).join('');
return `<div style="
position: absolute;
width: ${width}px;
height: ${height - 20}px;
top: ${top}px;
left: ${left}px;
border: 1px solid black;
background: white;
border-radius: 10px;
margin-top: 20px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
">${children}</div>`
}

View File

@ -1,16 +0,0 @@
import React from 'react'
import InternalLink from './InternalLink'
import assetPath from '../utils/assetPath'
import settings from 'public/settings.json';
const LandscapeLogo = () => {
const { name } = settings.global
return <span className="landscape-logo">
<InternalLink to="/">
<img src={assetPath("/images/left-logo.svg")} alt={name}/>
</InternalLink>
</span>
}
export default LandscapeLogo

View File

@ -1,14 +0,0 @@
import { useContext } from 'react'
import TreeSelector from './TreeSelector';
import { options } from '../types/fields';
import LandscapeContext from '../contexts/LandscapeContext'
const LicenseFilterContainer = () => {
const { navigate, params } = useContext(LandscapeContext)
const value = params.filters.license
const _options = options('license')
const onChange = license => navigate({ filters: { license }})
return <TreeSelector onChange={onChange} value={value} options={_options} />
}
export default LicenseFilterContainer

View File

@ -1,174 +0,0 @@
import React, { useContext, useEffect, useState, useRef } from 'react';
import StarIcon from '@material-ui/icons/Star';
import millify from 'millify'
import classNames from 'classnames'
import ListSubheader from '@material-ui/core/ListSubheader';
import _ from 'lodash';
import InternalLink from './InternalLink';
import fields from '../types/fields';
import LandscapeContext from '../contexts/LandscapeContext'
import assetPath from '../utils/assetPath'
import { useRouter } from 'next/router'
import { stringifyParams } from '../utils/routing'
import useCurrentDevice from '../utils/useCurrentDevice'
function getRelationStyle(relation) {
const relationInfo = fields.relation.valuesMap[relation]
if (relationInfo && relationInfo.color) {
return {
border: '4px solid ' + relationInfo.color
};
} else {
return {};
}
}
const Card = ({item, handler, ...props}) => {
const { params } = useContext(LandscapeContext)
const { cardStyle } = params
if (cardStyle === 'flat') {
return FlatCard({item, handler, ...props});
} else if (cardStyle === 'borderless') {
return BorderlessCard({item, handler, ...props});
} else {
return DefaultCard({item, handler, ...props});
}
}
const DefaultCard = ({item, handler, itemRef, ...props}) => {
return (
<div ref={itemRef} className="mosaic-wrap" key={item.id} {...props}>
<div className={classNames('mosaic', {nonoss : item.oss === false})} style={getRelationStyle(item.relation)}
onClick={() => handler(item.id)} >
<div className="logo_wrapper">
<img src={assetPath(item.href)} className='logo' max-height='100%' max-width='100%' alt={item.name} />
</div>
<div className="mosaic-info">
<div className="mosaic-title">
<h5>{item.name}</h5>
{item.organization}
</div>
<div className="mosaic-stars">
{ _.isNumber(item.stars) && item.stars &&
<div>
<StarIcon color="disabled" style={{ fontSize: 15 }}/>
<span style={{position: 'relative', top: -3}}>{item.starsAsText}</span>
</div>
}
{ Number.isInteger(item.amount) &&
<div className="mosaic-funding">{item.amountKind === 'funding' ? 'Funding: ': 'MCap: '} {'$'+ millify( item.amount )}</div>
}
</div>
</div>
</div>
</div>
);
}
const FlatCard = function({item, handler, itemRef, ...props}) {
return (
<div ref={itemRef} className="mosaic-wrap" key={item.id} {...props}>
<div className="mosaic" onClick={() => handler(item.id)} >
<img src={assetPath(item.href)} className='logo' alt={item.name} />
<div className="separator"/>
<h5>{item.flatName}</h5>
</div>
</div>
);
}
const BorderlessCard = function({item, handler, itemRef, ...props}) {
return (
<div className="mosaic-wrap" key={item.id} {...props}>
<div className="mosaic" onClick={() => handler(item.id)} >
<img src={assetPath(item.href)} className='logo' alt={item.name} />
</div>
</div>
);
}
const Header = ({groupedItem, ...props}) => {
return (
<div className="sh_wrapper" key={"subheader:" + groupedItem.header} {...props}>
<ListSubheader component="div" style={{fontSize: 24, paddingLeft: 16 }}>
{ groupedItem.href ? <InternalLink to={groupedItem.href}>{groupedItem.header}</InternalLink> : <span>{groupedItem.header}</span> }
<span className="items-count"> ({groupedItem.items.length})</span></ListSubheader>
</div>
);
}
/*
That is quite a complex component. It draws headers and cards, and also animates the difference
- previous list of items is remembered every time, lately referenced as 'old'
- we scroll to the top after every change, so we need to animate only those items which are at the start
- if a card was visible with previous parameters and is visible with current parameters, we apply a 'move' animation
- otherwise, old items fade out and new items fade in
- for performance, we draw first 30 items with animations, and delay rendering of the remaining parts to provide a quicker response to the user
- those 30 items are just an estimation, we calculate weather a card or a header are really visible in the current viewport or not
*/
const MainContent = () => {
const { navigate, params, groupedItems } = useContext(LandscapeContext)
const { cardStyle, isEmbed } = params
const loader = useRef(null)
const totalItems = groupedItems.reduce((sum, group) => sum + group.items.length, 0)
const [maxItems, setMaxItems] = useState(isEmbed ? totalItems : 100)
const { asPath } = useRouter()
const currentDevice = useCurrentDevice()
useEffect(() => {
const options = { root: null, rootMargin: '0px', threshold: 1.0 }
const callback = (entries, _) => {
entries.forEach(entry => {
if (entry.isIntersecting && maxItems < totalItems) {
setMaxItems(maxItems + 100)
}
})
}
const observer = new IntersectionObserver(callback, options);
observer.observe(loader.current)
return () => observer.disconnect()
});
useEffect(() => {
setMaxItems(isEmbed ? totalItems : 100)
}, [asPath])
const handler = selectedItemId => {
if (currentDevice.mobile() && isEmbed) {
const url = stringifyParams({ ...params, selectedItemId })
window.open(url,'_blank')
} else {
navigate({ selectedItemId }, { scroll: false })
}
}
let itemsCount = 0
const itemsAndHeaders = groupedItems.flatMap(groupedItem => {
const items = groupedItem.items.slice(0, maxItems - itemsCount)
itemsCount += items.length
const cards = items.map(item => <Card key={item.id} item={item} handler={handler}/>)
return [
items.length > 0 ? <Header key={groupedItem.href} groupedItem={groupedItem} /> : null,
...cards
]
});
return (
<div className={classNames('column-content', {[cardStyle + '-mode']: true})}>
{ itemsAndHeaders }
<div ref={loader} />
</div>
);
};
export default MainContent;

View File

@ -1,9 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
const Note = () => {
return <div className="sidebar-note">
Greyed logos are not open source
</div>
}
export default pure(Note);

View File

@ -0,0 +1,78 @@
const _ = require('lodash');
const { h } = require('../utils/format');
const l = function(x) {
return h((x || "").replace("https://", ""));
}
const { formatNumber } = require('../utils/formatNumber');
function highlightLinks(s) {
if (!s) {
return '';
}
// markdown styles
s = s.replace(/\[(.*?)\]\((https?:.*?)\)/g, '<a target="_blank" href="$2">$1</a>')
s = s.replace(/(\s|^)(https?:.*?)(\s|$)/g, ' <a target="_blank" href="$2">$2</a> ')
return s;
}
const getDate = function(date) {
if (!date) {
return '';
}
return new Date(date).toISOString().substring(0, 10);
}
const today = getDate(new Date());
module.exports.render = function({items}) {
console.info(items[0], items[0].latestCommitDate);
const old = _.orderBy( items.filter( (x) => x.latestCommitDate && new Date(x.latestCommitDate).getTime() + 3 * 30 * 86400 * 1000 < new Date().getTime()), 'latestCommitDate');
console.info(old.map( (x) => x.name));
return `
<head>
<link rel="icon" type="image/png" href="/favicon.png" />
<style>
</style>
</head>
<body>
<h1>List of obsolete items</h1>
${old.map(function(item) {
return `
<div style="display: flex; flex-direction: row;">
<div style="width: 200px; overflow: hidden; padding: 5px;">
<img src="logos/${item.image_data.fileName}"></img>
</div>
<div style="width: 300px; overflow: hidden; padding: 5px;">
<h3>${item.name}</h1>
<h3>${item.path}</h2>
<h3><span><b>Latest Commit: </b></span> ${getDate(item.latestCommitDate)}</h3>
<h3><span><b>Repo: </b></span> ${highlightLinks(item.repo_url)}</h3>
</div>
<div style="width: 600px; padding: 5px;">
<h4>Manual Actions</h4>
<pre>
1. Create an issue in their repo, inform that that a repo is scheduled for deletion.
2. Mark this item in a <b>landscape.yml</b> with an <b>extra</b> property <b>obsolete_since</b> equal to ${today}
3. After a month remove the entry from the repo
===
Issue Template:
Title: ${item.name} is going to be unreferenced from the interactive landscape because of no activity since ${getDate(item.latestCommitDate)}
Body:
<div style="font-size: 8px;">
Dear project maintainers of ${item.name},
I hope this message finds you well.
I noticed that your project has had no activity for the last 3 months and is about to be removed from the interactive landscape https://landscape.cncf.io/?selected=${item.id}
As a maintainer of an interactive landscape, we have included your project as a reference for our users and would like to ensure that the information we provide is up-to-date.
We understand that maintaining a project can be challenging and time-consuming, and we would like to offer any assistance that we can. Please let us know if there are any plans to continue the development of the project or if there is anything we can do to help.
Thank you for your time and efforts in creating and maintaining this project. We appreciate the value it has provided to the community and hope to continue to reference it in our interactive landscape.
Best regards, CNCF Landscape Team
</div>
</pre>
</div>
</div>
`;
}).join('<hr>')}
</body>
`
}

View File

@ -1,14 +0,0 @@
import ComboboxMultiSelector from './ComboboxMultiSelector';
import { options } from '../types/fields';
import { useContext } from 'react'
import LandscapeContext from '../contexts/LandscapeContext'
const OrganizationFilterContainer = () => {
const { navigate, params } = useContext(LandscapeContext)
const value = params.filters.organization
const _options = options('organization')
const onChange = organization => navigate({ filters: { organization } })
return <ComboboxMultiSelector onChange={onChange} value={value} options={_options} />
}
export default OrganizationFilterContainer

View File

@ -0,0 +1,97 @@
const { assetPath } = require('../utils/assetPath');
const { h } = require('../utils/format');
const { stringifyParams } = require('../utils/routing');
const { categoryBorder, categoryTitleHeight, subcategoryTitleHeight } = require('../utils/landscapeCalculations');
const renderCardLink = ({ url, children }) => {
if (url.indexOf('http') === 0) {
return `<a data-type=external target=_blank href="${url}" style="display: flex; flex-direction: column;">${children}</a>`;
} else {
url = stringifyParams({ mainContentMode: url });
return `<a data-type=tab href="${url}" style="display: flex; flex-direction: column;">${children}</a>`;
}
};
module.exports.renderOtherLandscapeLink = function({top, left, height, width, color, title, image, url, layout}) {
title = title || ''; //avoid undefined!
const imageSrc = image || assetPath(`images/${url}_preview.png`);
if (layout === 'category') {
return `<div style="
position: absolute;
top: ${top}px;
left: ${left}px;
height: ${height}px;
width: ${width}px;
background: ${color};
overflow: hidden;
cursor: pointer;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
padding: 1px;
display: flex;
">
${renderCardLink({url: url, children: `
<div style="width: ${width}px;height: 30px; line-height: 28px; text-align: center; color: white; font-size: 12px;">${h(title)}</div>
<div style="flex: 1; background: white; position: relative; display: flex; justify-content: center; align-items: center;">
<img loading="lazy" src="${imageSrc}" style="
width: ${width - 12}px;
height: ${height - 42}px;
object-fit: contain;
background-position: center;
background-repeat: no-repeat;" alt="${h(title)}" />
</div>`})}
</div>`;
}
if (layout === 'subcategory') {
return `<div style="
width: ${width}px;
left: ${left}px;
height: ${height}px;
top: ${top}px;
position: absolute;
overflow: hidden;">
${renderCardLink({url: url, children: `
<div
style="
position: absolute;
background: ${color};
top: ${subcategoryTitleHeight}px;
bottom: 0;
left: 0;
right: 0;
boxShadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
padding: ${categoryBorder}px;
display: flex;
"
>
<div style="
width: ${categoryTitleHeight}px;
writing-mode: vertical-rl;
transform: rotate(180deg);
text-align: center;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 13px;
color: white;
">
${h(title)}
</div>
<div style="
display: flex;
flex: 1;
background: white;
justify-content: center;
align-items: center; ">
<img loading="lazy" src="${imageSrc}" alt="${h(title)}"
style="width: ${width - 42}px;
height: ${height - 32}px;
object-fit: contain;
background-position: center;
background-repeat: no-repeat;" />
</div>
</div>`})}
</div>`
}
}

View File

@ -1,12 +0,0 @@
import React from 'react';
import { OutboundLink } from 'react-ga';
const OverrideOutboundLink = ({to, eventLabel, children, ...props}) => {
return (
<OutboundLink to={to} eventLabel={eventLabel || to} target="_blank" rel="noopener noreferrer" {...props}>
{children}
</OutboundLink>
)
}
export default OverrideOutboundLink

View File

@ -1,56 +0,0 @@
import React from 'react'
import { parse } from 'query-string'
import { useRouter } from 'next/router'
import { pure } from 'recompose'
import InternalLink from './InternalLink'
import convertLegacyUrl from '../utils/convertLegacyUrl'
import settings from 'public/settings.json'
const queriesMatch = (query, otherQuery) => {
const params = parse(query)
const otherParams = parse(otherQuery)
for (const key in params) {
if (params[key] !== otherParams[key]) {
return false
}
}
return true
}
const urlsMatch = (url, otherUrl) => {
const [path, query] = url.split('?')
const [otherPath, otherQuery] = otherUrl.split('?')
if (path !== otherPath) {
return false
}
return queriesMatch(query, otherQuery)
}
const presets = settings.presets.map(preset => {
const url = preset.url.indexOf('=') >= 0 ? convertLegacyUrl(preset.url) : preset.url
return { ...preset, url: url[0] === '/' ? url : `/${url}` }
})
const Preset = ({ preset }) => {
const router = useRouter()
const active = urlsMatch(preset.url, router.asPath)
return <div>
<InternalLink className={`preset ${active ? 'active' : null}`} to={preset.url}>
{preset.label}
</InternalLink>
</div>
}
const Presets = () => {
return <div className="sidebar-presets">
<h4>Example filters:</h4>
{presets.map(preset => <Preset key={preset.url} preset={preset} />)}
</div>
}
export default pure(Presets);

View File

@ -1,14 +0,0 @@
import TreeSelector from './TreeSelector';
import { options } from '../types/fields';
import { useContext } from 'react'
import LandscapeContext from '../contexts/LandscapeContext'
const ProjectFilterContainer = () => {
const { navigate, params } = useContext(LandscapeContext)
const value = params.filters.relation
const _options = options('relation')
const onChange = relation => navigate({ filters: { relation }})
return <TreeSelector onChange={onChange} options={_options} value={value} />
}
export default ProjectFilterContainer

View File

@ -1,24 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
const idToValue = (id) => id !== null ? id : 'any';
const valueToId = (value) => value === 'any' ? null : value;
const RadioSelector = ({value, options, onChange}) => {
return <RadioGroup name="stars"
value={idToValue(value)}
>
{ options.map( (entry) => (
<FormControlLabel
key={idToValue(entry.id)}
value={idToValue(entry.id)}
control={<Radio onClick={() => onChange(valueToId(entry.id))} />}
label={entry.label}
/>
)) }
</RadioGroup>
};
export default pure(RadioSelector);

View File

@ -1,24 +0,0 @@
import React, { useContext } from 'react';
import { pure } from 'recompose';
import ResetIcon from '@material-ui/icons/SettingsBackupRestore';
import LandscapeContext from '../contexts/LandscapeContext'
import { stringifyParams } from '../utils/routing'
import { useRouter } from 'next/router'
const ResetFilters = _ => {
const { params } = useContext(LandscapeContext)
const router = useRouter()
// TODO: clean up with validate
const reset = _ => {
const url = stringifyParams({ ...params, filters: null })
router.push(url)
}
return (
<a className="filters-action" onClick={reset} aria-label="Reset Filters">
<ResetIcon /><span>Reset Filters</span>
</a>
);
};
export default pure(ResetFilters);

View File

@ -1,17 +0,0 @@
import { useContext } from 'react'
import { sortOptions } from '../types/fields'
import SortFieldSelector from './SortFieldSelector'
import LandscapeContext from '../contexts/LandscapeContext'
const SortFieldContainer = () => {
const { navigate, params } = useContext(LandscapeContext)
const isBigPicture = params.mainContentMode !== 'card-mode'
const value = params.sortField
const options = sortOptions.filter(field => !field.disabled)
const onChange = sortField => navigate({ sortField })
return <SortFieldSelector isBigPicture={isBigPicture} value={value} onChange={onChange} options={options} />
}
export default SortFieldContainer

View File

@ -1,25 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import ComboboxSelector from './ComboboxSelector';
const SortFieldSelector = ({isBigPicture, value, options, onChange}) => {
if (!isBigPicture) {
return <ComboboxSelector value={value} options={options} onChange={onChange} />;
} else {
return (
<Select
disabled
value="empty"
style={{width:175 ,fontSize:'0.8em'}}
>
<MenuItem value="empty">
<em>N/A</em>
</MenuItem>
</Select>
);
}
};
export default pure(SortFieldSelector);

View File

@ -1,15 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import FormGroup from '@material-ui/core/FormGroup';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import SortFieldContainer from './SortFieldContainer';
const Sorting = () => {
return <FormGroup row>
<FormControl component="fieldset">
<FormLabel component="legend">Sort By</FormLabel>
<SortFieldContainer />
</FormControl>
</FormGroup>;
};
export default pure(Sorting);

View File

@ -1,30 +1,16 @@
import GuideLink from './GuideLink'
import css from 'styled-jsx/css'
import { smallItemHeight, smallItemWidth } from '../utils/landscapeCalculations'
const { renderGuideLink } = require('./GuideLink');
const { smallItemHeight, smallItemWidth } = require('../utils/landscapeCalculations');
const SubcategoryInfo = ({ label, anchor, row, column }) => {
const base = css.resolve`
module.exports.renderSubcategoryInfo = function({ label, anchor, row, column }) {
const style=`
width: ${smallItemWidth}px;
height: ${smallItemHeight}px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
`
const extra = css.resolve`
grid-column-start: ${column || 'auto'};
grid-row-start: ${row || 'auto'};
`
const className = `${base.className} ${extra.className}`
return <>
{base.styles}
{extra.styles}
<GuideLink className={className} label={label} anchor={anchor}/>
</>
`;
return renderGuideLink({label: label, anchor: anchor, style: style})
}
export default SubcategoryInfo

View File

@ -1,35 +0,0 @@
import React, { useContext } from 'react';
import { pure } from 'recompose';
import millify from 'millify';
import formatNumber from '../utils/formatNumber';
import _ from 'lodash';
import LandscapeContext from '../contexts/LandscapeContext'
import getSummary from '../utils/summaryCalculator'
const getText = ({summary}) => {
if (!summary.total) {
return 'There are no cards matching your filters';
}
const cardsText = summary.total === 1 ? 'card' : 'cards';
const startText = `You are viewing ${formatNumber(summary.total)} ${cardsText} with a total`;
const starsSection = summary.stars ? `of ${formatNumber(summary.stars)} stars` : null;
const marketCapSection = summary.marketCap ? `market cap of $${millify(summary.marketCap)}` : null;
const fundingSection = summary.funding ? `funding of $${millify(summary.funding)}` : null;
if (!marketCapSection && !fundingSection && !starsSection) {
return `You are viewing ${formatNumber(summary.total)} ${cardsText}.`;
}
const parts = [starsSection, marketCapSection, fundingSection].filter( (x) => !!x);
const startPartsText = _.slice(parts, 0, -1).join(', ');
const lastPart = _.slice(parts, -1)[0];
const text = [startPartsText, lastPart].filter( (x) => !!x).join(' and ');
return `${startText} ${text}.`;
}
const Summary = _ => {
const { entries, params } = useContext(LandscapeContext)
const summary = getSummary(params, entries)
return <h4 className="summary">{getText({summary})}</h4>;
}
export default pure(Summary);

View File

@ -0,0 +1,624 @@
const _ = require('lodash');
const { h } = require('../utils/format');
const l = function(x) {
return h((x || "").replace("https://", ""));
}
const { formatNumber } = require('../utils/formatNumber');
const getLanguages = function(item) {
if (item.extra && item.extra.summary_languages) {
return item.extra.summary_languages;
}
if (item.github_data && item.github_data.languages) {
const total = _.sum(item.github_data.languages.map( (x) => x.value));
const matching = item.github_data.languages.filter( (x) => x.value > total * 0.3).map( (x) => x.name);
return matching.join(', ');
} else {
return '';
}
}
function highlightLinks(s) {
if (!s) {
return '';
}
// markdown styles
s = s.replace(/\[(.*?)\]\((https?:.*?)\)/g, '<a target="_blank" href="$2">$1</a>')
s = s.replace(/(\s|^)(https?:.*?)(\s|$)/g, ' <a target="_blank" href="$2">$2</a> ')
return s;
}
const getDate = function(date) {
if (!date) {
return '';
}
return new Date(date).toISOString().substring(0, 10);
}
module.exports.render = function({items}) {
const projects = items.filter( (x) => !!x.relation && x.relation !== 'member');
const categories = _.uniq(projects.map( (x) => x.path.split(' / ')[0]));
const categoriesCount = {};
const categoryItems = {};
const subcategories = {};
for (let k of categories) {
categoriesCount[k] = projects.filter( (x) => x.path.split(' / ')[0] === k).length;
categoryItems[k] = projects.filter( (x) => x.path.split(' / ')[0] === k).map( (x) => projects.indexOf(x));
const arr = _.uniq(projects.filter( (x) => x.path.split(' / ')[0] === k).map( (x) => x.path.split(' / ')[1]));
for (let subcategory of arr) {
categoryItems[k + ':' + subcategory] = projects.filter( (x) => x.path === k + ' / ' + subcategory).map( (x) => projects.indexOf(x));
}
subcategories[k] = arr;
}
const columnWidth = 250;
return `
<head>
<link rel="icon" type="image/png" href="/favicon.png" />
<style>
${require('fs').readFileSync('src/fonts.css', 'utf-8')}
::root {
--navy: #38404a;
--navy-light: #696D70;
--blue: #2E67BF;
--blue-hover: #1D456B;
--spacing: 1em;
}
body {
color: rgba(0, 0, 0, 0.87);
margin: 0;
font-size: 0.875rem;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
font-weight: 400;
line-height: 1.43;
letter-spacing: 0.01071em;
background-color: #fafafa;
}
td a {
text-decoration: none;
color: #2E67BF;
}
.category {
font-size: 24px;
font-weight: bold;
}
.categories {
display: inline-block;
margin: 5px;
font-size: 14px;
}
.subcategories {
display: inline-block;
margin: 5px;
font-size: 14px;
}
table {
width: ${columnWidth * projects.length}px;
table-layout: fixed;
border-collapse: separate;
border-spacing: 0;
border-top: 1px solid white;
}
#headers {
width: 152px;
position: sticky;
left: 0;
}
#data {
position: absolute;
left: 155px;
top: 0px;
z-index: -1;
}
td,
th {
white-space: pre-wrap;
width: ${columnWidth}px;
margin: 0;
border: 1px solid white;
border-top-width: 0px;
height: 53px;
padding: 5px;
overflow: hidden;
font-size: 0.8em;
color: var(--navy);
}
.project-name {
text-align: center;
font-size: 15px;
font-weight: bold;
}
html, body {
height: 100%;
overflow: hidden;
padding: 0;
margin: 0;
outline: 0;
}
.table-wrapper {
position: absolute;
top: 165px;
width: calc(100% - 16px);
bottom: 0;
overflow-x: scroll;
overflow-y: scroll;
padding: 0;
left: 16px;
}
.sticky {
position: relative;
background-color: #1b446c;
color: white;
width: 152px;
top: auto;
font-size: 0.8em;
font-weight: bold;
padding: 0px;
}
.first-line {
background-color: #1b446c;
color: white;
}
.alternate-line {
background-color: #e5e5e5;
}
.sticky span {
white-space: nowrap;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
h1 {
font-size: 2.1rem;
line-height: 30px;
display: block;
margin: 0 0 14px;
color: var(--navy);
}
.landscape-logo {
width: 160px;
height: 48px;
display: inline-block;
}
.landscapeapp-logo {
position: absolute;
right: 5px;
top: 14px;
}
.landscapeapp-logo img {
width: 200px;
}
.main-header {
padding: 16px;
}
/* select starting stylings ------------------------------*/
.select {
font-family: 'Roboto';
position: relative;
width: 240px;
margin-bottom: 10px;
}
.select-disabled {
opacity: 0.35;
pointer-events: none;
}
.select-text {
position: relative;
font-family: inherit;
background-color: transparent;
width: 100%;
font-size: 11px;
padding: 10px 22px 10px 0;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(0,0,0, 0.12);
}
/* Remove focus */
.select-text:focus {
outline: none;
border-bottom: 1px solid rgba(0,0,0, 0);
}
/* Use custom arrow */
.select .select-text {
appearance: none;
-webkit-appearance:none
}
.select:after {
position: absolute;
top: 18px;
right: 10px;
/* Styling the down arrow */
width: 0;
height: 0;
padding: 0;
content: '';
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid rgba(0, 0, 0, 0.12);
pointer-events: none;
}
/* LABEL ======================================= */
.select-label {
color: rgb(105, 109, 112);
font-size: 10px;
font-weight: normal;
position: absolute;
pointer-events: none;
left: 0;
top: 10px;
transition: 0.2s ease all;
}
/* active state */
.select-text:focus ~ .select-label, .select-text ~ .select-label {
top: -10px;
transition: 0.2s ease all;
font-size: 11px;
}
/* BOTTOM BARS ================================= */
.select-bar {
position: relative;
display: block;
width: 100%;
}
.select-bar:before, .select-bar:after {
content: '';
height: 2px;
width: 0;
bottom: 1px;
position: absolute;
background: #2F80ED;
transition: 0.2s ease all;
}
.select-bar:before {
left: 50%;
}
.select-bar:after {
right: 50%;
}
/* active state */
.select-text:focus ~ .select-bar:before, .select-text:focus ~ .select-bar:after {
width: 50%;
}
.select-highlight {
position: absolute;
height: 60%;
width: 100px;
top: 25%;
left: 0;
pointer-events: none;
opacity: 0.5;
}
.info {
position: absolute;
top: 0px;
left: 540px;
font-size: 12px;
line-height: 1.2;
max-width: 500px;
}
</style>
</head>
<body>
<div class="main-header">
<span class="landscape-logo">
<a aria-label="reset filters" class="nav-link" href="/">
<img alt="landscape logo" src="/images/left-logo.svg">
</a>
</span>
<span style="display: inline-block; position: relative; top: -8px; left: 20px;">
<h1>CNCF Project Summary Table</h1>
</span>
<a rel="noopener noreferrer noopener noreferrer" class="landscapeapp-logo" title="CNCF" target="_blank" href="https://www.cncf.io">
<img src="/images/right-logo.svg" title="CNCF">
</a>
</div>
<div style="padding: 16px; position: relative; top: -19px;">
<div class="categories">
<div class="select">
<select class="select-text" required="">
<option value="" selected="">All: ${projects.length}</option>
${categories.map( (name) => `<option value="${name}">${name}: ${categoriesCount[name]}</option>`).join('')}
</select>
<span class="select-highlight"></span>
<span class="select-bar"></span>
<label class="select-label">Category</label>
</div>
</div>
<div class="subcategories" style="display: none">
<div class="select">
<select class="select-text" required="">
</select>
<span class="select-highlight"></span>
<span class="select-bar"></span>
<label class="select-label">Subcategory</label>
</div>
</div>
<div class="info">
The <i>CNCF Project Summary Table</i> provides a standardized, summary of CNCF projects.<br/><div style="height: 5px;"></div>
<b style="color: rgb(58,132,247);">The filters on the left side help refine your view.</b> Start by filtering by category (e.g., <i>orchestration and management</i>) and then subcategory (e.g., <i>service mesh</i> for an overview of all available CNCF service meshes).
</div>
</div>
<div class="table-wrapper">
<table id="headers">
<tr class="landscape first-line">
<td class="sticky">
<span> Project </span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Description</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Maturity</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Target Users</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Tags</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Use Case</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Business Use</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Languages</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>First Commit</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Last Commit</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Release Cadence</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Github Stars</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Integrations</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Website</span>
</td>
</tr>
<tr class="landscape alternate-line">
<td class="sticky">
<span>Github</span>
</td>
</tr>
<tr class="landscape">
<td class="sticky">
<span>Overview Video</span>
</td>
</tr>
</table>
<table id="data">
<tr class="landscape first-line">
${projects.map( (project, index) => `
<td class="project-name" data-project-index="${index}">${h(project.name)}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h((project.github_data || project)['description'])}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h(project.relation)}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_personas']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_tags'] || '').split(',').map( (tag) => `<div>- ${tag.trim()}</div>`).join('') }</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_use_case']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_business_use_case']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h(getLanguages(project))}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h(getDate((project.github_start_commit_data || {}).start_date))}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h(getDate((project.github_data || {}).latest_commit_date))}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${h((project.extra || {})['summary_release_rate']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td>${h(formatNumber((project.github_data || {}).stars))}</td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => `
<td>${highlightLinks((project.extra || {})['summary_integrations']) || '&nbsp;'}</td>
`).join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => `
<td><a href="${h((project.homepage_url))}" target="_blank">${l(project.homepage_url)}</a></td>
`).join('')}
</tr>
<tr class="landscape alternate-line">
${projects.map( (project) => project.repo_url ? `
<td><a href="${h(project.repo_url)}" target="_blank">${l(project.repo_url)}</a></td>
`: '<td>&nbsp;</td>').join('')}
</tr>
<tr class="landscape">
${projects.map( (project) => project.extra && project.extra.summary_intro_url ? `
<td><a href="${h(project.extra.summary_intro_url)}" target="_blank">${l(project.extra.summary_intro_url)}</a></td>
`: '<td>&nbsp;</td>').join('')}
</tr>
</table>
<div style="height: 20px;"></div>
</div>
<script>
function setHeight() {
const rows = [...document.querySelectorAll('#data tr')];
const headersRows = [...document.querySelectorAll('#headers tr')];
for (let row of rows) {
const index = rows.indexOf(row);
const headerEl = headersRows[index].querySelector('td');
const firstEl = [...row.querySelectorAll('td')].filter((x) => x.style.display !== 'none')[0];
headerEl.style.height = (firstEl.getBoundingClientRect().height) + 'px';
}
}
window.App = {
totalCount: ${projects.length},
categories: ${JSON.stringify(categories)},
categoryItems: ${JSON.stringify(categoryItems)},
subcategories: ${JSON.stringify(subcategories)}
};
document.querySelector('.categories select').addEventListener('change', function(e) {
const selectedOption = Array.from(document.querySelectorAll('.categories option')).find( (x) => x.selected);
const categoryId = selectedOption.value;
if (!categoryId) {
document.querySelector('#data').style.width = '';
document.querySelector('.subcategories').style.display = 'none';
} else {
document.querySelector('.subcategories').style.display = '';
const newWidth = ${columnWidth} * App.categoryItems[categoryId].length;
document.querySelector('#data').style.width = newWidth + 'px';
const subcategories = window.App.subcategories[categoryId];
const baseMarkup = '<option value="">All</option>';
const markup = subcategories.map( (s) => '<option value="' + s + '">' + s + ':&nbsp;' + window.App.categoryItems[categoryId + ':' + s].length + '</option>').join('');
document.querySelector('.subcategories select').innerHTML = baseMarkup + markup;
}
for (let tr of [...document.querySelectorAll('tr')]) {
let index = 0;
for (let td of [...tr.querySelectorAll('td')].slice(1)) {
const isVisible = categoryId ? App.categoryItems[categoryId].includes(index) : true;
td.style.display = isVisible ? '' : 'none';
index += 1;
}
}
setHeight();
});
document.querySelector('.subcategories select').addEventListener('change', function(e) {
const categoryId = Array.from(document.querySelectorAll('.categories option')).find( (x) => x.selected).value;
const subcategoryId = Array.from(document.querySelectorAll('.subcategories option')).find( (x) => x.selected).value;
let key = subcategoryId ? (categoryId + ':' + subcategoryId) : categoryId;
const newWidth = ${columnWidth} * App.categoryItems[key].length;
document.querySelector('#data').style.width = newWidth + 'px';
for (let tr of [...document.querySelectorAll('tr')]) {
let index = 0;
for (let td of [...tr.querySelectorAll('td')].slice(1)) {
const isVisible = App.categoryItems[key].includes(index);
td.style.display = isVisible ? '' : 'none';
index += 1;
}
}
setHeight();
});
setHeight();
</script>
</body>
`
}

View File

@ -1,109 +0,0 @@
import React from 'react';
import { pure } from 'recompose';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import Checkbox from '@material-ui/core/Checkbox';
import ListItemText from '@material-ui/core/ListItemText';
import _ from 'lodash';
const TreeSelector = ({value, options, onChange}) => {
const renderValue = function(selected) {
if (selected.length === 0) {
return 'Any';
}
const properOptions = selected.filter(function(selectedId) {
const option = _.find(options, {id: selectedId});
if (option.level === 1) {
return _.every(option.children, function(childId) {
return selected.indexOf(childId) !== -1;
});
}
if (option.level === 2) {
const parentOption = _.find(options, {id: option.parentId});
const isEverythingSelected = _.every(parentOption.children, function(childId) {
return selected.indexOf(childId) !== -1;
});
return !isEverythingSelected;
}
});
return properOptions.map( (x) => _.find(options, {id: x}).label).join(', ');
}
const onItemChanged = function(newSelection) {
// we have new list of checked items(newSelection and previous list of
// checked items(value), we want to get a single item which was checked /
// unchecked
var itemId = _.difference(newSelection, value).concat(_.difference(value, newSelection))[0];
var newValue;
const option = _.find(options, {id: itemId});
const withoutCategory = function() {
return value.filter(function(v) {
return v !== option.id && option.children.indexOf(v) === -1;
});
}
const checkedCategory = function() {
return options.filter(function(o) {
return o.id === option.id || option.children.indexOf(o.id) !== -1;
}).map(function(o) { return o.id});
}
if (option.level === 1) {
if (value.indexOf(option.id) !== -1) {
//uncheck category
newValue = withoutCategory();
} else {
//check category
newValue = withoutCategory().concat(checkedCategory());
}
} else if (option.level === 2) {
if (value.indexOf(option.id) !== -1) {
//uncheck subcategory, may be whole category
newValue = value.filter(function(x) {
return x !== option.id;
});
let parentCategory = _.find(options, {id: option.parentId});
if (_.every(parentCategory.children, function(childId) {
return newValue.indexOf(childId) === -1;
})) {
newValue = newValue.filter(function(x) {
return x !== parentCategory.id;
});
}
} else {
//check subcategory, may be whole category
newValue = value.concat([option.id]);
let parentCategory = _.find(options, {id: option.parentId});
if (_.every(parentCategory.children, function(childId) {
return newValue.indexOf(childId) !== -1;
})) {
newValue = newValue.concat([parentCategory.id]);
}
}
}
newValue = options.filter(function(o) {
return newValue.indexOf(o.id) !== -1;
}).map(function(o) {
return o.id;
});
onChange(newValue);
};
return <Select
multiple
style={{width:175 ,fontSize:'0.8em'}}
value={value}
renderValue={renderValue }
onChange={(e) => onItemChanged(e.target.value)}
displayEmpty
>
{ options.map( (el) => (
<MenuItem key={el.id}
value={el.id}
style={{height:5}}>
<span style={{width: (el.level - 1) * 20 }}/>
<Checkbox color="primary" disableRipple checked={value.indexOf(el.id) !== -1} />
<ListItemText disableTypography primary={el.label} style={{fontSize:'0.8em', padding:0}} />
</MenuItem>
)) }
</Select>
};
export default pure(TreeSelector);

View File

@ -1,46 +0,0 @@
// locate zoom buttons
import React, { useEffect, useRef, useState } from 'react'
import qs from 'query-string';
import settings from 'public/settings.json'
const bird = ( <svg
viewBox="0 0 300 244">
<g transform="translate(-539.17946,-568.85777)" >
<path fillOpacity="1" fillRule="nonzero"
d="m 633.89823,812.04479 c 112.46038,0 173.95627,-93.16765 173.95627,-173.95625 0,-2.64628 -0.0539,-5.28062 -0.1726,-7.90305 11.93799,-8.63016 22.31446,-19.39999 30.49762,-31.65984 -10.95459,4.86937 -22.74358,8.14741 -35.11071,9.62551 12.62341,-7.56929 22.31446,-19.54304 26.88583,-33.81739 -11.81284,7.00307 -24.89517,12.09297 -38.82383,14.84055 -11.15723,-11.88436 -27.04079,-19.31655 -44.62892,-19.31655 -33.76374,0 -61.14426,27.38052 -61.14426,61.13233 0,4.79784 0.5364,9.46458 1.58538,13.94057 -50.81546,-2.55686 -95.87353,-26.88582 -126.02546,-63.87991 -5.25082,9.03545 -8.27852,19.53111 -8.27852,30.73006 0,21.21186 10.79366,39.93837 27.20766,50.89296 -10.03077,-0.30992 -19.45363,-3.06348 -27.69044,-7.64676 -0.009,0.25652 -0.009,0.50661 -0.009,0.78077 0,29.60957 21.07478,54.3319 49.0513,59.93435 -5.13757,1.40062 -10.54335,2.15158 -16.12196,2.15158 -3.93364,0 -7.76596,-0.38716 -11.49099,-1.1026 7.78383,24.2932 30.35457,41.97073 57.11525,42.46543 -20.92578,16.40207 -47.28712,26.17062 -75.93712,26.17062 -4.92898,0 -9.79834,-0.28036 -14.58427,-0.84634 27.05868,17.34379 59.18936,27.46396 93.72193,27.46396" />
</g>
</svg>);
const TweetButton = function({cls}) {
const [ready, setReady] = useState(false)
const [isHidden, setIsHidden] = useState(false)
const tweetRef = useRef()
useEffect(() => {
setReady(true)
}, [])
useEffect(() => {
const tweetButtonStyle = tweetRef.current && window.getComputedStyle(tweetRef.current)
setIsHidden(tweetRef.current && (tweetButtonStyle.display === 'none' || tweetButtonStyle.visibility === 'hidden'))
}, [ready])
if (!ready) {
return null;
}
const { origin, pathname } = window.location
const url = `${origin}${pathname}`
const { text } = settings.twitter
const params = qs.stringify({ text, url })
const twitterUrl = `https://twitter.com/intent/tweet?${params}`
return <div className={`tweet-button ${cls}`} style={{ ...(isHidden ? { display: 'none' } : null)}}>
<a ref={tweetRef} href={twitterUrl}>{bird}<span>Tweet</span></a>
<div className="tweet-count-wrapper">
<div className="tweet-count">{process.env.tweets}</div>
</div>
</div>
}
export default TweetButton

View File

@ -1,43 +0,0 @@
import { useRef, useEffect } from 'react'
import { Timeline } from 'react-twitter-widgets'
import useCurrentDevice from '../utils/useCurrentDevice'
const TwitterTimeline = ({ twitter }) => {
const timelineRef = useRef(null)
const name = twitter.split('/').pop()
const currentDevice = useCurrentDevice()
// This is a hack to fix overflow issues on Safari iPhone
// see https://github.com/cncf/landscapeapp/issues/331
useEffect(() => {
if (currentDevice.ios() && navigator.vendor.match(/^apple/i)) {
timelineRef.addEventListener("DOMSubtreeModified", (el) => {
if (el.target.tagName === "IFRAME") {
const head = el.target.contentDocument.head;
const newStyle = el.target.contentDocument.createElement("style");
const css = [
".TweetAuthor { max-width: 300px; text-overflow: ellipsis; }",
".timeline-Tweet-text a:not(.customisable) { word-break: break-all; }"
];
newStyle.innerHTML = css.join(" ");
head.appendChild(newStyle);
}
})
}
}, [])
return <div ref={timelineRef}>
<Timeline
dataSource={{
sourceType: 'profile',
screenName: name
}}
options={{
username: name,
tweetLimit: 3
}}
/>
</div>
}
export default TwitterTimeline

View File

@ -0,0 +1,66 @@
const { renderItem } = require("./Item");
const { h } = require('../utils/format');
const {
calculateVerticalCategory,
categoryTitleHeight,
itemMargin, smallItemWidth,
subcategoryMargin
} = require("../utils/landscapeCalculations");
const { renderSubcategoryInfo } = require( './SubcategoryInfo');
const { renderCategoryHeader } = require('./CategoryHeader');
module.exports.renderVerticalCategory = function({header, guideInfo, subcategories, top, left, width, height, color, href, fitWidth}) {
const subcategoriesWithCalculations = calculateVerticalCategory({ subcategories, fitWidth, width });
return `<div>
<div style="
position: absolute;
top: ${top}px;
left: ${left}px;
height: ${height}px;
width: ${width}px;
background: ${color};
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
padding: 0px;
display: flex;
flex-direction: column;
" class="big-picture-section">
<div style="height: ${categoryTitleHeight}px; width: 100%; display: flex;">
${renderCategoryHeader({href: href, label: header, guideAnchor: guideInfo, background: color})}
</div>
<div style="
width: 100%;
position: relative;
flex: 1;
padding: ${subcategoryMargin}px 0;
display: flex;
flex-direction: column;
justify-content: space-between;
background: white
">
${subcategoriesWithCalculations.map(subcategory => {
const { guideInfo, width, columns, name } = subcategory;
const style = `
display: grid;
grid-template-columns: repeat(${columns}, ${smallItemWidth}px);
`;
const extraStyle = fitWidth ? `justify-content: space-evenly; flex: 1;` : `grid-gap: ${itemMargin}px; `
return `<div style="
position: relative;
flex-grow: ${subcategory.rows};
display: flex;
flex-direction: column;">
<div style="line-height: 15px; text-align: center;">
<a data-type=internal href="${subcategory.href}">${h(name)}</a>
</div>
<div style="width: ${width}px; overflow: hidden; margin: 0 auto; ${style} ${extraStyle}">
${subcategory.allItems.map(renderItem).join('')}
${guideInfo ? renderSubcategoryInfo({label: name, anchor: guideInfo, column: columns}) : ''}
</div>
</div>`
}).join('')}
</div>
</div>
</div>`;
}

View File

@ -1,46 +0,0 @@
import { createContext } from 'react'
import { useRouter } from 'next/router'
import { parseParams } from '../utils/routing'
import { findLandscapeSettings } from '../utils/landscapeSettings'
import { getGroupedItemsForContentMode } from '../utils/itemsCalculator'
import selectedItemCalculator from '../utils/selectedItemCalculator'
import { calculateSize } from '../utils/landscapeCalculations'
import { stringifyParams } from '../utils/routing'
const LandscapeContext = createContext()
export const LandscapeProvider = ({ entries, pageParams, guideIndex = {}, children }) => {
const router = useRouter()
const params = parseParams({ ...pageParams, ...router.query })
const landscapeSettings = findLandscapeSettings(params.mainContentMode)
const isBigPicture = params.mainContentMode !== 'card-mode'
const groupedItems = getGroupedItemsForContentMode(params, entries, landscapeSettings)
const selectedItemId = params.selectedItemId
const { nextItemId, previousItemId } = selectedItemCalculator(groupedItems, selectedItemId, isBigPicture)
const size = calculateSize(landscapeSettings)
const navigate = (newParams = {}, options = {}) => {
const filters = { ...(params.filters || {}), ...(newParams.filters || {}) }
const url = stringifyParams({ ...params, ...newParams, filters })
router.push(url, null, options)
}
const baseProps = {
entries,
navigate,
groupedItems,
nextItemId,
previousItemId,
params,
landscapeSettings,
guideIndex,
...size
}
return <LandscapeContext.Provider value={baseProps}>
{children}
</LandscapeContext.Provider>
}
export default LandscapeContext

205
src/embedded-script.js Normal file
View File

@ -0,0 +1,205 @@
// An embedded version of a script
const CncfLandscapeApp = {
init: function() {
setInterval(function() {
window.parent.postMessage({
type: 'landscapeapp-resize',
height: document.body.scrollHeight
}, '*');
}, 1000);
this.state = {
selected: null
}
if (window.parentIFrame) {
document.addEventListener('keydown', (e) => {
if (e.keyCode === 27) {
if (CncfLandscapeApp.state.selected) {
this.state.selected = null;
this.hideSelectedItem();
}
}
});
}
document.body.addEventListener('click', (e) => {
const cardEl = e.target.closest('[data-id]');
if (cardEl) {
const selectedItemId = cardEl.getAttribute('data-id');
CncfLandscapeApp.state.selected = selectedItemId;
this.showSelectedItem(selectedItemId);
e.preventDefault();
e.stopPropagation();
}
const modalBodyEl = e.target.closest('.modal-body');
const shadowEl = e.target.closest('.modal-container');
if (shadowEl && !modalBodyEl) {
this.state.selected = null;
this.hideSelectedItem();
e.preventDefault();
e.stopPropagation();
}
const modalClose = e.target.closest('.modal-close');
if (modalClose) {
this.state.selected = null;
this.hideSelectedItem();
e.preventDefault();
e.stopPropagation();
}
const nextItem = e.target.closest('.modal-next');
if (nextItem && CncfLandscapeApp.nextItemId) {
CncfLandscapeApp.state.selected = CncfLandscapeApp.nextItemId;
this.showSelectedItem(CncfLandscapeApp.state.selected);
e.preventDefault();
e.stopPropagation();
}
const prevItem = e.target.closest('.modal-prev');
if (prevItem && CncfLandscapeApp.prevItemId) {
CncfLandscapeApp.state.selected = CncfLandscapeApp.prevItemId;
this.showSelectedItem(CncfLandscapeApp.state.selected);
e.preventDefault();
e.stopPropagation();
}
const selectedItemInternalLinkEl = e.target.closest('.modal-body a[data-type=internal]')
if (selectedItemInternalLinkEl) {
e.preventDefault();
e.stopPropagation();
return
}
}, false);
// support custom css styles and custom js eval code through iframe
window.addEventListener('message', (event) => {
var data = event.data;
if (data.type === "css") {
var styles = data.css;
var el = document.createElement('style');
el.type = 'text/css';
if (el.styleSheet) {
el.styleSheet.cssText = styles;
} else {
el.appendChild(document.createTextNode(styles));
}
document.getElementsByTagName("head")[0].appendChild(el);
}
if (data.type === "js") {
eval(data.js);
}
});
// support css styles via param
const params = new URLSearchParams(window.location.search.substring(1));
if (params.get('css')) {
const element = document.createElement("link");
element.setAttribute("rel", "stylesheet");
element.setAttribute("type", "text/css");
element.setAttribute("href", params.get('css'));
document.getElementsByTagName("head")[0].appendChild(element);
}
if (params.get('style')) {
const element = document.createElement("style");
let style = params.get('style');
try {
style = JSON.parse(style)
} catch(ex) {
}
element.innerHTML = style;
document.getElementsByTagName("head")[0].appendChild(element);
}
},
showSelectedItem: async function(selectedItemId) {
if (!window.parentIFrame) {
window.parent.postMessage({
type: 'landscapeapp-show',
selected: selectedItemId,
location: {
search: window.location.search,
pathname: window.location.pathname
}
}, '*');
return;
}
this.selectedItems = this.selectedItems || {};
if (!this.selectedItems[selectedItemId]) {
const result = await fetch(`${this.basePath}/data/items/info-${selectedItemId}.html`);
const text = await result.text();
this.selectedItems[selectedItemId] = text;
}
document.querySelector('.modal').style.display="";
document.querySelector('.modal .modal-content').outerHTML = this.selectedItems[selectedItemId];
document.querySelector('body').style.overflow = 'hidden';
if (window.twttr) {
window.twttr.widgets.load();
} else {
setTimeout( () => window.twttr && window.twttr.widgets.load(), 1000);
}
//calculate previous and next items;
const selectedItemEl = document.querySelector(`[data-id=${selectedItemId}]`);
const parent = selectedItemEl.closest('.cards-section');
const allItems = parent.querySelectorAll('[data-id]');
const index = [].indexOf.call(allItems, selectedItemEl);
const prevItem = index > 0 ? allItems[index - 1].getAttribute('data-id') : null;
const nextItem = index < allItems.length - 1 ? allItems[index + 1].getAttribute('data-id') : null;
this.nextItemId = nextItem;
this.prevItemId = prevItem;
if (nextItem) {
document.querySelector('.modal-next').removeAttribute('disabled');
} else {
document.querySelector('.modal-next').setAttribute('disabled', '');
}
if (prevItem) {
document.querySelector('.modal-prev').removeAttribute('disabled');
} else {
document.querySelector('.modal-prev').setAttribute('disabled', '');
}
if (window.parentIFrame) {
window.parentIFrame.sendMessage({type: 'showModal'});
window.parentIFrame.getPageInfo(function(info) {
var offset = info.scrollTop - info.offsetTop;
var maxHeight = info.clientHeight * 0.9;
if (maxHeight > 640) {
maxHeight = 640;
}
var defaultTop = (info.windowHeight - maxHeight) / 2;
var top = defaultTop + 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.marginTop = 0;
modal.style.marginBottom = 0;
modal.style.bottom = '';
modal.style.maxHeight = maxHeight + 'px';
}
}, 10);
});
}
},
hideSelectedItem: function() {
document.querySelector('.modal').style.display="none";
document.querySelector('body').style.overflow = '';
if (window.parentIFrame) {
window.parentIFrame.sendMessage({type: 'hideModal'})
}
}
}
document.addEventListener('DOMContentLoaded', () => CncfLandscapeApp.init());

18
src/fonts.css Normal file

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More