From b2d7c5b22c8b5f3b68f87de2eb5ffe618f3f756e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cintia=20S=C3=A1nchez=20Garc=C3=ADa?= Date: Thu, 11 Apr 2024 08:01:08 +0200 Subject: [PATCH 01/55] Allow setting web application base path (#557) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cintia Sanchez Garcia Signed-off-by: Sergio Castaño Arteaga Co-authored-by: Cintia Sanchez Garcia Co-authored-by: Sergio Castaño Arteaga --- docs/config/settings.yml | 12 ++- embed/embed.html | 2 +- embed/package.json | 3 +- .../iframeResizer.contentWindow-v4.3.9.min.js | 0 embed/src/App.tsx | 6 +- embed/src/types.ts | 1 + embed/src/utils/getUrl.tsx | 7 +- embed/vite.config.ts | 15 +-- embed/yarn.lock | 95 +------------------ src/build/datasets.rs | 7 +- src/build/settings.rs | 34 +++++++ src/new/template/settings.yml | 30 +++--- web/package.json | 3 +- web/{src/assets/js => public/assets}/gtm.js | 0 web/src/App.tsx | 21 ++-- web/src/data.ts | 10 ++ web/src/layout/common/Image.tsx | 2 +- web/src/layout/explore/index.tsx | 7 +- web/src/layout/guide/index.tsx | 4 +- web/src/layout/navigation/EmbedModal.tsx | 25 ++--- web/src/layout/navigation/Header.tsx | 28 +++--- web/src/layout/navigation/MobileDropdown.tsx | 7 +- web/src/layout/navigation/MobileHeader.tsx | 8 +- web/src/layout/notFound/index.tsx | 3 +- web/src/layout/stores/groupActive.tsx | 11 +-- web/src/types.ts | 1 + web/src/utils/getBasePath.ts | 5 + web/src/utils/isExploreSection.ts | 7 ++ web/src/window.d.ts | 1 + web/vite.config.ts | 11 +-- web/yarn.lock | 42 +------- 31 files changed, 182 insertions(+), 226 deletions(-) rename embed/{src/assets/js => public/assets}/iframeResizer.contentWindow-v4.3.9.min.js (100%) rename web/{src/assets/js => public/assets}/gtm.js (100%) create mode 100644 web/src/utils/getBasePath.ts create mode 100644 web/src/utils/isExploreSection.ts diff --git a/docs/config/settings.yml b/docs/config/settings.yml index feff137f..88589661 100644 --- a/docs/config/settings.yml +++ b/docs/config/settings.yml @@ -28,6 +28,14 @@ url: https://landscape.cncf.io # gtm: # Google Tag Manager configuration # container_id: # Landscape web application container ID +# Base path (optional) +# +# Base path where the landscape will be hosted. By default the generated +# landscape is prepared to be hosted at the root of the domain. However, if the +# landscape will be hosted in a subpath, this value must be set accordingly. +# +# base_path: / + # Categories (optional) # # Categories information is read from the `landscape.yml` data file. The way @@ -303,8 +311,8 @@ tags: - category: "Wasm" subcategories: - "Packaging, Registries & Application Delivery" - contributor-strategy: [] - environmental-sustainability: [] + contributor-strategy: [ ] + environmental-sustainability: [ ] network: - category: "Orchestration & Management" subcategories: diff --git a/embed/embed.html b/embed/embed.html index f0845ace..c9588371 100644 --- a/embed/embed.html +++ b/embed/embed.html @@ -14,7 +14,7 @@ const resizer = urlParams.get('iframe-resizer'); if (typeof resizer !== null && resizer === 'true') { const script = document.createElement('script'); - script.setAttribute('src', '/embed/assets/iframeResizer.contentWindow-v4.3.9.min.js'); + script.setAttribute('src', './embed/assets/iframeResizer.contentWindow-v4.3.9.min.js'); document.body.appendChild(script); } })(); diff --git a/embed/package.json b/embed/package.json index 3e34c5ef..7cffc0d2 100644 --- a/embed/package.json +++ b/embed/package.json @@ -24,7 +24,6 @@ "eslint-plugin-solid": "^0.13.1", "typescript": "^5.3.3", "vite": "^5.1.4", - "vite-plugin-solid": "^2.10.1", - "vite-plugin-static-copy": "^1.0.1" + "vite-plugin-solid": "^2.10.1" } } diff --git a/embed/src/assets/js/iframeResizer.contentWindow-v4.3.9.min.js b/embed/public/assets/iframeResizer.contentWindow-v4.3.9.min.js similarity index 100% rename from embed/src/assets/js/iframeResizer.contentWindow-v4.3.9.min.js rename to embed/public/assets/iframeResizer.contentWindow-v4.3.9.min.js diff --git a/embed/src/App.tsx b/embed/src/App.tsx index d61fb51e..51fbde72 100644 --- a/embed/src/App.tsx +++ b/embed/src/App.tsx @@ -7,6 +7,7 @@ import NoData from './common/NoData'; import StyleView from './common/StyleView'; import { Alignment, + BASE_PATH_PARAM, BaseItem, Data, DEFAULT_DISPLAY_CATEGORY_HEADER, @@ -116,6 +117,7 @@ const SubcategoryTitle = styled('div')` `; const App = () => { + const [basePath, setBasePath] = createSignal(''); const [key, setKey] = createSignal(); const [data, setData] = createSignal(); const [displayHeader, setDisplayHeader] = createSignal(DEFAULT_DISPLAY_HEADER); @@ -139,6 +141,7 @@ const App = () => { onMount(() => { const urlParams = new URLSearchParams(window.location.search); + const basePathParam = urlParams.get(BASE_PATH_PARAM); const keyParam = urlParams.get(KEY_PARAM); const displayHeaderParam = urlParams.get(DISPLAY_HEADER_PARAM); const styleParam = urlParams.get(ITEMS_STYLE_PARAM); @@ -223,6 +226,7 @@ const App = () => { } // When size and style are not valid, we don´t save the key if (isValidSize && isValidStyle) { + setBasePath(basePathParam || ''); setKey(keyParam); } else { setData(null); @@ -240,7 +244,7 @@ const App = () => { fetch( import.meta.env.MODE === 'development' ? `http://localhost:8000/data/embed_${key()}.json` - : `../data/embed_${key()}.json` + : `${basePath()}/data/embed_${key()}.json` ) .then((res) => { if (res.ok) { diff --git a/embed/src/types.ts b/embed/src/types.ts index 3c438776..fd1619c8 100644 --- a/embed/src/types.ts +++ b/embed/src/types.ts @@ -14,6 +14,7 @@ export const ITEMS_ALIGNMENT_PARAM = 'items-alignment'; export const ITEMS_SPACING_PARAM = 'items-spacing'; export const TITLE_BGCOLOR_PARAM = 'bg-color'; export const TITLE_FGCOLOR_PARAM = 'fg-color'; +export const BASE_PATH_PARAM = 'base-path'; export interface Data { category: Category; diff --git a/embed/src/utils/getUrl.tsx b/embed/src/utils/getUrl.tsx index 7574201b..d7d388d7 100644 --- a/embed/src/utils/getUrl.tsx +++ b/embed/src/utils/getUrl.tsx @@ -1,6 +1,9 @@ +import { BASE_PATH_PARAM } from '../types'; + const getUrl = (): string => { - const url = new URL(document.location.href); - return url.origin; + const urlParams = new URLSearchParams(location.search); + const basePathParam = urlParams.get(BASE_PATH_PARAM); + return `${location.origin}${basePathParam || ''}`; }; export default getUrl; diff --git a/embed/vite.config.ts b/embed/vite.config.ts index fbd4a6a4..64b1fa3f 100644 --- a/embed/vite.config.ts +++ b/embed/vite.config.ts @@ -1,10 +1,8 @@ import { defineConfig } from 'vite'; import solid from 'vite-plugin-solid'; -import { viteStaticCopy } from 'vite-plugin-static-copy'; - export default defineConfig({ - base: '/embed', + base: '', build: { rollupOptions: { input: { @@ -12,14 +10,5 @@ export default defineConfig({ }, }, }, - plugins: [solid(), - viteStaticCopy({ - targets: [ - { - src: './src/assets/js/iframeResizer.contentWindow-v4.3.9.min.js', - dest: 'assets' - } - ] - }) - ] + plugins: [solid()] }); diff --git a/embed/yarn.lock b/embed/yarn.lock index 23c28c26..3ae4d173 100644 --- a/embed/yarn.lock +++ b/embed/yarn.lock @@ -728,14 +728,6 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -769,11 +761,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -789,7 +776,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -833,21 +820,6 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -1095,7 +1067,7 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== -fast-glob@^3.2.11, fast-glob@^3.2.9: +fast-glob@^3.2.9: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -1159,15 +1131,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -fs-extra@^11.1.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" - integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1183,7 +1146,7 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -1238,11 +1201,6 @@ goober@^2.1.10: resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.14.tgz#4a5c94fc34dc086a8e6035360ae1800005135acd" integrity sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg== -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -1304,19 +1262,12 @@ inline-style-parser@0.1.1: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -1387,15 +1338,6 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - kebab-case@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/kebab-case/-/kebab-case-1.0.2.tgz#5eac97d5d220acf606d40e3c0ecfea21f1f9e1eb" @@ -1501,11 +1443,6 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1571,7 +1508,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -1607,13 +1544,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -1815,11 +1745,6 @@ typescript@^5.3.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" @@ -1852,16 +1777,6 @@ vite-plugin-solid@^2.10.1: solid-refresh "^0.6.3" vitefu "^0.2.5" -vite-plugin-static-copy@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vite-plugin-static-copy/-/vite-plugin-static-copy-1.0.1.tgz#c8aa9871d920b0de9c8583caae5510669546cf8e" - integrity sha512-3eGL4mdZoPJMDBT68pv/XKIHR4MgVolStIxxv1gIBP4R8TpHn9C9EnaU0hesqlseJ4ycLGUxckFTu/jpuJXQlA== - dependencies: - chokidar "^3.5.3" - fast-glob "^3.2.11" - fs-extra "^11.1.0" - picocolors "^1.0.0" - vite@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.4.tgz#14e9d3e7a6e488f36284ef13cebe149f060bcfb6" diff --git a/src/build/datasets.rs b/src/build/datasets.rs index 76e98e24..8c0670a9 100644 --- a/src/build/datasets.rs +++ b/src/build/datasets.rs @@ -69,11 +69,15 @@ mod base { /// Base dataset information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] + #[allow(clippy::struct_field_names)] pub(crate) struct Base { pub finances_available: bool, pub foundation: String, pub qr_code: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub base_path: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] pub categories: Vec, @@ -123,7 +127,7 @@ mod base { finances_available: false, foundation: settings.foundation.clone(), qr_code: qr_code.to_string(), - images: settings.images.clone(), + base_path: settings.base_path.clone(), categories: landscape_data.categories.clone(), categories_overridden: vec![], colors: settings.colors.clone(), @@ -132,6 +136,7 @@ mod base { groups: settings.groups.clone().unwrap_or_default(), guide_summary: BTreeMap::new(), header: settings.header.clone(), + images: settings.images.clone(), items: vec![], members_category: settings.members_category.clone(), upcoming_event: settings.upcoming_event.clone(), diff --git a/src/build/settings.rs b/src/build/settings.rs index 97f7888c..dd4b73b7 100644 --- a/src/build/settings.rs +++ b/src/build/settings.rs @@ -27,6 +27,9 @@ pub(crate) struct LandscapeSettings { #[serde(skip_serializing_if = "Option::is_none")] pub analytics: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub base_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub categories: Option>, @@ -112,8 +115,10 @@ impl LandscapeSettings { /// Create a new landscape settings instance from the raw data provided. fn new_from_raw_data(raw_data: &str) -> Result { let mut settings: LandscapeSettings = serde_yaml::from_str(raw_data)?; + settings.validate().context("the landscape settings file provided is not valid")?; settings.footer_text_to_html().context("error converting footer md text to html")?; + settings.remove_base_path_trailing_slash(); settings.set_groups_normalized_name(); Ok(settings) @@ -131,6 +136,15 @@ impl LandscapeSettings { Ok(()) } + /// Remove base_path trailing slash if present. + fn remove_base_path_trailing_slash(&mut self) { + if let Some(base_path) = &mut self.base_path { + if let Some(base_path_updated) = base_path.strip_suffix('/') { + *base_path = base_path_updated.to_string(); + } + } + } + /// Set the normalized name field of the provided groups. fn set_groups_normalized_name(&mut self) { if let Some(groups) = self.groups.as_mut() { @@ -150,6 +164,7 @@ impl LandscapeSettings { // Check url is valid validate_url("landscape", &Some(self.url.clone()))?; + self.validate_base_path()?; self.validate_categories()?; self.validate_colors()?; self.validate_featured_items()?; @@ -165,6 +180,25 @@ impl LandscapeSettings { Ok(()) } + /// Check base path is valid. + fn validate_base_path(&self) -> Result<()> { + let Some(base_path) = &self.base_path else { + return Ok(()); + }; + + // Check base path is not empty + if base_path.is_empty() { + bail!("base_path cannot be empty"); + } + + // Check base path starts with a slash + if !base_path.starts_with('/') { + bail!("base_path must start with a slash"); + } + + Ok(()) + } + /// Check categories are valid. fn validate_categories(&self) -> Result<()> { if let Some(categories) = &self.categories { diff --git a/src/new/template/settings.yml b/src/new/template/settings.yml index 0fefc3b6..703dbade 100644 --- a/src/new/template/settings.yml +++ b/src/new/template/settings.yml @@ -28,9 +28,17 @@ url: http://127.0.0.1:8000 # gtm: # Google Tag Manager configuration # container_id: # Landscape web application container ID +# Base path (optional) +# +# Base path where the landscape will be hosted. By default the generated +# landscape is prepared to be hosted at the root of the domain. However, if the +# landscape will be hosted in a subpath, this value must be set accordingly. +# +# base_path: / + # Categories (optional) # -# Categories information is read from the `landscape.yml` data file. The way +# Categories information is read from the `landscape.yml` data file. The way # categories are displayed in the web application is computed dynamically based # on the number of categories and subcategories, as well as the number of items # on each. Sometimes, however, we may want subcategories to be displayed in a @@ -56,11 +64,11 @@ url: http://127.0.0.1:8000 # # colors: # color1: # Buttons, groups, links -# color2: # Some highlighted items like filters button, search icon -# color3: # Participation stats bars, spinners, modal titles -# color4: # Categories titles in filters, fieldset in filters modal -# color4: # Categories and subcategories frames (odd) -# color5: # Categories and subcategories frames (even) +# color2: # Some highlighted items like filters button, search icon +# color3: # Participation stats bars, spinners, modal titles +# color4: # Categories titles in filters, fieldset in filters modal +# color4: # Categories and subcategories frames (odd) +# color5: # Categories and subcategories frames (even) # colors: color1: "rgba(0, 107, 204, 1)" @@ -138,7 +146,7 @@ footer: # # Defines the preferred size of the landscape items in the grid mode. When the # landscape contains many items, it is recommended to use the `small` size. -# However, if there aren't many items, choosing `medium` or `large` may make +# However, if there aren't many items, choosing `medium` or `large` may make # the landscape look nicer. Users will still be able to adjust the items size # from the UI using the zoom controls. # @@ -185,7 +193,7 @@ header: # Images (optional) # -# Urls of some images used in the landscape UI. +# Urls of some images used in the landscape UI. # # images: # favicon: @@ -199,7 +207,7 @@ header: # but it is important that we define it here as there are some special # operations that depend on it. # -# members_category: +# members_category: # # Osano (optional) @@ -233,7 +241,7 @@ screenshot_width: 1500 # (by using the `tag` field in the `extra` item's section). However, sometimes # this information is not available at the item level. This configuration # section provides a mechanism to automatically asign a TAG to projects items -# based on the categories and subcategories they belong to. +# based on the categories and subcategories they belong to. # # For example, we can define that all projects in the category are # owned by . When the items are processed, the corresponding TAG will be @@ -265,4 +273,4 @@ screenshot_width: 1500 # start: # Start date: (required, format: YYYY-MM-DD) # end: # End date: (required, format: YYYY-MM-DD) # banner_url: # Event banner image url (required, recommended dimensions: 2400x300) -# details_url: # Event details URL (required) +# details_url: # Event details URL (required) diff --git a/web/package.json b/web/package.json index 003d9fbf..aecc0d6e 100644 --- a/web/package.json +++ b/web/package.json @@ -37,8 +37,7 @@ "typescript": "^5.3.3", "vite": "^5.1.5", "vite-plugin-ejs": "^1.7.0", - "vite-plugin-solid": "^2.10.1", - "vite-plugin-static-copy": "^1.0.1" + "vite-plugin-solid": "^2.10.1" }, "description": "Landscape2 is a tool that generates interactive landscapes websites.", "license": "Apache-2.0" diff --git a/web/src/assets/js/gtm.js b/web/public/assets/gtm.js similarity index 100% rename from web/src/assets/js/gtm.js rename to web/public/assets/gtm.js diff --git a/web/src/App.tsx b/web/src/App.tsx index d894a734..0cde832d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,6 +3,15 @@ import isUndefined from 'lodash/isUndefined'; import range from 'lodash/range'; import { createSignal, onMount } from 'solid-js'; +import { + EMBED_SETUP_PATH, + EXPLORE_PATH, + FINANCES_PATH, + GUIDE_PATH, + LOGOS_PREVIEW_PATH, + SCREENSHOTS_PATH, + STATS_PATH, +} from './data'; import Layout from './layout'; import Explore from './layout/explore'; import Finances from './layout/finances'; @@ -69,13 +78,13 @@ const App = () => { return ( - } /> - - - - } /> - + } /> + + + + + } /> ); }; diff --git a/web/src/data.ts b/web/src/data.ts index 7bd72dd5..1116b2e5 100644 --- a/web/src/data.ts +++ b/web/src/data.ts @@ -13,8 +13,18 @@ import { ViewMode, ZoomLevelsPerSize, } from './types'; +import getBasePath from './utils/getBasePath'; import getFoundationNameLabel from './utils/getFoundationNameLabel'; +export const BASE_PATH = getBasePath(); +export const EXPLORE_PATH = BASE_PATH === '' ? '/' : `${BASE_PATH}/`; +export const EMBED_SETUP_PATH = `${BASE_PATH}/embed-setup`; +export const STATS_PATH = `${BASE_PATH}/stats`; +export const GUIDE_PATH = `${BASE_PATH}/guide`; +export const FINANCES_PATH = `${BASE_PATH}/finances`; +export const LOGOS_PREVIEW_PATH = `${BASE_PATH}/logos-preview`; +export const SCREENSHOTS_PATH = '/screenshot'; + export const TAB_PARAM = 'tab'; export const VIEW_MODE_PARAM = 'view-mode'; export const GROUP_PARAM = 'group'; diff --git a/web/src/layout/common/Image.tsx b/web/src/layout/common/Image.tsx index 73749ce1..d31f15c5 100644 --- a/web/src/layout/common/Image.tsx +++ b/web/src/layout/common/Image.tsx @@ -22,7 +22,7 @@ const Image = (props: Props) => { {`${props.name} setError(true)} loading={!isUndefined(props.enableLazyLoad) && props.enableLazyLoad ? 'lazy' : undefined} width="auto" diff --git a/web/src/layout/explore/index.tsx b/web/src/layout/explore/index.tsx index cf19c16f..455119c7 100644 --- a/web/src/layout/explore/index.tsx +++ b/web/src/layout/explore/index.tsx @@ -9,6 +9,7 @@ import { batch, createEffect, createSignal, For, Match, on, onCleanup, onMount, import { ALL_OPTION, + BASE_PATH, CLASSIFY_PARAM, DEFAULT_CLASSIFY, DEFAULT_SORT, @@ -274,7 +275,7 @@ const Explore = (props: Props) => { } } - navigate(`${location.pathname}?${updatedSearchParams.toString()}${getHash()}`, { + navigate(`${BASE_PATH}/?${updatedSearchParams.toString()}${getHash()}`, { state: location.state, replace: true, scroll: true, // default @@ -336,7 +337,7 @@ const Explore = (props: Props) => { const query = params.toString(); - navigate(`${location.pathname}${query === '' ? '' : `?${query}`}${getHash()}`, { + navigate(`${BASE_PATH}/${query === '' ? '' : `?${query}`}${getHash()}`, { state: location.state, replace: true, scroll: true, // default @@ -366,7 +367,7 @@ const Explore = (props: Props) => { }); if (viewMode() === ViewMode.Card) { - navigate(`${location.pathname}${location.search}${location.hash !== '' ? location.hash : getHash()}`, { + navigate(`${BASE_PATH}/${location.search}${location.hash !== '' ? location.hash : getHash()}`, { state: location.state, replace: true, scroll: true, // default diff --git a/web/src/layout/guide/index.tsx b/web/src/layout/guide/index.tsx index 01bd8e96..ea0c5d25 100644 --- a/web/src/layout/guide/index.tsx +++ b/web/src/layout/guide/index.tsx @@ -2,7 +2,7 @@ import { useLocation, useNavigate } from '@solidjs/router'; import isUndefined from 'lodash/isUndefined'; import { createEffect, createMemo, createSignal, For, on, onMount, Show } from 'solid-js'; -import { SMALL_DEVICES_BREAKPOINTS } from '../../data'; +import { GUIDE_PATH, SMALL_DEVICES_BREAKPOINTS } from '../../data'; import useBreakpointDetect from '../../hooks/useBreakpointDetect'; import { CategoryGuide, Guide, StateContent, SubcategoryGuide, SVGIconKind, ToCTitle } from '../../types'; import getNormalizedName from '../../utils/getNormalizedName'; @@ -133,7 +133,7 @@ const GuideIndex = () => { ); const updateRoute = (title: string) => { - navigate(`${location.pathname}${location.search}#${title}`, { + navigate(`${GUIDE_PATH}${location.search}#${title}`, { replace: true, scroll: false, state: { fromMenu: true }, diff --git a/web/src/layout/navigation/EmbedModal.tsx b/web/src/layout/navigation/EmbedModal.tsx index 25093427..d5b2bb20 100644 --- a/web/src/layout/navigation/EmbedModal.tsx +++ b/web/src/layout/navigation/EmbedModal.tsx @@ -1,7 +1,7 @@ import { useLocation, useNavigate } from '@solidjs/router'; import isUndefined from 'lodash/isUndefined'; import sortBy from 'lodash/sortBy'; -import { batch, createEffect, createSignal, For, on, onMount, Show } from 'solid-js'; +import { batch, createEffect, createMemo, createSignal, For, on, onMount, Show } from 'solid-js'; import { Alignment, @@ -40,10 +40,11 @@ import { TITLE_SIZE_PARAM, UPPERCASE_TITLE_PARAM, } from '../../../../embed/src/types'; -import { SMALL_DEVICES_BREAKPOINTS } from '../../data'; +import { BASE_PATH, EMBED_SETUP_PATH, SMALL_DEVICES_BREAKPOINTS } from '../../data'; import useBreakpointDetect from '../../hooks/useBreakpointDetect'; import { Category, Subcategory, SVGIconKind } from '../../types'; import capitalizeFirstLetter from '../../utils/capitalizeFirstLetter'; +import isExploreSection from '../../utils/isExploreSection'; import rgba2hex from '../../utils/rgba2hex'; import CheckBox from '../common/Checkbox'; import CodeBlock from '../common/CodeBlock'; @@ -91,8 +92,8 @@ const EmbedModal = () => { ? rgba2hex(window.baseDS.colors.color5) : DEFAULT_TITLE_BG_COLOR; // Icon is only visible when Explore section is loaded - const isVisible = () => ['/', '/embed-setup'].includes(location.pathname); - const isEmbedSetupActive = () => location.pathname === '/embed-setup'; + const isVisible = createMemo(() => isExploreSection(location.pathname)); + const isEmbedSetupActive = () => location.pathname === EMBED_SETUP_PATH; const [visibleModal, setVisibleModal] = createSignal(isEmbedSetupActive()); const categoriesList = () => sortBy(window.baseDS.categories, ['name']); const [subcategoriesList, setSubcategoriesList] = createSignal( @@ -127,10 +128,10 @@ const EmbedModal = () => { const [prevHash, setPrevHash] = createSignal(''); const [prevSearch, setPrevSearch] = createSignal(''); - const getUrl = () => { + const getIFrameUrl = () => { return `${ import.meta.env.MODE === 'development' ? 'http://localhost:8000' : window.location.origin - }/embed/embed.html?${KEY_PARAM}=${key() || categoriesList()[0].normalized_name}&${DISPLAY_HEADER_PARAM}=${ + }${BASE_PATH}/embed/embed.html?${KEY_PARAM}=${key() || categoriesList()[0].normalized_name}&${DISPLAY_HEADER_PARAM}=${ displayHeader() ? 'true' : 'false' }&${DISPLAY_HEADER_CATEGORY_PARAM}=${ displayCategoryTitle() ? 'true' : 'false' @@ -144,7 +145,7 @@ const EmbedModal = () => { itemsSpacingType() === SpacingType.Custom && !isUndefined(itemsSpacing()) ? `&${ITEMS_SPACING_PARAM}=${itemsSpacing()}` : '' - }&${TITLE_BGCOLOR_PARAM}=${encodeURIComponent(bgColor())}&${TITLE_FGCOLOR_PARAM}=${encodeURIComponent(fgColor())}`; + }&${TITLE_BGCOLOR_PARAM}=${encodeURIComponent(bgColor())}&${TITLE_FGCOLOR_PARAM}=${encodeURIComponent(fgColor())}${!isUndefined(window.baseDS.base_path) ? `&base-path=${encodeURIComponent(window.baseDS.base_path)}` : ''}`; }; const onUpdateSpacingType = (type: SpacingType) => { @@ -224,7 +225,7 @@ const EmbedModal = () => { }; const onClose = () => { - navigate(`/${prevSearch() !== '' ? prevSearch() : ''}${prevHash()}`, { + navigate(`${BASE_PATH}/${prevSearch() !== '' ? prevSearch() : ''}${prevHash()}`, { replace: true, }); setVisibleModal(false); @@ -256,11 +257,11 @@ const EmbedModal = () => { }; onMount(() => { - setUrl(getUrl()); + setUrl(getIFrameUrl()); }); createEffect(() => { - setUrl(getUrl()); + setUrl(getIFrameUrl()); }); createEffect( @@ -276,7 +277,7 @@ const EmbedModal = () => { if (visibleModal()) { setPrevSearch(location.search); setPrevHash(location.hash); - setUrl(getUrl()); + setUrl(getIFrameUrl()); } }) ); @@ -291,7 +292,7 @@ const EmbedModal = () => { e.preventDefault(); e.stopPropagation(); setVisibleModal(true); - navigate('embed-setup', { + navigate(EMBED_SETUP_PATH, { replace: true, }); }} diff --git a/web/src/layout/navigation/Header.tsx b/web/src/layout/navigation/Header.tsx index 494a9572..c815169f 100644 --- a/web/src/layout/navigation/Header.tsx +++ b/web/src/layout/navigation/Header.tsx @@ -1,10 +1,11 @@ import { useLocation, useNavigate } from '@solidjs/router'; import isEmpty from 'lodash/isEmpty'; import isUndefined from 'lodash/isUndefined'; -import { Show } from 'solid-js'; +import { createMemo, Show } from 'solid-js'; -import { ALL_OPTION } from '../../data'; +import { ALL_OPTION, EXPLORE_PATH, GUIDE_PATH, SCREENSHOTS_PATH, STATS_PATH } from '../../data'; import { SVGIconKind, ViewMode } from '../../types'; +import isExploreSection from '../../utils/isExploreSection'; import scrollToTop from '../../utils/scrollToTop'; import DownloadDropdown from '../common/DownloadDropdown'; import ExternalLink from '../common/ExternalLink'; @@ -21,6 +22,7 @@ const Header = () => { const logo = () => (window.baseDS.header ? window.baseDS.header!.logo : undefined); const setViewMode = useSetViewMode(); const setSelectedGroup = useSetGroupActive(); + const isExploreActive = createMemo(() => isExploreSection(location.pathname)); const isActive = (path: string) => { return path === location.pathname; @@ -41,7 +43,7 @@ const Header = () => { class="btn btn-link p-0 pe-3 me-2 me-xl-5" onClick={() => { resetDefaultExploreValues(); - navigate('/', { + navigate(EXPLORE_PATH, { state: { from: 'logo-header' }, }); scrollToTop(false); @@ -59,7 +61,7 @@ const Header = () => { {
:
- + {(f: string) => { const activeFiltersPerCategory = () => activeFilters()[f as FilterCategory]; if (isUndefined(activeFiltersPerCategory()) || isEmpty(activeFiltersPerCategory())) return null; @@ -48,107 +64,147 @@ const ActiveFiltersList = (props: Props) => { !isUndefined(props.maturityOptions) && f === FilterCategory.Maturity && props.maturityOptions.every((element) => activeFiltersPerCategory()!.includes(element)); + + const allLicensesSelected = () => + !isUndefined(licenseOptions()) && + licenseOptions()!.length > 0 && + f === FilterCategory.License && + licenseOptions()!.every((element) => activeFiltersPerCategory()!.includes(element)); + const foundationLabel = getFoundationNameLabel(); return ( - - - -
-
- {f}: - {foundationLabel} + <> + + + +
+
+ {f}: + {foundationLabel} +
+
- -
- - + + - - - {(c: string) => { - return ( - + +
+
+ {f}: + Open Source +
+ -
-
- ); - }} -
-
+ + +
+
+
- - - {(c: string) => { - return ( - -
-
- {f}: - + + {(c: string) => { + return ( + +
+
+ {c} +
+
- + {c}}> + + <>{formatProfitLabel(c)} + + + {formatTAGName(c)} + + + <>{startCase(c.replace(REGEX_UNDERSCORE, ' '))} + + + Not open source + + + Not {getFoundationNameLabel()} project + + +
- - ); - }} - - - + +
+
+ ); + }} +
+ ); }}
diff --git a/web/src/layout/common/Section.tsx b/web/src/layout/common/Section.tsx index 7f90601d..5aac6f88 100644 --- a/web/src/layout/common/Section.tsx +++ b/web/src/layout/common/Section.tsx @@ -3,13 +3,12 @@ import isUndefined from 'lodash/isUndefined'; import { For, Show } from 'solid-js'; import { FilterCategory, FilterOption, FilterSection } from '../../types'; -import getFoundationNameLabel from '../../utils/getFoundationNameLabel'; import CheckBox from './Checkbox'; import styles from './Section.module.css'; interface Props { section?: FilterSection; - extraMaturity?: FilterSection; + extraOptions?: { [key: string]: FilterSection | undefined }; activeFilters?: string[]; colClass?: string; title?: string; @@ -45,7 +44,7 @@ const Section = (props: Props) => { if (isUndefined(props.section)) { return false; } - if (props.section.value === FilterCategory.Maturity && isUndefined(props.extraMaturity)) { + if (props.section.value === FilterCategory.Maturity && isUndefined(props.extraOptions)) { return false; } return true; @@ -56,13 +55,9 @@ const Section = (props: Props) => { {(opt: FilterOption) => { let subOpts: string[] | undefined; let suboptions = opt.suboptions; - if ( - opt.value === getFoundationNameLabel() && - props.section?.value === FilterCategory.Maturity && - !isUndefined(props.extraMaturity) - ) { - suboptions = props.extraMaturity.options; - subOpts = props.extraMaturity.options.map((subOpt: FilterOption) => subOpt.value); + if (!isUndefined(props.extraOptions) && !isUndefined(props.extraOptions[opt.value])) { + suboptions = props.extraOptions[opt.value]!.options; + subOpts = props.extraOptions[opt.value]!.options.map((subOpt: FilterOption) => subOpt.value); } else if (opt.suboptions) { subOpts = opt.suboptions.map((subOpt: FilterOption) => subOpt.value); } diff --git a/web/src/layout/common/itemModal/RepositoriesSection.module.css b/web/src/layout/common/itemModal/RepositoriesSection.module.css index 61083ac9..d3fbfc6f 100644 --- a/web/src/layout/common/itemModal/RepositoriesSection.module.css +++ b/web/src/layout/common/itemModal/RepositoriesSection.module.css @@ -55,8 +55,13 @@ max-width: calc(100% - 1.5rem); } +.goodFirstBadge { + height: 19px; +} + .goodFirstBadge img { - margin-top: -3px; + border: 1px solid var(--bs-gray-700); + max-height: 100%; } .badges { diff --git a/web/src/layout/common/itemModal/RepositoriesSection.tsx b/web/src/layout/common/itemModal/RepositoriesSection.tsx index 5ebb13c3..8207ef89 100644 --- a/web/src/layout/common/itemModal/RepositoriesSection.tsx +++ b/web/src/layout/common/itemModal/RepositoriesSection.tsx @@ -71,11 +71,11 @@ const RepositoryInfo = (props: RepoProps) => {
diff --git a/web/src/layout/explore/filters/Filters.module.css b/web/src/layout/explore/filters/Filters.module.css index f355d8be..e49e0b4c 100644 --- a/web/src/layout/explore/filters/Filters.module.css +++ b/web/src/layout/explore/filters/Filters.module.css @@ -41,6 +41,11 @@ color: var(--color4); } +.section { + max-height: 235px; + overscroll-behavior: contain; +} + @media (max-width: 1199.98px) { .modal { width: 90%; diff --git a/web/src/layout/explore/filters/SearchbarSection.tsx b/web/src/layout/explore/filters/SearchbarSection.tsx index 09a63a02..3c164362 100644 --- a/web/src/layout/explore/filters/SearchbarSection.tsx +++ b/web/src/layout/explore/filters/SearchbarSection.tsx @@ -12,6 +12,7 @@ export interface Props { title?: string; placeholder?: string; section?: FilterSection; + extraOptions?: { [key: string]: FilterSection }; initialActiveFilters: Accessor; updateActiveFilters: (value: FilterCategory, options: string[]) => void; resetFilter: (value: FilterCategory) => void; @@ -200,15 +201,34 @@ const SearchbarSection = (props: Props) => { {(opt: FilterOption) => { return ( - onChange(value, checked)} - /> + <> + onChange(value, checked)} + /> + +
+ + {(subOpt: FilterOption) => ( + onChange(value, checked)} + /> + )} + +
+
+ ); }}
diff --git a/web/src/layout/explore/filters/index.tsx b/web/src/layout/explore/filters/index.tsx index c895dda7..8f154668 100644 --- a/web/src/layout/explore/filters/index.tsx +++ b/web/src/layout/explore/filters/index.tsx @@ -67,6 +67,9 @@ const Filters = (props: Props) => { if (filter === FilterCategory.Maturity) { opts.push(`non-${getFoundationNameLabel()}`); } + if (filter === FilterCategory.License) { + opts.push('non-oss'); + } cleanFilters[filter] = intersection(opts, props.initialActiveFilters()[filter]); } }); @@ -305,11 +308,11 @@ const Filters = (props: Props) => {
{ activeFilters={{ ...tmpActiveFilters() }[FilterCategory.TAG]} updateActiveFilters={updateActiveFilters} resetFilter={resetFilter} - sectionClass={styles.section} - /> - + +
+
{ activeFilters={{ ...tmpActiveFilters() }[FilterCategory.Extra]} updateActiveFilters={updateActiveFilters} resetFilter={resetFilter} - sectionClass={styles.section} + sectionClass={`overflow-auto visibleScroll ${styles.section}`} />
@@ -373,7 +379,7 @@ const Filters = (props: Props) => { activeFilters={{ ...tmpActiveFilters() }[FilterCategory.OrgType]} updateActiveFilters={updateActiveFilters} resetFilter={resetFilter} - sectionClass={styles.section} + sectionClass={`overflow-auto visibleScroll ${styles.section}`} /> { const [classifyOptions, setClassifyOptions] = createSignal(Object.values(ClassifyOption)); const [sortOptions, setSortOptions] = createSignal(Object.values(SortOption)); const [numItems, setNumItems] = createSignal<{ [key: string]: number }>({}); + const [licenseOptions, setLicenseOptions] = createSignal([]); const checkIfFullDataRequired = (): boolean => { if (viewMode() === ViewMode.Card) { @@ -135,17 +134,12 @@ const Explore = (props: Props) => { } }; - const getMaturityOptions = () => { - const maturity = props.initialData.items.map((i: Item) => i.maturity); - return uniq(compact(maturity)) || []; - }; - const checkVisibleItemsNumber = (numItemsPerGroup: { [key: string]: number }) => { setNumItems(numItemsPerGroup); setNumVisibleItems(numItemsPerGroup[selectedGroup() || ALL_OPTION]); }; - const maturityOptions = getMaturityOptions(); + const maturityOptions = itemsDataGetter.getMaturityOptions(); const getActiveFiltersFromUrl = (): ActiveFilters => { const currentFilters: ActiveFilters = {}; @@ -156,6 +150,8 @@ const Explore = (props: Props) => { if (Object.values(FilterCategory).includes(f)) { if (f === FilterCategory.Maturity && value === getFoundationNameLabel()) { currentFilters[f] = maturityOptions; + } else if (f === FilterCategory.License && value === 'oss') { + currentFilters[f] = licenseOptions(); } else { if (currentFilters[f]) { currentFilters[f] = [...currentFilters[f]!, value]; @@ -231,15 +227,15 @@ const Explore = (props: Props) => { // Reset classify and sort when view mode or group changes if ([VIEW_MODE_PARAM, GROUP_PARAM].includes(param)) { - // Remove all filters from current searchparams - Object.values(FilterCategory).forEach((f: string) => { - updatedSearchParams.delete(f); - }); - EXTRA_FILTERS.forEach((f: string) => { - updatedSearchParams.delete(f); - }); - if (param === GROUP_PARAM) { + // Remove all filters from current searchparams + Object.values(FilterCategory).forEach((f: string) => { + updatedSearchParams.delete(f); + }); + EXTRA_FILTERS.forEach((f: string) => { + updatedSearchParams.delete(f); + }); + setActiveFilters({}); } @@ -313,6 +309,11 @@ const Explore = (props: Props) => { maturityOptions.every((element) => f![filterId as FilterCategory]!.includes(element)) ) { return params.append(filterId as string, foundation); + } else if ( + filterId === FilterCategory.License && + licenseOptions().every((element) => f![filterId as FilterCategory]!.includes(element)) + ) { + return params.append(filterId, 'oss'); } else { return f![filterId as FilterCategory]!.forEach((id: string) => { if (id.toString() !== foundation) { @@ -374,6 +375,8 @@ const Explore = (props: Props) => { setClassifyAndSortOptions(options); setClassifyOptions(options[selectedGroup() || ALL_OPTION].classify); setSortOptions(options[selectedGroup() || ALL_OPTION].sort); + setLicenseOptions(itemsDataGetter.getLicenseOptionsPerGroup(selectedGroup() || ALL_OPTION)); + // Full data is only applied when card view is active or filters are not empty to avoid re-render grid // After this when filters are applied or view mode changes, we use fullData if available if (viewMode() === ViewMode.Card || checkIfFullDataRequired()) { @@ -383,6 +386,7 @@ const Explore = (props: Props) => { setGroupsData(data.grid); setCardData(data.card); setCardMenu(data.menu); + setActiveFilters(getActiveFiltersFromUrl()); setFullDataApplied(true); setReadyData(true); @@ -413,6 +417,7 @@ const Explore = (props: Props) => { on(selectedGroup, () => { if (groupsData()) { setNumVisibleItems(numItems()[selectedGroup() || ALL_OPTION]); + setLicenseOptions(itemsDataGetter.getLicenseOptionsPerGroup(selectedGroup() || ALL_OPTION)); } }) ); @@ -455,10 +460,10 @@ const Explore = (props: Props) => { const removeFilter = (name: FilterCategory, value: string) => { let tmpActiveFilters: string[] = ({ ...activeFilters() }[name] || []).filter((f: string) => f !== value); - if (name === FilterCategory.Maturity) { - if (isEqual(tmpActiveFilters, [window.baseDS.foundation.toLowerCase()])) { - tmpActiveFilters = []; - } + if (name === FilterCategory.Maturity && value === getFoundationNameLabel()) { + tmpActiveFilters = difference(tmpActiveFilters, maturityOptions); + } else if (name === FilterCategory.License && value === 'oss') { + tmpActiveFilters = difference(tmpActiveFilters, licenseOptions()); } updateActiveFilters(name, tmpActiveFilters); }; @@ -477,10 +482,6 @@ const Explore = (props: Props) => { applyFilters({}); }; - const resetFilter = (name: FilterCategory) => { - updateActiveFilters(name, []); - }; - const applyFilters = (newFilters: ActiveFilters) => { setVisibleLoading(true); @@ -871,7 +872,7 @@ const Explore = (props: Props) => { diff --git a/web/src/utils/filterData.ts b/web/src/utils/filterData.ts index c5f9069c..cc468e1e 100644 --- a/web/src/utils/filterData.ts +++ b/web/src/utils/filterData.ts @@ -56,17 +56,23 @@ const filterData = (items: Item[], activeFilters: ActiveFilters): Item[] => { // License License if (activeFilters[FilterCategory.License]) { - if (isUndefined(item.repositories)) { + if (isUndefined(item.oss) && !activeFilters[FilterCategory.License].includes('non-oss')) { return false; } else { - const licenses: string[] = []; - item.repositories.forEach((repo: Repository) => { - if (repo.github_data && repo.github_data.license) { - licenses.push(repo.github_data.license); + if (!isUndefined(item.oss)) { + if (isUndefined(item.repositories)) { + return false; + } else { + const licenses: string[] = []; + item.repositories.forEach((repo: Repository) => { + if (repo.github_data && repo.github_data.license) { + licenses.push(repo.github_data.license); + } + }); + if (!licenses.some((l: string) => activeFilters[FilterCategory.License]?.includes(l))) { + return false; + } } - }); - if (!licenses.some((l: string) => activeFilters[FilterCategory.License]?.includes(l))) { - return false; } } } @@ -97,8 +103,10 @@ const filterData = (items: Item[], activeFilters: ActiveFilters): Item[] => { ) { return false; } else { - if (!isUndefined(item.maturity) && !activeFilters[FilterCategory.Maturity].includes(item.maturity)) { - return false; + if (!isUndefined(item.maturity)) { + if (!activeFilters[FilterCategory.Maturity].includes(item.maturity)) { + return false; + } } } } diff --git a/web/src/utils/formatLabelProfit.ts b/web/src/utils/formatLabelProfit.ts index 75dcb481..78c40c12 100644 --- a/web/src/utils/formatLabelProfit.ts +++ b/web/src/utils/formatLabelProfit.ts @@ -1,7 +1,8 @@ import { REGEX_UNDERSCORE } from '../data'; +const REGEX_NON = /non/g; const formatProfitLabel = (text: string): string => { - return text.replace(REGEX_UNDERSCORE, ' '); + return text.replace(REGEX_UNDERSCORE, ' ').replace(REGEX_NON, 'not'); }; export default formatProfitLabel; diff --git a/web/src/utils/itemsDataGetter.ts b/web/src/utils/itemsDataGetter.ts index 13291973..c12121ca 100644 --- a/web/src/utils/itemsDataGetter.ts +++ b/web/src/utils/itemsDataGetter.ts @@ -1,9 +1,11 @@ +import compact from 'lodash/compact'; import groupBy from 'lodash/groupBy'; import intersection from 'lodash/intersection'; import isEmpty from 'lodash/isEmpty'; import isEqual from 'lodash/isEqual'; import isUndefined from 'lodash/isUndefined'; import some from 'lodash/some'; +import uniq from 'lodash/uniq'; import uniqWith from 'lodash/uniqWith'; import { ALL_OPTION, DEFAULT_CLASSIFY } from '../data'; @@ -509,6 +511,31 @@ export class ItemsDataGetter { } } + // Get maturity options + public getMaturityOptions(): string[] { + const maturity = window.baseDS.items.map((i: Item) => i.maturity); + return uniq(compact(maturity)) || []; + } + + // Get license options + public getLicenseOptionsPerGroup(group: string): string[] { + if (this.ready && this.landscapeData && this.landscapeData.items) { + const allGroupedItems = this.getGroupedData(); + const options: string[] = []; + allGroupedItems[group].forEach((i: Item) => { + if (i.repositories) { + i.repositories.forEach((r: Repository) => { + if (r.github_data) { + options.push(r.github_data!.license); + } + }); + } + }); + return uniq(compact(options)); + } + return []; + } + // Prepare logos options public prepareLogosOptions(): LogosOptionsGroup[] { const options: LogosOptionsGroup[] = []; From f43f8901fdc0c0f5d1972133657ea9abaf2253d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cintia=20S=C3=A1nchez=20Garc=C3=ADa?= Date: Mon, 22 Apr 2024 15:34:55 +0200 Subject: [PATCH 23/55] Remove open issues label border (#584) Signed-off-by: Cintia Sanchez Garcia --- web/src/layout/common/itemModal/RepositoriesSection.module.css | 3 +-- web/src/layout/common/itemModal/RepositoriesSection.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/layout/common/itemModal/RepositoriesSection.module.css b/web/src/layout/common/itemModal/RepositoriesSection.module.css index d3fbfc6f..2169edbf 100644 --- a/web/src/layout/common/itemModal/RepositoriesSection.module.css +++ b/web/src/layout/common/itemModal/RepositoriesSection.module.css @@ -56,11 +56,10 @@ } .goodFirstBadge { - height: 19px; + height: 20px; } .goodFirstBadge img { - border: 1px solid var(--bs-gray-700); max-height: 100%; } diff --git a/web/src/layout/common/itemModal/RepositoriesSection.tsx b/web/src/layout/common/itemModal/RepositoriesSection.tsx index 8207ef89..7bcfec1a 100644 --- a/web/src/layout/common/itemModal/RepositoriesSection.tsx +++ b/web/src/layout/common/itemModal/RepositoriesSection.tsx @@ -75,7 +75,7 @@ const RepositoryInfo = (props: RepoProps) => { href={`https://github.com/${formatRepoUrl(props.repository.url)}/issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue"`} > From c0831bbf7876ea1954381f8b85e57928e5db7326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Fri, 26 Apr 2024 13:45:56 +0200 Subject: [PATCH 24/55] Add experimental overlay feature (#587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga Signed-off-by: Cintia Sanchez Garcia Co-authored-by: Sergio Castaño Arteaga Co-authored-by: Cintia Sanchez Garcia --- .github/dependabot.yml | 2 +- .github/workflows/build-images.yml | 2 +- .github/workflows/ci.yml | 12 +- .github/workflows/release.yml | 31 +- .gitignore | 18 +- Cargo.lock | 82 ++- Cargo.toml | 35 +- README.md | 2 +- askama.toml | 2 - build.rs | 51 -- crates/cli/Cargo.toml | 61 ++ Dockerfile => crates/cli/Dockerfile | 12 +- crates/cli/askama.toml | 2 + crates/cli/build.rs | 112 ++++ {src => crates/cli/src}/build/api.rs | 0 {src => crates/cli/src}/build/cache.rs | 0 {src => crates/cli/src}/build/clomonitor.rs | 0 {src => crates/cli/src}/build/crunchbase.rs | 284 +++------ {src => crates/cli/src}/build/export.rs | 0 {src => crates/cli/src}/build/github.rs | 180 ++---- {src => crates/cli/src}/build/logos.rs | 20 +- {src => crates/cli/src}/build/mod.rs | 199 ++++--- crates/cli/src/build/projects.rs | 122 ++++ crates/cli/src/deploy/mod.rs | 21 + {src => crates/cli/src}/deploy/s3.rs | 15 +- crates/cli/src/lib.rs | 12 + crates/cli/src/main.rs | 82 +++ {src => crates/cli/src}/new/mod.rs | 13 +- {src => crates/cli/src}/new/template/data.yml | 0 .../cli/src}/new/template/guide.yml | 0 .../cli/src}/new/template/logos/cncf.svg | 0 .../cli/src}/new/template/settings.yml | 0 {src => crates/cli/src}/serve/mod.rs | 28 +- {src => crates/cli/src}/validate/mod.rs | 38 +- .../cli/templates}/projects.md | 0 crates/core/Cargo.toml | 25 + {src/build => crates/core/src}/data.rs | 548 +++++++----------- crates/core/src/data/legacy.rs | 232 ++++++++ {src/build => crates/core/src}/datasets.rs | 62 +- {src/build => crates/core/src}/guide.rs | 41 +- crates/core/src/lib.rs | 13 + {src/build => crates/core/src}/settings.rs | 81 ++- {src/build => crates/core/src}/stats.rs | 82 +-- crates/core/src/util.rs | 85 +++ crates/wasm/overlay/Cargo.toml | 24 + crates/wasm/overlay/overlay-test.html | 22 + crates/wasm/overlay/src/lib.rs | 159 +++++ src/build/projects.rs | 124 ---- src/deploy/mod.rs | 3 - src/main.rs | 243 -------- {embed => ui/embed}/.eslintrc | 0 {embed => ui/embed}/.prettierignore | 0 {embed => ui/embed}/.prettierrc | 0 {embed => ui/embed}/embed.html | 0 {embed => ui/embed}/env.d.ts | 0 {embed => ui/embed}/package.json | 0 .../iframeResizer.contentWindow-v4.3.9.min.js | 0 {embed => ui/embed}/src/App.tsx | 0 .../embed}/src/common/ExternalLink.tsx | 0 {embed => ui/embed}/src/common/GridItem.tsx | 0 {embed => ui/embed}/src/common/Image.tsx | 0 {embed => ui/embed}/src/common/Loading.tsx | 0 {embed => ui/embed}/src/common/NoData.tsx | 0 {embed => ui/embed}/src/common/StyleView.tsx | 0 {embed => ui/embed}/src/index.tsx | 0 {embed => ui/embed}/src/types.ts | 0 {embed => ui/embed}/src/utils/getUrl.tsx | 0 {embed => ui/embed}/src/vite-env.d.ts | 0 {embed => ui/embed}/src/window.d.ts | 0 {embed => ui/embed}/tsconfig.json | 0 {embed => ui/embed}/tsconfig.node.json | 0 {embed => ui/embed}/vite.config.ts | 0 {embed => ui/embed}/yarn.lock | 0 {web => ui/webapp}/.eslintrc | 0 {web => ui/webapp}/.gitignore | 0 {web => ui/webapp}/.prettierrc | 0 {web => ui/webapp}/index.html | 0 {web => ui/webapp}/package.json | 0 {web => ui/webapp}/public/assets/gtm.js | 0 {web => ui/webapp}/src/App.css | 4 + ui/webapp/src/App.module.css | 49 ++ ui/webapp/src/App.tsx | 170 ++++++ {web => ui/webapp}/src/data.ts | 83 ++- {web => ui/webapp}/src/env.d.ts | 0 {web => ui/webapp}/src/hooks/useBodyScroll.ts | 0 .../webapp}/src/hooks/useBreakpointDetect.tsx | 0 .../webapp}/src/hooks/useOutsideClick.ts | 0 {web => ui/webapp}/src/index.tsx | 0 ui/webapp/src/layout/Layout.module.css | 7 + .../common/ActiveFiltersList.module.css | 0 .../src/layout/common/ActiveFiltersList.tsx | 0 .../src/layout/common/Badge.module.css | 0 .../webapp}/src/layout/common/Badge.tsx | 0 .../common/ButtonCopyToClipboard.module.css | 0 .../layout/common/ButtonCopyToClipboard.tsx | 0 .../common/ButtonToTopScroll.module.css | 0 .../src/layout/common/ButtonToTopScroll.tsx | 0 .../src/layout/common/Checkbox.module.css | 0 .../webapp}/src/layout/common/Checkbox.tsx | 0 .../src/layout/common/CodeBlock.module.css | 0 .../webapp}/src/layout/common/CodeBlock.tsx | 0 .../layout/common/CollapsableText.module.css | 0 .../src/layout/common/CollapsableText.tsx | 0 .../layout/common/DownloadDropdown.module.css | 0 .../src/layout/common/DownloadDropdown.tsx | 0 .../src/layout/common/ExternalLink.module.css | 0 .../src/layout/common/ExternalLink.tsx | 0 .../layout/common/FiltersInLine.module.css | 0 .../src/layout/common/FiltersInLine.tsx | 0 .../src/layout/common/FoundationBadge.tsx | 4 +- .../layout/common/FullScreenModal.module.css | 0 .../src/layout/common/FullScreenModal.tsx | 0 .../src/layout/common/HoverableItem.tsx | 0 .../webapp}/src/layout/common/Image.tsx | 24 +- .../src/layout/common/Loading.module.css | 0 .../webapp}/src/layout/common/Loading.tsx | 11 +- .../layout/common/MaturityBadge.module.css | 0 .../src/layout/common/MaturityBadge.tsx | 0 .../src/layout/common/Modal.module.css | 0 .../webapp}/src/layout/common/Modal.tsx | 0 .../src/layout/common/NoData.module.css | 0 .../webapp}/src/layout/common/NoData.tsx | 2 +- .../src/layout/common/Pagination.module.css | 0 .../webapp}/src/layout/common/Pagination.tsx | 0 .../webapp}/src/layout/common/SVGIcon.tsx | 2 +- .../src/layout/common/Searchbar.module.css | 0 .../webapp}/src/layout/common/Searchbar.tsx | 8 +- .../src/layout/common/Section.module.css | 0 .../webapp}/src/layout/common/Section.tsx | 0 .../src/layout/common/Sidebar.module.css | 0 .../webapp}/src/layout/common/Sidebar.tsx | 0 .../webapp}/src/layout/common/Tabs.module.css | 0 {web => ui/webapp}/src/layout/common/Tabs.tsx | 0 .../itemModal/AcquisitionsTable.module.css | 0 .../common/itemModal/AcquisitionsTable.tsx | 0 .../layout/common/itemModal/BadgeModal.tsx | 4 +- .../layout/common/itemModal/Box.module.css | 0 .../src/layout/common/itemModal/Box.tsx | 0 .../common/itemModal/Content.module.css | 0 .../src/layout/common/itemModal/Content.tsx | 6 +- .../itemModal/FundingRoundsTable.module.css | 0 .../common/itemModal/FundingRoundsTable.tsx | 0 .../common/itemModal/ItemDropdown.module.css | 0 .../layout/common/itemModal/ItemDropdown.tsx | 0 .../common/itemModal/ItemModal.module.css | 0 .../itemModal/LanguagesStats.module.css | 0 .../common/itemModal/LanguagesStats.tsx | 0 .../itemModal/MaturitySection.module.css | 0 .../common/itemModal/MaturitySection.tsx | 0 .../common/itemModal/MobileContent.module.css | 0 .../layout/common/itemModal/MobileContent.tsx | 4 +- .../MobileMaturitySection.module.css | 0 .../itemModal/MobileMaturitySection.tsx | 0 .../common/itemModal/ParentProject.module.css | 0 .../layout/common/itemModal/ParentProject.tsx | 3 +- .../itemModal/ParticipationStats.module.css | 0 .../common/itemModal/ParticipationStats.tsx | 0 .../itemModal/RepositoriesSection.module.css | 0 .../common/itemModal/RepositoriesSection.tsx | 0 .../src/layout/common/itemModal/index.tsx | 0 .../common/zoomModal/ZoomModal.module.css | 0 .../src/layout/common/zoomModal/index.tsx | 0 .../webapp}/src/layout/explore/Content.tsx | 0 .../src/layout/explore/Explore.module.css | 0 .../src/layout/explore/card/Card.module.css | 0 .../webapp}/src/layout/explore/card/Card.tsx | 0 .../explore/card/CardCategory.module.css | 0 .../layout/explore/card/CardTitle.module.css | 0 .../src/layout/explore/card/CardTitle.tsx | 0 .../layout/explore/card/Content.module.css | 0 .../src/layout/explore/card/Content.tsx | 0 .../src/layout/explore/card/Menu.module.css | 0 .../webapp}/src/layout/explore/card/Menu.tsx | 0 .../webapp}/src/layout/explore/card/index.tsx | 0 .../layout/explore/filters/Filters.module.css | 0 .../filters/SearchbarSection.module.css | 0 .../explore/filters/SearchbarSection.tsx | 0 .../src/layout/explore/filters/index.tsx | 0 .../src/layout/explore/grid/Grid.module.css | 0 .../webapp}/src/layout/explore/grid/Grid.tsx | 4 +- .../explore/grid/GridCategory.module.css | 0 .../layout/explore/grid/GridItem.module.css | 0 .../src/layout/explore/grid/GridItem.tsx | 0 .../webapp}/src/layout/explore/grid/index.tsx | 6 +- .../webapp}/src/layout/explore/index.tsx | 0 .../src/layout/explore/mobile/Card.module.css | 0 .../src/layout/explore/mobile/Card.tsx | 0 .../mobile/ExploreMobileIndex.module.css | 0 .../explore/mobile/ExploreMobileIndex.tsx | 0 .../explore/mobile/MobileGrid.module.css | 0 .../src/layout/explore/mobile/MobileGrid.tsx | 0 .../src/layout/finances/Finances.module.css | 0 .../layout/finances/MobileFilters.module.css | 0 .../src/layout/finances/MobileFilters.tsx | 0 .../webapp}/src/layout/finances/index.tsx | 3 +- .../webapp}/src/layout/guide/Guide.module.css | 0 .../guide/SubcategoryExtended.module.css | 0 .../src/layout/guide/SubcategoryExtended.tsx | 3 +- .../layout/guide/SubcategoryGrid.module.css | 0 .../src/layout/guide/SubcategoryGrid.tsx | 0 .../webapp}/src/layout/guide/ToC.module.css | 0 {web => ui/webapp}/src/layout/guide/ToC.tsx | 0 {web => ui/webapp}/src/layout/guide/index.tsx | 20 +- {web => ui/webapp}/src/layout/index.tsx | 0 .../webapp}/src/layout/logos/Logos.module.css | 0 {web => ui/webapp}/src/layout/logos/index.tsx | 0 .../layout/navigation/EmbedModal.module.css | 0 .../src/layout/navigation/EmbedModal.tsx | 3 +- .../src/layout/navigation/Footer.module.css | 0 .../webapp}/src/layout/navigation/Footer.tsx | 24 +- .../src/layout/navigation/Header.module.css | 5 + .../webapp}/src/layout/navigation/Header.tsx | 30 +- .../src/layout/navigation/MiniFooter.tsx | 0 .../navigation/MobileDropdown.module.css | 0 .../src/layout/navigation/MobileDropdown.tsx | 0 .../layout/navigation/MobileHeader.module.css | 4 + .../src/layout/navigation/MobileHeader.tsx | 27 +- .../src/layout/notFound/NotFound.module.css | 0 .../webapp}/src/layout/notFound/index.tsx | 0 .../src/layout/screenshots/Grid.module.css | 0 .../webapp}/src/layout/screenshots/Grid.tsx | 0 .../layout/screenshots/Screenshots.module.css | 0 .../webapp}/src/layout/screenshots/index.tsx | 0 .../webapp}/src/layout/stats/Box.module.css | 0 {web => ui/webapp}/src/layout/stats/Box.tsx | 0 .../webapp}/src/layout/stats/ChartsGroup.tsx | 0 .../layout/stats/CollapsableTable.module.css | 0 .../src/layout/stats/CollapsableTable.tsx | 0 .../webapp}/src/layout/stats/Content.tsx | 0 .../webapp}/src/layout/stats/HeatMapChart.tsx | 0 .../stats/HorizontalBarChart.module.css | 0 .../src/layout/stats/HorizontalBarChart.tsx | 0 .../webapp}/src/layout/stats/Stats.module.css | 0 .../src/layout/stats/TimestampLineChart.tsx | 0 .../layout/stats/VerticalBarChart.module.css | 0 .../src/layout/stats/VerticalBarChart.tsx | 0 {web => ui/webapp}/src/layout/stats/index.tsx | 0 .../webapp}/src/layout/stores/activeItem.tsx | 0 .../src/layout/stores/financesData.tsx | 0 .../webapp}/src/layout/stores/fullData.tsx | 2 +- .../webapp}/src/layout/stores/gridWidth.tsx | 0 .../webapp}/src/layout/stores/groupActive.tsx | 3 +- .../webapp}/src/layout/stores/guideFile.tsx | 0 .../webapp}/src/layout/stores/mobileTOC.tsx | 0 .../src/layout/stores/upcomingEventData.tsx | 0 .../webapp}/src/layout/stores/viewMode.tsx | 0 .../src/layout/stores/visibleZoomSection.tsx | 0 {web => ui/webapp}/src/layout/stores/zoom.tsx | 0 .../upcomingEvents/UpcomingEvents.module.css | 0 .../src/layout/upcomingEvents/index.tsx | 0 {web => ui/webapp}/src/styles/cookies.css | 0 {web => ui/webapp}/src/styles/default.scss | 2 +- {web => ui/webapp}/src/styles/light.scss | 3 +- {web => ui/webapp}/src/styles/mixins.scss | 0 {web => ui/webapp}/src/types.ts | 0 .../webapp}/src/utils/calculateAxisValues.ts | 0 .../src/utils/calculateGridItemsPerRow.ts | 0 .../src/utils/calculateGridWidthInPx.ts | 0 .../src/utils/capitalizeFirstLetter.ts | 0 .../src/utils/checkIfCategoryInGroup.ts | 0 ui/webapp/src/utils/checkQueryStringValue.ts | 11 + {web => ui/webapp}/src/utils/cleanEmojis.ts | 0 .../webapp}/src/utils/countVisibleItems.tsx | 0 {web => ui/webapp}/src/utils/cutString.ts | 0 {web => ui/webapp}/src/utils/filterData.ts | 0 .../webapp}/src/utils/formatLabelProfit.ts | 0 .../webapp}/src/utils/generateColorsArray.ts | 0 ui/webapp/src/utils/getBasePath.ts | 5 + .../src/utils/getCategoriesWithItems.ts | 0 ui/webapp/src/utils/getFoundationNameLabel.ts | 8 + {web => ui/webapp}/src/utils/getGroupName.ts | 0 .../webapp}/src/utils/getItemDescription.ts | 0 {web => ui/webapp}/src/utils/getName.ts | 0 .../webapp}/src/utils/getNormalizedName.ts | 0 {web => ui/webapp}/src/utils/goToElement.ts | 0 .../webapp}/src/utils/gridCategoryLayout.ts | 0 .../webapp}/src/utils/isElementInView.ts | 0 .../webapp}/src/utils/isExploreSection.ts | 0 .../webapp}/src/utils/isSectionInGuide.ts | 0 .../webapp}/src/utils/itemsDataGetter.ts | 140 +++-- .../webapp}/src/utils/itemsIterator.tsx | 0 {web => ui/webapp}/src/utils/nestArray.ts | 0 ui/webapp/src/utils/overlayData.ts | 148 +++++ .../webapp}/src/utils/prepareFilters.ts | 0 .../webapp}/src/utils/prepareFinances.tsx | 0 ui/webapp/src/utils/prepareLink.ts | 13 + {web => ui/webapp}/src/utils/prepareMenu.ts | 0 {web => ui/webapp}/src/utils/prettifyBytes.ts | 0 .../webapp}/src/utils/prettifyNumber.ts | 0 {web => ui/webapp}/src/utils/rgba2hex.ts | 0 {web => ui/webapp}/src/utils/scrollToTop.ts | 0 {web => ui/webapp}/src/utils/sortItems.ts | 0 .../src/utils/sortItemsByOrderValue.ts | 0 .../webapp}/src/utils/sortMenuOptions.ts | 0 .../webapp}/src/utils/sortObjectByValue.ts | 0 {web => ui/webapp}/src/utils/sumValues.ts | 0 .../webapp}/src/utils/updateAlphaInColor.ts | 0 {web => ui/webapp}/src/vite-env.d.ts | 0 {web => ui/webapp}/src/window.d.ts | 3 +- {web => ui/webapp}/tsconfig.json | 2 +- {web => ui/webapp}/tsconfig.node.json | 0 {web => ui/webapp}/vite.config.ts | 0 {web => ui/webapp}/yarn.lock | 0 web/src/App.tsx | 92 --- web/src/layout/Layout.module.css | 3 - web/src/utils/getBasePath.ts | 5 - web/src/utils/getFoundationNameLabel.ts | 8 - 307 files changed, 2510 insertions(+), 1630 deletions(-) delete mode 100644 askama.toml delete mode 100644 build.rs create mode 100644 crates/cli/Cargo.toml rename Dockerfile => crates/cli/Dockerfile (77%) create mode 100644 crates/cli/askama.toml create mode 100644 crates/cli/build.rs rename {src => crates/cli/src}/build/api.rs (100%) rename {src => crates/cli/src}/build/cache.rs (100%) rename {src => crates/cli/src}/build/clomonitor.rs (100%) rename {src => crates/cli/src}/build/crunchbase.rs (58%) rename {src => crates/cli/src}/build/export.rs (100%) rename {src => crates/cli/src}/build/github.rs (73%) rename {src => crates/cli/src}/build/logos.rs (90%) rename {src => crates/cli/src}/build/mod.rs (86%) create mode 100644 crates/cli/src/build/projects.rs create mode 100644 crates/cli/src/deploy/mod.rs rename {src => crates/cli/src}/deploy/s3.rs (95%) create mode 100644 crates/cli/src/lib.rs create mode 100644 crates/cli/src/main.rs rename {src => crates/cli/src}/new/mod.rs (88%) rename {src => crates/cli/src}/new/template/data.yml (100%) rename {src => crates/cli/src}/new/template/guide.yml (100%) rename {src => crates/cli/src}/new/template/logos/cncf.svg (100%) rename {src => crates/cli/src}/new/template/settings.yml (100%) rename {src => crates/cli/src}/serve/mod.rs (77%) rename {src => crates/cli/src}/validate/mod.rs (50%) rename {templates => crates/cli/templates}/projects.md (100%) create mode 100644 crates/core/Cargo.toml rename {src/build => crates/core/src}/data.rs (63%) create mode 100644 crates/core/src/data/legacy.rs rename {src/build => crates/core/src}/datasets.rs (92%) rename {src/build => crates/core/src}/guide.rs (88%) create mode 100644 crates/core/src/lib.rs rename {src/build => crates/core/src}/settings.rs (93%) rename {src/build => crates/core/src}/stats.rs (90%) create mode 100644 crates/core/src/util.rs create mode 100644 crates/wasm/overlay/Cargo.toml create mode 100644 crates/wasm/overlay/overlay-test.html create mode 100644 crates/wasm/overlay/src/lib.rs delete mode 100644 src/build/projects.rs delete mode 100644 src/deploy/mod.rs delete mode 100644 src/main.rs rename {embed => ui/embed}/.eslintrc (100%) rename {embed => ui/embed}/.prettierignore (100%) rename {embed => ui/embed}/.prettierrc (100%) rename {embed => ui/embed}/embed.html (100%) rename {embed => ui/embed}/env.d.ts (100%) rename {embed => ui/embed}/package.json (100%) rename {embed => ui/embed}/public/assets/iframeResizer.contentWindow-v4.3.9.min.js (100%) rename {embed => ui/embed}/src/App.tsx (100%) rename {embed => ui/embed}/src/common/ExternalLink.tsx (100%) rename {embed => ui/embed}/src/common/GridItem.tsx (100%) rename {embed => ui/embed}/src/common/Image.tsx (100%) rename {embed => ui/embed}/src/common/Loading.tsx (100%) rename {embed => ui/embed}/src/common/NoData.tsx (100%) rename {embed => ui/embed}/src/common/StyleView.tsx (100%) rename {embed => ui/embed}/src/index.tsx (100%) rename {embed => ui/embed}/src/types.ts (100%) rename {embed => ui/embed}/src/utils/getUrl.tsx (100%) rename {embed => ui/embed}/src/vite-env.d.ts (100%) rename {embed => ui/embed}/src/window.d.ts (100%) rename {embed => ui/embed}/tsconfig.json (100%) rename {embed => ui/embed}/tsconfig.node.json (100%) rename {embed => ui/embed}/vite.config.ts (100%) rename {embed => ui/embed}/yarn.lock (100%) rename {web => ui/webapp}/.eslintrc (100%) rename {web => ui/webapp}/.gitignore (100%) rename {web => ui/webapp}/.prettierrc (100%) rename {web => ui/webapp}/index.html (100%) rename {web => ui/webapp}/package.json (100%) rename {web => ui/webapp}/public/assets/gtm.js (100%) rename {web => ui/webapp}/src/App.css (97%) create mode 100644 ui/webapp/src/App.module.css create mode 100644 ui/webapp/src/App.tsx rename {web => ui/webapp}/src/data.ts (75%) rename {web => ui/webapp}/src/env.d.ts (100%) rename {web => ui/webapp}/src/hooks/useBodyScroll.ts (100%) rename {web => ui/webapp}/src/hooks/useBreakpointDetect.tsx (100%) rename {web => ui/webapp}/src/hooks/useOutsideClick.ts (100%) rename {web => ui/webapp}/src/index.tsx (100%) create mode 100644 ui/webapp/src/layout/Layout.module.css rename {web => ui/webapp}/src/layout/common/ActiveFiltersList.module.css (100%) rename {web => ui/webapp}/src/layout/common/ActiveFiltersList.tsx (100%) rename {web => ui/webapp}/src/layout/common/Badge.module.css (100%) rename {web => ui/webapp}/src/layout/common/Badge.tsx (100%) rename {web => ui/webapp}/src/layout/common/ButtonCopyToClipboard.module.css (100%) rename {web => ui/webapp}/src/layout/common/ButtonCopyToClipboard.tsx (100%) rename {web => ui/webapp}/src/layout/common/ButtonToTopScroll.module.css (100%) rename {web => ui/webapp}/src/layout/common/ButtonToTopScroll.tsx (100%) rename {web => ui/webapp}/src/layout/common/Checkbox.module.css (100%) rename {web => ui/webapp}/src/layout/common/Checkbox.tsx (100%) rename {web => ui/webapp}/src/layout/common/CodeBlock.module.css (100%) rename {web => ui/webapp}/src/layout/common/CodeBlock.tsx (100%) rename {web => ui/webapp}/src/layout/common/CollapsableText.module.css (100%) rename {web => ui/webapp}/src/layout/common/CollapsableText.tsx (100%) rename {web => ui/webapp}/src/layout/common/DownloadDropdown.module.css (100%) rename {web => ui/webapp}/src/layout/common/DownloadDropdown.tsx (100%) rename {web => ui/webapp}/src/layout/common/ExternalLink.module.css (100%) rename {web => ui/webapp}/src/layout/common/ExternalLink.tsx (100%) rename {web => ui/webapp}/src/layout/common/FiltersInLine.module.css (100%) rename {web => ui/webapp}/src/layout/common/FiltersInLine.tsx (100%) rename {web => ui/webapp}/src/layout/common/FoundationBadge.tsx (76%) rename {web => ui/webapp}/src/layout/common/FullScreenModal.module.css (100%) rename {web => ui/webapp}/src/layout/common/FullScreenModal.tsx (100%) rename {web => ui/webapp}/src/layout/common/HoverableItem.tsx (100%) rename {web => ui/webapp}/src/layout/common/Image.tsx (55%) rename {web => ui/webapp}/src/layout/common/Loading.module.css (100%) rename {web => ui/webapp}/src/layout/common/Loading.tsx (75%) rename {web => ui/webapp}/src/layout/common/MaturityBadge.module.css (100%) rename {web => ui/webapp}/src/layout/common/MaturityBadge.tsx (100%) rename {web => ui/webapp}/src/layout/common/Modal.module.css (100%) rename {web => ui/webapp}/src/layout/common/Modal.tsx (100%) rename {web => ui/webapp}/src/layout/common/NoData.module.css (100%) rename {web => ui/webapp}/src/layout/common/NoData.tsx (70%) rename {web => ui/webapp}/src/layout/common/Pagination.module.css (100%) rename {web => ui/webapp}/src/layout/common/Pagination.tsx (100%) rename {web => ui/webapp}/src/layout/common/SVGIcon.tsx (99%) rename {web => ui/webapp}/src/layout/common/Searchbar.module.css (100%) rename {web => ui/webapp}/src/layout/common/Searchbar.tsx (96%) rename {web => ui/webapp}/src/layout/common/Section.module.css (100%) rename {web => ui/webapp}/src/layout/common/Section.tsx (100%) rename {web => ui/webapp}/src/layout/common/Sidebar.module.css (100%) rename {web => ui/webapp}/src/layout/common/Sidebar.tsx (100%) rename {web => ui/webapp}/src/layout/common/Tabs.module.css (100%) rename {web => ui/webapp}/src/layout/common/Tabs.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/AcquisitionsTable.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/AcquisitionsTable.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/BadgeModal.tsx (96%) rename {web => ui/webapp}/src/layout/common/itemModal/Box.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/Box.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/Content.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/Content.tsx (98%) rename {web => ui/webapp}/src/layout/common/itemModal/FundingRoundsTable.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/FundingRoundsTable.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/ItemDropdown.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/ItemDropdown.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/ItemModal.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/LanguagesStats.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/LanguagesStats.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/MaturitySection.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/MaturitySection.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/MobileContent.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/MobileContent.tsx (99%) rename {web => ui/webapp}/src/layout/common/itemModal/MobileMaturitySection.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/MobileMaturitySection.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/ParentProject.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/ParentProject.tsx (95%) rename {web => ui/webapp}/src/layout/common/itemModal/ParticipationStats.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/ParticipationStats.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/RepositoriesSection.module.css (100%) rename {web => ui/webapp}/src/layout/common/itemModal/RepositoriesSection.tsx (100%) rename {web => ui/webapp}/src/layout/common/itemModal/index.tsx (100%) rename {web => ui/webapp}/src/layout/common/zoomModal/ZoomModal.module.css (100%) rename {web => ui/webapp}/src/layout/common/zoomModal/index.tsx (100%) rename {web => ui/webapp}/src/layout/explore/Content.tsx (100%) rename {web => ui/webapp}/src/layout/explore/Explore.module.css (100%) rename {web => ui/webapp}/src/layout/explore/card/Card.module.css (100%) rename {web => ui/webapp}/src/layout/explore/card/Card.tsx (100%) rename {web => ui/webapp}/src/layout/explore/card/CardCategory.module.css (100%) rename {web => ui/webapp}/src/layout/explore/card/CardTitle.module.css (100%) rename {web => ui/webapp}/src/layout/explore/card/CardTitle.tsx (100%) rename {web => ui/webapp}/src/layout/explore/card/Content.module.css (100%) rename {web => ui/webapp}/src/layout/explore/card/Content.tsx (100%) rename {web => ui/webapp}/src/layout/explore/card/Menu.module.css (100%) rename {web => ui/webapp}/src/layout/explore/card/Menu.tsx (100%) rename {web => ui/webapp}/src/layout/explore/card/index.tsx (100%) rename {web => ui/webapp}/src/layout/explore/filters/Filters.module.css (100%) rename {web => ui/webapp}/src/layout/explore/filters/SearchbarSection.module.css (100%) rename {web => ui/webapp}/src/layout/explore/filters/SearchbarSection.tsx (100%) rename {web => ui/webapp}/src/layout/explore/filters/index.tsx (100%) rename {web => ui/webapp}/src/layout/explore/grid/Grid.module.css (100%) rename {web => ui/webapp}/src/layout/explore/grid/Grid.tsx (98%) rename {web => ui/webapp}/src/layout/explore/grid/GridCategory.module.css (100%) rename {web => ui/webapp}/src/layout/explore/grid/GridItem.module.css (100%) rename {web => ui/webapp}/src/layout/explore/grid/GridItem.tsx (100%) rename {web => ui/webapp}/src/layout/explore/grid/index.tsx (95%) rename {web => ui/webapp}/src/layout/explore/index.tsx (100%) rename {web => ui/webapp}/src/layout/explore/mobile/Card.module.css (100%) rename {web => ui/webapp}/src/layout/explore/mobile/Card.tsx (100%) rename {web => ui/webapp}/src/layout/explore/mobile/ExploreMobileIndex.module.css (100%) rename {web => ui/webapp}/src/layout/explore/mobile/ExploreMobileIndex.tsx (100%) rename {web => ui/webapp}/src/layout/explore/mobile/MobileGrid.module.css (100%) rename {web => ui/webapp}/src/layout/explore/mobile/MobileGrid.tsx (100%) rename {web => ui/webapp}/src/layout/finances/Finances.module.css (100%) rename {web => ui/webapp}/src/layout/finances/MobileFilters.module.css (100%) rename {web => ui/webapp}/src/layout/finances/MobileFilters.tsx (100%) rename {web => ui/webapp}/src/layout/finances/index.tsx (99%) rename {web => ui/webapp}/src/layout/guide/Guide.module.css (100%) rename {web => ui/webapp}/src/layout/guide/SubcategoryExtended.module.css (100%) rename {web => ui/webapp}/src/layout/guide/SubcategoryExtended.tsx (97%) rename {web => ui/webapp}/src/layout/guide/SubcategoryGrid.module.css (100%) rename {web => ui/webapp}/src/layout/guide/SubcategoryGrid.tsx (100%) rename {web => ui/webapp}/src/layout/guide/ToC.module.css (100%) rename {web => ui/webapp}/src/layout/guide/ToC.tsx (100%) rename {web => ui/webapp}/src/layout/guide/index.tsx (95%) rename {web => ui/webapp}/src/layout/index.tsx (100%) rename {web => ui/webapp}/src/layout/logos/Logos.module.css (100%) rename {web => ui/webapp}/src/layout/logos/index.tsx (100%) rename {web => ui/webapp}/src/layout/navigation/EmbedModal.module.css (100%) rename {web => ui/webapp}/src/layout/navigation/EmbedModal.tsx (99%) rename {web => ui/webapp}/src/layout/navigation/Footer.module.css (100%) rename {web => ui/webapp}/src/layout/navigation/Footer.tsx (87%) rename {web => ui/webapp}/src/layout/navigation/Header.module.css (93%) rename {web => ui/webapp}/src/layout/navigation/Header.tsx (87%) rename {web => ui/webapp}/src/layout/navigation/MiniFooter.tsx (100%) rename {web => ui/webapp}/src/layout/navigation/MobileDropdown.module.css (100%) rename {web => ui/webapp}/src/layout/navigation/MobileDropdown.tsx (100%) rename {web => ui/webapp}/src/layout/navigation/MobileHeader.module.css (95%) rename {web => ui/webapp}/src/layout/navigation/MobileHeader.tsx (82%) rename {web => ui/webapp}/src/layout/notFound/NotFound.module.css (100%) rename {web => ui/webapp}/src/layout/notFound/index.tsx (100%) rename {web => ui/webapp}/src/layout/screenshots/Grid.module.css (100%) rename {web => ui/webapp}/src/layout/screenshots/Grid.tsx (100%) rename {web => ui/webapp}/src/layout/screenshots/Screenshots.module.css (100%) rename {web => ui/webapp}/src/layout/screenshots/index.tsx (100%) rename {web => ui/webapp}/src/layout/stats/Box.module.css (100%) rename {web => ui/webapp}/src/layout/stats/Box.tsx (100%) rename {web => ui/webapp}/src/layout/stats/ChartsGroup.tsx (100%) rename {web => ui/webapp}/src/layout/stats/CollapsableTable.module.css (100%) rename {web => ui/webapp}/src/layout/stats/CollapsableTable.tsx (100%) rename {web => ui/webapp}/src/layout/stats/Content.tsx (100%) rename {web => ui/webapp}/src/layout/stats/HeatMapChart.tsx (100%) rename {web => ui/webapp}/src/layout/stats/HorizontalBarChart.module.css (100%) rename {web => ui/webapp}/src/layout/stats/HorizontalBarChart.tsx (100%) rename {web => ui/webapp}/src/layout/stats/Stats.module.css (100%) rename {web => ui/webapp}/src/layout/stats/TimestampLineChart.tsx (100%) rename {web => ui/webapp}/src/layout/stats/VerticalBarChart.module.css (100%) rename {web => ui/webapp}/src/layout/stats/VerticalBarChart.tsx (100%) rename {web => ui/webapp}/src/layout/stats/index.tsx (100%) rename {web => ui/webapp}/src/layout/stores/activeItem.tsx (100%) rename {web => ui/webapp}/src/layout/stores/financesData.tsx (100%) rename {web => ui/webapp}/src/layout/stores/fullData.tsx (92%) rename {web => ui/webapp}/src/layout/stores/gridWidth.tsx (100%) rename {web => ui/webapp}/src/layout/stores/groupActive.tsx (94%) rename {web => ui/webapp}/src/layout/stores/guideFile.tsx (100%) rename {web => ui/webapp}/src/layout/stores/mobileTOC.tsx (100%) rename {web => ui/webapp}/src/layout/stores/upcomingEventData.tsx (100%) rename {web => ui/webapp}/src/layout/stores/viewMode.tsx (100%) rename {web => ui/webapp}/src/layout/stores/visibleZoomSection.tsx (100%) rename {web => ui/webapp}/src/layout/stores/zoom.tsx (100%) rename {web => ui/webapp}/src/layout/upcomingEvents/UpcomingEvents.module.css (100%) rename {web => ui/webapp}/src/layout/upcomingEvents/index.tsx (100%) rename {web => ui/webapp}/src/styles/cookies.css (100%) rename {web => ui/webapp}/src/styles/default.scss (98%) rename {web => ui/webapp}/src/styles/light.scss (97%) rename {web => ui/webapp}/src/styles/mixins.scss (100%) rename {web => ui/webapp}/src/types.ts (100%) rename {web => ui/webapp}/src/utils/calculateAxisValues.ts (100%) rename {web => ui/webapp}/src/utils/calculateGridItemsPerRow.ts (100%) rename {web => ui/webapp}/src/utils/calculateGridWidthInPx.ts (100%) rename {web => ui/webapp}/src/utils/capitalizeFirstLetter.ts (100%) rename {web => ui/webapp}/src/utils/checkIfCategoryInGroup.ts (100%) create mode 100644 ui/webapp/src/utils/checkQueryStringValue.ts rename {web => ui/webapp}/src/utils/cleanEmojis.ts (100%) rename {web => ui/webapp}/src/utils/countVisibleItems.tsx (100%) rename {web => ui/webapp}/src/utils/cutString.ts (100%) rename {web => ui/webapp}/src/utils/filterData.ts (100%) rename {web => ui/webapp}/src/utils/formatLabelProfit.ts (100%) rename {web => ui/webapp}/src/utils/generateColorsArray.ts (100%) create mode 100644 ui/webapp/src/utils/getBasePath.ts rename {web => ui/webapp}/src/utils/getCategoriesWithItems.ts (100%) create mode 100644 ui/webapp/src/utils/getFoundationNameLabel.ts rename {web => ui/webapp}/src/utils/getGroupName.ts (100%) rename {web => ui/webapp}/src/utils/getItemDescription.ts (100%) rename {web => ui/webapp}/src/utils/getName.ts (100%) rename {web => ui/webapp}/src/utils/getNormalizedName.ts (100%) rename {web => ui/webapp}/src/utils/goToElement.ts (100%) rename {web => ui/webapp}/src/utils/gridCategoryLayout.ts (100%) rename {web => ui/webapp}/src/utils/isElementInView.ts (100%) rename {web => ui/webapp}/src/utils/isExploreSection.ts (100%) rename {web => ui/webapp}/src/utils/isSectionInGuide.ts (100%) rename {web => ui/webapp}/src/utils/itemsDataGetter.ts (86%) rename {web => ui/webapp}/src/utils/itemsIterator.tsx (100%) rename {web => ui/webapp}/src/utils/nestArray.ts (100%) create mode 100644 ui/webapp/src/utils/overlayData.ts rename {web => ui/webapp}/src/utils/prepareFilters.ts (100%) rename {web => ui/webapp}/src/utils/prepareFinances.tsx (100%) create mode 100644 ui/webapp/src/utils/prepareLink.ts rename {web => ui/webapp}/src/utils/prepareMenu.ts (100%) rename {web => ui/webapp}/src/utils/prettifyBytes.ts (100%) rename {web => ui/webapp}/src/utils/prettifyNumber.ts (100%) rename {web => ui/webapp}/src/utils/rgba2hex.ts (100%) rename {web => ui/webapp}/src/utils/scrollToTop.ts (100%) rename {web => ui/webapp}/src/utils/sortItems.ts (100%) rename {web => ui/webapp}/src/utils/sortItemsByOrderValue.ts (100%) rename {web => ui/webapp}/src/utils/sortMenuOptions.ts (100%) rename {web => ui/webapp}/src/utils/sortObjectByValue.ts (100%) rename {web => ui/webapp}/src/utils/sumValues.ts (100%) rename {web => ui/webapp}/src/utils/updateAlphaInColor.ts (100%) rename {web => ui/webapp}/src/vite-env.d.ts (100%) rename {web => ui/webapp}/src/window.d.ts (75%) rename {web => ui/webapp}/tsconfig.json (94%) rename {web => ui/webapp}/tsconfig.node.json (100%) rename {web => ui/webapp}/vite.config.ts (100%) rename {web => ui/webapp}/yarn.lock (100%) delete mode 100644 web/src/App.tsx delete mode 100644 web/src/layout/Layout.module.css delete mode 100644 web/src/utils/getBasePath.ts delete mode 100644 web/src/utils/getFoundationNameLabel.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6b7b16b7..93bb337a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,7 +16,7 @@ updates: - "patch" - package-ecosystem: "npm" - directory: "/web" + directory: "/ui" schedule: interval: "weekly" groups: diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index 0a177860..4926cb1d 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -21,5 +21,5 @@ jobs: password: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: Build and push image run: | - docker build -f Dockerfile -t public.ecr.aws/g6m3a0y9/landscape2:latest . + docker build -f crates/cli/Dockerfile -t public.ecr.aws/g6m3a0y9/landscape2:latest . docker push public.ecr.aws/g6m3a0y9/landscape2:latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9625aeb2..1bc90fe8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,10 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@master with: - toolchain: 1.76.0 + toolchain: 1.77.0 components: clippy, rustfmt + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Run clippy run: cargo clippy --all-targets --all-features -- --deny warnings - name: Run rustfmt @@ -36,16 +38,16 @@ jobs: - name: Cache node modules uses: actions/cache@v4 with: - path: ./web/node_modules + path: ./ui/webapp/node_modules key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - name: Install dependencies - working-directory: ./web + working-directory: ./ui/webapp run: yarn install --network-concurrency 1 - name: Run prettier - working-directory: ./web + working-directory: ./ui/webapp run: yarn format:diff - name: Run eslint - working-directory: ./web + working-directory: ./ui/webapp run: yarn lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09a4cd9d..72d4dfc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# Copyright 2022-2023, axodotdev +# Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: @@ -6,9 +6,9 @@ # * checks for a Git Tag that looks like a release # * builds artifacts with cargo-dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip -# * on success, uploads the artifacts to a Github Release +# * on success, uploads the artifacts to a GitHub Release # -# Note that the Github Release will be created with a generated +# Note that the GitHub Release will be created with a generated # title/body based on your changelogs. name: Release @@ -31,7 +31,7 @@ permissions: # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will -# spin up, creating an independent announcement for each one. However Github +# spin up, creating an independent announcement for each one. However, GitHub # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # @@ -61,7 +61,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.2/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. @@ -104,10 +104,15 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true - uses: actions/checkout@v4 with: submodules: recursive - uses: swatinem/rust-cache@v2 + with: + key: ${{ join(matrix.targets, '-') }} - name: Install cargo-dist run: ${{ matrix.install_dist }} # Get the dist-manifest @@ -134,7 +139,7 @@ jobs: run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -161,7 +166,7 @@ jobs: submodules: recursive - name: Install cargo-dist shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.2/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -177,7 +182,7 @@ jobs: # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" - jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json >> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" @@ -206,7 +211,7 @@ jobs: with: submodules: recursive - name: Install cargo-dist - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.12.2/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 @@ -214,7 +219,7 @@ jobs: pattern: artifacts-* path: target/distrib/ merge-multiple: true - # This is a harmless no-op for Github Releases, hosting for that happens in "announce" + # This is a harmless no-op for GitHub Releases, hosting for that happens in "announce" - id: host shell: bash run: | @@ -269,7 +274,7 @@ jobs: done git push - # Create a Github Release while uploading all files to it + # Create a GitHub Release while uploading all files to it announce: needs: - plan @@ -286,7 +291,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: "Download Github Artifacts" + - name: "Download GitHub Artifacts" uses: actions/download-artifact@v4 with: pattern: artifacts-* @@ -296,7 +301,7 @@ jobs: run: | # Remove the granular manifests rm -f artifacts/*-dist-manifest.json - - name: Create Github Release + - name: Create GitHub Release uses: ncipollo/release-action@v1 with: tag: ${{ needs.plan.outputs.tag }} diff --git a/.gitignore b/.gitignore index 13c4d0ba..f2e4eb07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,17 @@ .DS_Store .idea .vscode +crates/wasm/overlay/data +crates/wasm/overlay/dist +crates/wasm/overlay/sources npm-debug.log* +target +target-wasm +ui/embed/dist +ui/embed/node_modules +ui/embed/static/* +ui/webapp/dist +ui/webapp/node_modules +ui/webapp/static/* yarn-debug.log* yarn-error.log* -target -web/node_modules -web/dist -web/static/* -embed/node_modules -embed/dist -embed/static/* diff --git a/Cargo.lock b/Cargo.lock index e2229a4c..43a75199 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1803,9 +1803,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1855,9 +1855,9 @@ dependencies = [ "hex", "imagesize", "itertools 0.12.1", + "landscape2-core", "lazy_static", "leaky-bucket", - "markdown", "md-5", "mime_guess", "mockall", @@ -1870,7 +1870,6 @@ dependencies = [ "rust-embed", "serde", "serde_json", - "serde_yaml", "sha2", "tokio", "tower", @@ -1883,6 +1882,38 @@ dependencies = [ "which 6.0.1", ] +[[package]] +name = "landscape2-core" +version = "0.8.1" +dependencies = [ + "anyhow", + "chrono", + "clap", + "itertools 0.12.1", + "lazy_static", + "markdown", + "regex", + "reqwest 0.12.2", + "serde", + "serde_yaml", + "tracing", + "url", +] + +[[package]] +name = "landscape2-overlay" +version = "0.8.1" +dependencies = [ + "anyhow", + "landscape2-core", + "reqwest 0.12.2", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2220,9 +2251,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.6+3.1.4" +version = "300.2.3+3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" dependencies = [ "cc", ] @@ -3027,6 +3058,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.197" @@ -3915,9 +3957,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3925,9 +3967,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -3940,9 +3982,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -3952,9 +3994,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3962,9 +4004,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -3975,9 +4017,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-timer" @@ -3996,9 +4038,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index d57e8b78..325901c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,20 @@ -[package] -name = "landscape2" -description = "Landscape2 CLI tool" +[workspace] +resolver = "2" +members = [ + "crates/cli", + "crates/core", + "crates/wasm/overlay" +] + +[workspace.package] version = "0.8.1" license = "Apache-2.0" edition = "2021" -rust-version = "1.70" -repository = "https://github.com/cncf/landscape2" -readme = "README.md" +rust-version = "1.77" authors = ["Sergio Castaño Arteaga", "Cintia Sanchez Garcia"] +homepage = "https://github.com/cncf/landscape2" -[dependencies] +[workspace.dependencies] anyhow = "1.0.81" askama = { version = "0.12.1", features = ["serde-json"] } askama_escape = { version = "0.10.3", features = ["json"] } @@ -33,6 +38,7 @@ leaky-bucket = "1.0.1" markdown = "1.0.0-alpha.16" md-5 = "0.10.6" mime_guess = "2.0.4" +mockall = "0.12.1" num_cpus = "1.16.0" octorust = "0.7.0" parse_link_header = "0.3.3" @@ -42,6 +48,7 @@ reqwest = { version = "0.12.2", features = ["json", "native-tls-vendored"] } rust-embed = "8.3.0" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" +serde-wasm-bindgen = "0.6.5" serde_yaml = "0.9.34" sha2 = "0.10.8" tokio = { version = "1.37.0", features = [ @@ -58,14 +65,14 @@ tower-http = { version = "0.5.2", features = ["fs", "set-header"] } url = "2.5.0" usvg = "0.37.0" walkdir = "2.5.0" - -[dev-dependencies] -mockall = "0.12.1" - -[build-dependencies] -anyhow = "1.0.81" +wasm-bindgen = "0.2.92" +wasm-bindgen-futures = "0.4.42" +web-sys = { version = "0.3.69", features = ["console"] } which = "6.0.1" +[profile.release] +lto = true + # The profile that 'cargo dist' will build with [profile.dist] inherits = "release" @@ -74,7 +81,7 @@ lto = "thin" # Config for 'cargo dist' [workspace.metadata.dist] # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.12.2" +cargo-dist-version = "0.13.3" # CI backends to support ci = ["github"] # The installers to generate for each app diff --git a/README.md b/README.md index e4dd72e5..99302327 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ The landscape2 CLI tool is also distributed in a [container image](https://galle ### Building from source -You can build **landscape2** from the source by using [Cargo](https://rustup.rs), the Rust package manager. [yarn](https://classic.yarnpkg.com/lang/en/docs/install/) is required during the installation process to build the web application, which will be embedded into the `landscape2` binary as part of the build process. +You can build **landscape2** from the source by using [Cargo](https://rustup.rs), the Rust package manager. [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) is required to build the wasm modules. [yarn](https://classic.yarnpkg.com/lang/en/docs/install/) is also required during the installation process to build the web application, which will be embedded into the `landscape2` binary as part of the build process. ```text cargo install --git https://github.com/cncf/landscape2 diff --git a/askama.toml b/askama.toml deleted file mode 100644 index 11ed5625..00000000 --- a/askama.toml +++ /dev/null @@ -1,2 +0,0 @@ -[general] -dirs = ["templates", "web/dist"] diff --git a/build.rs b/build.rs deleted file mode 100644 index 1bbac29e..00000000 --- a/build.rs +++ /dev/null @@ -1,51 +0,0 @@ -use anyhow::{bail, Result}; -use std::process::Command; -use which::which; - -fn main() -> Result<()> { - // Tell Cargo to rerun this build script if the source changes - println!("cargo:rerun-if-changed=embed/src"); - println!("cargo:rerun-if-changed=embed/embed.html"); - println!("cargo:rerun-if-changed=web/src"); - println!("cargo:rerun-if-changed=web/static"); - println!("cargo:rerun-if-changed=web/index.html"); - - // Check if required external tools are available - if which("yarn").is_err() { - bail!("yarn not found in PATH (it is required to build the web application)"); - } - - // Build embeddable views - yarn(&["--cwd", "embed", "install"])?; - yarn(&["--cwd", "embed", "build"])?; - - // Build web application - yarn(&["--cwd", "web", "install"])?; - yarn(&["--cwd", "web", "build"])?; - - Ok(()) -} - -/// Run yarn command with the provided arguments. -fn yarn(args: &[&str]) -> Result<()> { - // Setup command based on the target OS - let mut cmd; - if cfg!(target_os = "windows") { - cmd = Command::new("cmd"); - cmd.args(["/C", "yarn"]); - } else { - cmd = Command::new("yarn"); - } - cmd.args(args); - - // Run command and check output - let output = cmd.output()?; - if !output.status.success() { - bail!( - "\n\n> {cmd:?} (stderr)\n{}\n> {cmd:?} (stdout)\n{}\n", - String::from_utf8(output.stderr)?, - String::from_utf8(output.stdout)? - ); - } - Ok(()) -} diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml new file mode 100644 index 00000000..b8a9a3fa --- /dev/null +++ b/crates/cli/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "landscape2" +description = "Landscape2 CLI tool" +repository = "https://github.com/cncf/landscape2" +readme = "../../README.md" +version.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true + +[dependencies] +anyhow = { workspace = true } +askama = { workspace = true } +askama_escape = { workspace = true } +async-trait = { workspace = true } +aws-config = { workspace = true } +aws-sdk-s3 = { workspace = true } +axum = { workspace = true } +base64 = { workspace = true } +chrono = { workspace = true } +clap = { workspace = true } +csv = { workspace = true } +deadpool = { workspace = true } +dirs = { workspace = true } +futures = { workspace = true } +headless_chrome = { workspace = true } +hex = { workspace = true } +imagesize = { workspace = true } +itertools = { workspace = true } +landscape2-core = { path = "../core" } +lazy_static = { workspace = true } +leaky-bucket = { workspace = true } +md-5 = { workspace = true } +mime_guess = { workspace = true } +num_cpus = { workspace = true } +octorust = { workspace = true } +parse_link_header = { workspace = true } +qrcode = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true } +rust-embed = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true } +url = { workspace = true } +usvg = { workspace = true } +walkdir = { workspace = true } + +[dev-dependencies] +mockall = { workspace = true } + +[build-dependencies] +anyhow = { workspace = true } +which = { workspace = true } diff --git a/Dockerfile b/crates/cli/Dockerfile similarity index 77% rename from Dockerfile rename to crates/cli/Dockerfile index fcc85b3f..649e3c62 100644 --- a/Dockerfile +++ b/crates/cli/Dockerfile @@ -1,15 +1,11 @@ # Build CLI tool FROM rust:1-alpine3.19 as builder -RUN apk --no-cache add musl-dev perl make libconfig-dev openssl-dev yarn +RUN apk --no-cache add musl-dev perl make libconfig-dev openssl-dev yarn wasm-pack binaryen WORKDIR /landscape2 -COPY embed embed -COPY src src -COPY templates templates -COPY web web -COPY build.rs ./ -COPY askama.toml ./ +COPY crates crates +COPY ui ui COPY Cargo.* ./ -WORKDIR /landscape2/src +WORKDIR /landscape2/crates/cli RUN cargo build --release # Final stage diff --git a/crates/cli/askama.toml b/crates/cli/askama.toml new file mode 100644 index 00000000..3472fb03 --- /dev/null +++ b/crates/cli/askama.toml @@ -0,0 +1,2 @@ +[general] +dirs = ["templates", "../../ui/webapp/dist"] diff --git a/crates/cli/build.rs b/crates/cli/build.rs new file mode 100644 index 00000000..d0312aad --- /dev/null +++ b/crates/cli/build.rs @@ -0,0 +1,112 @@ +use anyhow::{bail, Result}; +use std::{ + path::{Path, PathBuf}, + process::Command, +}; +use which::which; + +fn main() -> Result<()> { + // Tell Cargo to rerun this build script if the source changes + println!("cargo:rerun-if-changed=../wasm/overlay"); + println!("cargo:rerun-if-changed=../../ui/embed/src"); + println!("cargo:rerun-if-changed=../../ui/embed/embed.html"); + println!("cargo:rerun-if-changed=../../ui/webapp/src"); + println!("cargo:rerun-if-changed=../../ui/webapp/static"); + println!("cargo:rerun-if-changed=../../ui/webapp/index.html"); + + // Check if required external tools are available + if which("cargo").is_err() { + bail!("cargo not found in PATH (required)"); + } + if which("wasm-pack").is_err() { + bail!("wasm-pack not found in PATH (required to build the wasm modules)"); + } + if which("yarn").is_err() { + bail!("yarn not found in PATH (required to build the web application)"); + } + + // Build overlay wasm module + let mut wasm_profile = "--dev"; + if let Ok(profile) = std::env::var("PROFILE") { + if profile == "release" { + wasm_profile = "--release"; + } + }; + let wasm_target_dir = workspace_dir()?.join("target-wasm").to_string_lossy().to_string(); + run( + "wasm-pack", + &[ + "build", + "--target", + "web", + "--out-dir", + "../../../ui/webapp/wasm/overlay", + wasm_profile, + "../wasm/overlay", + "--target-dir", + &wasm_target_dir, + ], + )?; + + // Build embed + run("yarn", &["--cwd", "../../ui/embed", "install"])?; + run("yarn", &["--cwd", "../../ui/embed", "build"])?; + + // Build web application + run("yarn", &["--cwd", "../../ui/webapp", "install"])?; + run("yarn", &["--cwd", "../../ui/webapp", "build"])?; + + Ok(()) +} + +/// Return workspace directory. +fn workspace_dir() -> Result { + // Run `cargo locate-project` command to get workspace directory + let mut cmd = new_cmd("cargo"); + cmd.args(["locate-project", "--workspace", "--message-format=plain"]); + let output = cmd.output()?; + if !output.status.success() { + bail!( + "error getting workspace directory: {}", + String::from_utf8(output.stderr)? + ); + } + + // Extract workspace directory from `cargo locate-project` output + let workspace_dir = Path::new(String::from_utf8(output.stdout)?.trim()) + .parent() + .expect("parent to exist") + .to_path_buf(); + + Ok(workspace_dir) +} + +/// Helper function to run a command. +fn run(program: &str, args: &[&str]) -> Result<()> { + // Setup command + let mut cmd = new_cmd(program); + cmd.args(args); + + // Execute it and check output + let output = cmd.output()?; + if !output.status.success() { + bail!( + "\n\n> {cmd:?} (stderr)\n{}\n> {cmd:?} (stdout)\n{}\n", + String::from_utf8(output.stderr)?, + String::from_utf8(output.stdout)? + ); + } + + Ok(()) +} + +/// Helper function to setup a command based on the target OS. +fn new_cmd(program: &str) -> Command { + if cfg!(target_os = "windows") { + let mut cmd = Command::new("cmd"); + cmd.args(["/C", program]); + cmd + } else { + Command::new(program) + } +} diff --git a/src/build/api.rs b/crates/cli/src/build/api.rs similarity index 100% rename from src/build/api.rs rename to crates/cli/src/build/api.rs diff --git a/src/build/cache.rs b/crates/cli/src/build/cache.rs similarity index 100% rename from src/build/cache.rs rename to crates/cli/src/build/cache.rs diff --git a/src/build/clomonitor.rs b/crates/cli/src/build/clomonitor.rs similarity index 100% rename from src/build/clomonitor.rs rename to crates/cli/src/build/clomonitor.rs diff --git a/src/build/crunchbase.rs b/crates/cli/src/build/crunchbase.rs similarity index 58% rename from src/build/crunchbase.rs rename to crates/cli/src/build/crunchbase.rs index 6a1d1bc3..a508c6dc 100644 --- a/src/build/crunchbase.rs +++ b/crates/cli/src/build/crunchbase.rs @@ -5,8 +5,9 @@ use super::{cache::Cache, LandscapeData}; use anyhow::{bail, format_err, Result}; use async_trait::async_trait; -use chrono::{DateTime, Datelike, NaiveDate, Utc}; +use chrono::{Datelike, NaiveDate, Utc}; use futures::stream::{self, StreamExt}; +use landscape2_core::data::{Acquisition, CrunchbaseData, FundingRound, Organization}; use lazy_static::lazy_static; use leaky_bucket::RateLimiter; #[cfg(test)] @@ -92,7 +93,7 @@ pub(crate) async fn collect_crunchbase_data( // Otherwise we pull it from Crunchbase if a key was provided else if let Some(cb) = cb.clone() { limiter.acquire_one().await; - (url.clone(), Organization::new(cb, &url).await) + (url.clone(), collect_organization_data(cb, &url).await) } else { (url.clone(), Err(format_err!("no api key provided"))) } @@ -120,198 +121,80 @@ pub(crate) async fn collect_crunchbase_data( Ok(crunchbase_data) } -/// Type alias to represent some organizations' Crunchbase data. -pub(crate) type CrunchbaseData = BTreeMap; - -/// Type alias to represent a crunchbase url. -type CrunchbaseUrl = String; - -/// Organization information collected from Crunchbase. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Organization { - pub generated_at: DateTime, - - #[serde(skip_serializing_if = "Option::is_none")] - pub acquisitions: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub city: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub company_type: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub country: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub funding: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub funding_rounds: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub homepage_url: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub categories: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub kind: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub linkedin_url: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub num_employees_max: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub num_employees_min: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub region: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub stock_exchange: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ticker: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub twitter_url: Option, -} - -impl Organization { - /// Create a new Organization instance from information obtained from the - /// Crunchbase API. - async fn new(cb: DynCB, cb_url: &str) -> Result { - // Collect some information from Crunchbase - let permalink = get_permalink(cb_url)?; - let cb_org = cb.get_organization(&permalink).await?; - - // Prepare acquisitions - let acquisitions = cb_org - .cards - .acquiree_acquisitions - .map(|cb_acquisitions| { - cb_acquisitions - .into_iter() - .map(Into::into) - .filter(|a: &Acquisition| { - if let Some(announced_on) = a.announced_on { - // Only acquisitions done in the last 6 years - if Utc::now().year() - announced_on.year() < 6 { - return true; - } +/// Collect organization data from Crunchbase. +#[instrument(skip_all, err)] +async fn collect_organization_data(cb: DynCB, cb_url: &str) -> Result { + // Collect some information from Crunchbase + let permalink = get_permalink(cb_url)?; + let cb_org = cb.get_organization(&permalink).await?; + + // Prepare acquisitions + let acquisitions = cb_org + .cards + .acquiree_acquisitions + .map(|cb_acquisitions| { + cb_acquisitions + .into_iter() + .map(new_acquisition_from) + .filter(|a: &Acquisition| { + if let Some(announced_on) = a.announced_on { + // Only acquisitions done in the last 6 years + if Utc::now().year() - announced_on.year() < 6 { + return true; } - false - }) - .collect() - }) - .filter(|vec: &Vec| !vec.is_empty()); - - // Prepare funding rounds - let funding_rounds = cb_org - .cards - .raised_funding_rounds - .map(|cb_funding_rounds| cb_funding_rounds.into_iter().map(Into::into).collect()) - .filter(|vec: &Vec| !vec.is_empty()); - - // Prepare number of employees - let (num_employees_min, num_employees_max) = match cb_org.properties.num_employees_enum { - Some(value) => match value.as_str() { - "c_00001_00010" => (Some(1), Some(10)), - "c_00011_00050" => (Some(11), Some(50)), - "c_00051_00100" => (Some(51), Some(100)), - "c_00101_00250" => (Some(101), Some(250)), - "c_00251_00500" => (Some(251), Some(500)), - "c_00501_01000" => (Some(501), Some(1000)), - "c_01001_05000" => (Some(1001), Some(5000)), - "c_05001_10000" => (Some(5001), Some(10000)), - "c_10001_max" => (Some(10001), None), - _ => (None, None), - }, - None => (None, None), - }; - - // Prepare organization instance using the information collected - Ok(Organization { - generated_at: Utc::now(), - acquisitions, - city: get_location_value(&cb_org.cards.headquarters_address, "city"), - company_type: cb_org.properties.company_type, - country: get_location_value(&cb_org.cards.headquarters_address, "country"), - description: cb_org.properties.short_description, - funding: cb_org.properties.funding_total.as_ref().and_then(|f| f.value_usd), - funding_rounds, - homepage_url: cb_org.properties.website.and_then(|v| v.value), - categories: cb_org.properties.categories.and_then(|c| c.into_iter().map(|c| c.value).collect()), - kind: cb_org.properties.funding_total.map(|_| "funding".to_string()), - linkedin_url: cb_org.properties.linkedin.and_then(|v| v.value), - name: cb_org.properties.name, - num_employees_max, - num_employees_min, - region: get_location_value(&cb_org.cards.headquarters_address, "region"), - stock_exchange: cb_org.properties.stock_exchange_symbol, - ticker: cb_org.properties.stock_symbol.and_then(|v| v.value), - twitter_url: cb_org.properties.twitter.and_then(|v| v.value), + } + false + }) + .collect() }) - } -} - -/// Acquisition details collected from Crunchbase. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Acquisition { - #[serde(skip_serializing_if = "Option::is_none")] - pub acquiree_cb_permalink: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub acquiree_name: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub announced_on: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub price: Option, -} - -impl From for Acquisition { - fn from(cba: CBAcquisition) -> Self { - Acquisition { - acquiree_cb_permalink: cba.acquiree_identifier.as_ref().and_then(|i| i.permalink.clone()), - acquiree_name: cba.acquiree_identifier.and_then(|i| i.value.clone()), - announced_on: cba.announced_on.and_then(|a| a.value), - price: cba.price.and_then(|p| p.value_usd), - } - } -} - -/// FundingRound details collected from Crunchbase. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct FundingRound { - #[serde(skip_serializing_if = "Option::is_none")] - pub amount: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub announced_on: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub kind: Option, -} + .filter(|vec: &Vec| !vec.is_empty()); + + // Prepare funding rounds + let funding_rounds = cb_org + .cards + .raised_funding_rounds + .map(|cb_funding_rounds| cb_funding_rounds.into_iter().map(new_funding_round_from).collect()) + .filter(|vec: &Vec| !vec.is_empty()); + + // Prepare number of employees + let (num_employees_min, num_employees_max) = match cb_org.properties.num_employees_enum { + Some(value) => match value.as_str() { + "c_00001_00010" => (Some(1), Some(10)), + "c_00011_00050" => (Some(11), Some(50)), + "c_00051_00100" => (Some(51), Some(100)), + "c_00101_00250" => (Some(101), Some(250)), + "c_00251_00500" => (Some(251), Some(500)), + "c_00501_01000" => (Some(501), Some(1000)), + "c_01001_05000" => (Some(1001), Some(5000)), + "c_05001_10000" => (Some(5001), Some(10000)), + "c_10001_max" => (Some(10001), None), + _ => (None, None), + }, + None => (None, None), + }; -impl From for FundingRound { - fn from(cbfr: CBFundingRound) -> Self { - FundingRound { - amount: cbfr.money_raised.and_then(|p| p.value_usd), - announced_on: cbfr.announced_on, - kind: cbfr.investment_type, - } - } + // Prepare organization instance using the information collected + Ok(Organization { + generated_at: Utc::now(), + acquisitions, + city: get_location_value(&cb_org.cards.headquarters_address, "city"), + company_type: cb_org.properties.company_type, + country: get_location_value(&cb_org.cards.headquarters_address, "country"), + description: cb_org.properties.short_description, + funding: cb_org.properties.funding_total.as_ref().and_then(|f| f.value_usd), + funding_rounds, + homepage_url: cb_org.properties.website.and_then(|v| v.value), + categories: cb_org.properties.categories.and_then(|c| c.into_iter().map(|c| c.value).collect()), + kind: cb_org.properties.funding_total.map(|_| "funding".to_string()), + linkedin_url: cb_org.properties.linkedin.and_then(|v| v.value), + name: cb_org.properties.name, + num_employees_max, + num_employees_min, + region: get_location_value(&cb_org.cards.headquarters_address, "region"), + stock_exchange: cb_org.properties.stock_exchange_symbol, + ticker: cb_org.properties.stock_symbol.and_then(|v| v.value), + twitter_url: cb_org.properties.twitter.and_then(|v| v.value), + }) } /// Crunchbase API base url. @@ -499,3 +382,22 @@ fn get_permalink(cb_url: &str) -> Result { let c = CRUNCHBASE_URL.captures(cb_url).ok_or_else(|| format_err!("invalid crunchbase url"))?; Ok(c["permalink"].to_string()) } + +/// Create a new Acquisition instance from the Crunchbase data provided. +fn new_acquisition_from(cba: CBAcquisition) -> Acquisition { + Acquisition { + acquiree_cb_permalink: cba.acquiree_identifier.as_ref().and_then(|i| i.permalink.clone()), + acquiree_name: cba.acquiree_identifier.and_then(|i| i.value.clone()), + announced_on: cba.announced_on.and_then(|a| a.value), + price: cba.price.and_then(|p| p.value_usd), + } +} + +/// Create a new FundingRound instance from the Crunchbase data provided. +fn new_funding_round_from(cbfr: CBFundingRound) -> FundingRound { + FundingRound { + amount: cbfr.money_raised.and_then(|p| p.value_usd), + announced_on: cbfr.announced_on, + kind: cbfr.investment_type, + } +} diff --git a/src/build/export.rs b/crates/cli/src/build/export.rs similarity index 100% rename from src/build/export.rs rename to crates/cli/src/build/export.rs diff --git a/src/build/github.rs b/crates/cli/src/build/github.rs similarity index 73% rename from src/build/github.rs rename to crates/cli/src/build/github.rs index 07f0d798..c9a844b1 100644 --- a/src/build/github.rs +++ b/crates/cli/src/build/github.rs @@ -8,6 +8,7 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use deadpool::unmanaged::{Object, Pool}; use futures::stream::{self, StreamExt}; +use landscape2_core::data::{Commit, Contributors, GithubData, Release, RepositoryGithubData}; use lazy_static::lazy_static; #[cfg(test)] use mockall::automock; @@ -15,7 +16,6 @@ use octorust::auth::Credentials; use octorust::types::{FullRepository, ParticipationStats}; use regex::Regex; use reqwest::header::{self, HeaderMap, HeaderValue}; -use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::env; use tracing::{debug, instrument, warn}; @@ -102,13 +102,13 @@ pub(crate) async fn collect_github_data(cache: &Cache, landscape_data: &Landscap // Otherwise we pull it from GitHub if any tokens were provided else if let Some(gh_pool) = &gh_pool { let gh = gh_pool.get().await.expect("token -when available-"); - (url.clone(), Repository::new(gh, &url).await) + (url.clone(), collect_repository_data(gh, &url).await) } else { (url.clone(), Err(format_err!("no tokens provided"))) } }) .buffer_unordered(concurrency) - .collect::>>() + .collect::>>() .await .into_iter() .filter_map(|(url, result)| { @@ -127,116 +127,42 @@ pub(crate) async fn collect_github_data(cache: &Cache, landscape_data: &Landscap Ok(github_data) } -/// Type alias to represent some repositories' GitHub data. -pub(crate) type GithubData = BTreeMap; - -/// Type alias to represent a GitHub repository url. -pub(crate) type RepositoryUrl = String; - -/// Repository information collected from GitHub. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Repository { - pub contributors: Contributors, - pub description: String, - pub generated_at: DateTime, - pub latest_commit: Commit, - pub participation_stats: Vec, - pub stars: i64, - pub url: String, - - #[serde(skip_serializing_if = "Option::is_none")] - pub first_commit: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub languages: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub latest_release: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub license: Option, -} - -impl Repository { - /// Create a new Repository instance from information available on GitHub. - async fn new(gh: Object, repo_url: &str) -> Result { - // Collect some information from GitHub - let (owner, repo) = get_owner_and_repo(repo_url)?; - let gh_repo = gh.get_repository(&owner, &repo).await?; - let contributors_count = gh.get_contributors_count(&owner, &repo).await?; - let first_commit = gh.get_first_commit(&owner, &repo, &gh_repo.default_branch).await?; - let languages = gh.get_languages(&owner, &repo).await?; - let latest_commit = gh.get_latest_commit(&owner, &repo, &gh_repo.default_branch).await?; - let latest_release = gh.get_latest_release(&owner, &repo).await?; - let participation_stats = gh.get_participation_stats(&owner, &repo).await?.all; - - // Prepare repository instance using the information collected - Ok(Repository { - generated_at: Utc::now(), - contributors: Contributors { - count: contributors_count, - url: format!("https://github.com/{owner}/{repo}/graphs/contributors"), - }, - description: gh_repo.description, - first_commit, - languages, - latest_commit, - latest_release, - license: gh_repo.license.and_then(|l| { - if l.name == "NOASSERTION" { - None - } else { - Some(l.name) - } - }), - participation_stats, - stars: gh_repo.stargazers_count, - url: gh_repo.html_url, - }) - } -} - -/// Commit information. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Commit { - pub ts: Option>, - pub url: String, -} - -impl From for Commit { - fn from(value: octorust::types::CommitDataType) -> Self { - let mut commit = Commit { - url: value.html_url, - ts: None, - }; - if let Some(author) = value.commit.author { - commit.ts = Some(DateTime::parse_from_rfc3339(&author.date).expect("date to be valid").into()); - } - commit - } -} - -/// Contributors information. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Contributors { - pub count: usize, - pub url: String, -} - -/// Release information. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Release { - pub ts: Option>, - pub url: String, -} - -impl From for Release { - fn from(value: octorust::types::Release) -> Self { - Self { - ts: value.published_at, - url: value.html_url, - } - } +/// Collect repository data from GitHub. +#[instrument(skip_all, err)] +async fn collect_repository_data(gh: Object, repo_url: &str) -> Result { + // Collect some information from GitHub + let (owner, repo) = get_owner_and_repo(repo_url)?; + let gh_repo = gh.get_repository(&owner, &repo).await?; + let contributors_count = gh.get_contributors_count(&owner, &repo).await?; + let first_commit = gh.get_first_commit(&owner, &repo, &gh_repo.default_branch).await?; + let languages = gh.get_languages(&owner, &repo).await?; + let latest_commit = gh.get_latest_commit(&owner, &repo, &gh_repo.default_branch).await?; + let latest_release = gh.get_latest_release(&owner, &repo).await?; + let participation_stats = gh.get_participation_stats(&owner, &repo).await?.all; + + // Prepare repository instance using the information collected + Ok(RepositoryGithubData { + generated_at: Utc::now(), + contributors: Contributors { + count: contributors_count, + url: format!("https://github.com/{owner}/{repo}/graphs/contributors"), + }, + description: gh_repo.description, + first_commit, + languages, + latest_commit, + latest_release, + license: gh_repo.license.and_then(|l| { + if l.name == "NOASSERTION" { + None + } else { + Some(l.name) + } + }), + participation_stats, + stars: gh_repo.stargazers_count, + url: gh_repo.html_url, + }) } /// GitHub API base url. @@ -321,8 +247,8 @@ impl GH for GHApi { } /// [GH::get_first_commit] - #[instrument(skip(self), err)] #[allow(clippy::cast_possible_wrap)] + #[instrument(skip(self), err)] async fn get_first_commit(&self, owner: &str, repo: &str, ref_: &str) -> Result> { // Get last commits page let url = format!("{GITHUB_API_URL}/repos/{owner}/{repo}/commits?sha={ref_}&per_page=1"); @@ -338,7 +264,7 @@ impl GH for GHApi { .body .pop() { - return Ok(Some(Commit::from(commit))); + return Ok(Some(new_commit_from(commit))); } Ok(None) } @@ -355,14 +281,14 @@ impl GH for GHApi { #[instrument(skip(self), err)] async fn get_latest_commit(&self, owner: &str, repo: &str, ref_: &str) -> Result { let response = self.gh_client.repos().get_commit(owner, repo, 1, 1, ref_).await?; - Ok(response.body.into()) + Ok(new_commit_from(response.body)) } /// [GH::get_latest_release] #[instrument(skip(self), err)] async fn get_latest_release(&self, owner: &str, repo: &str) -> Result> { match self.gh_client.repos().get_latest_release(owner, repo).await { - Ok(response) => Ok(Some(response.body.into())), + Ok(response) => Ok(Some(new_release_from(response.body))), Err(err) => { if err.to_string().to_lowercase().contains("not found") { return Ok(None); @@ -412,3 +338,23 @@ fn get_owner_and_repo(repo_url: &str) -> Result<(String, String)> { let c = GITHUB_REPO_URL.captures(repo_url).ok_or_else(|| format_err!("invalid repository url"))?; Ok((c["owner"].to_string(), c["repo"].to_string())) } + +/// Create a new commit instance from the octorust commit data provided. +fn new_commit_from(value: octorust::types::CommitDataType) -> Commit { + let mut commit = Commit { + url: value.html_url, + ts: None, + }; + if let Some(author) = value.commit.author { + commit.ts = Some(DateTime::parse_from_rfc3339(&author.date).expect("date to be valid").into()); + } + commit +} + +/// Create a new release instance from the octorust release data provided. +fn new_release_from(value: octorust::types::Release) -> Release { + Release { + ts: value.published_at, + url: value.html_url, + } +} diff --git a/src/build/logos.rs b/crates/cli/src/build/logos.rs similarity index 90% rename from src/build/logos.rs rename to crates/cli/src/build/logos.rs index c455ef37..bb2aab57 100644 --- a/src/build/logos.rs +++ b/crates/cli/src/build/logos.rs @@ -1,18 +1,17 @@ //! This module provides some helper functions to prepare logos to be displayed //! on the landscape web application. -use crate::LogosSource; +use super::settings::LogosViewbox; use anyhow::{bail, Result}; +use clap::Args; use lazy_static::lazy_static; use regex::bytes::Regex; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use std::fs; +use std::{fs, path::PathBuf}; use usvg::{NodeExt, Rect, TreeParsing}; -use super::settings::LogosViewbox; - lazy_static! { /// Regular expression used to remove the SVG logos' title. static ref SVG_TITLE: Regex = Regex::new(".*",).expect("exprs in SVG_TITLE to be valid"); @@ -21,6 +20,19 @@ lazy_static! { static ref SVG_VIEWBOX: Regex = Regex::new(r#"viewBox="[0-9. ]*""#).expect("expr in SVG_VIEWBOX to be valid"); } +/// Landscape logos source. +#[derive(Args, Clone, Default)] +#[group(required = true, multiple = false)] +pub struct LogosSource { + /// Local path where the logos are stored. + #[arg(long)] + pub logos_path: Option, + + /// Base URL where the logos are hosted. + #[arg(long)] + pub logos_url: Option, +} + /// Represents some information about an item's logo. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub(crate) struct Logo { diff --git a/src/build/mod.rs b/crates/cli/src/build/mod.rs similarity index 86% rename from src/build/mod.rs rename to crates/cli/src/build/mod.rs index 83af8e87..b6d36025 100644 --- a/src/build/mod.rs +++ b/crates/cli/src/build/mod.rs @@ -3,33 +3,37 @@ use self::{ cache::Cache, crunchbase::collect_crunchbase_data, - datasets::{Datasets, NewDatasetsInput}, export::generate_items_csv, github::collect_github_data, - logos::prepare_logo, - projects::{generate_projects_csv, Project, ProjectsMd}, - settings::{Analytics, LogosViewbox, Osano}, + logos::{prepare_logo, LogosSource}, + projects::{generate_projects_csv, ProjectsMd}, }; use crate::{ - build::api::{Api, ApiSources}, - serve, BuildArgs, GuideSource, LogosSource, ServeArgs, + build::{ + api::{Api, ApiSources}, + projects::collect_projects, + }, + serve::{self, serve}, }; use anyhow::{bail, Context, Result}; use askama::Template; use base64::{engine::general_purpose::STANDARD as b64, Engine as _}; -pub(crate) use data::LandscapeData; use futures::stream::{self, StreamExt}; -pub(crate) use guide::LandscapeGuide; use headless_chrome::{ browser, protocol::cdp::Page::{self, CaptureScreenshotFormatOption}, types::PrintToPdfOptions, Browser, LaunchOptions, }; +use landscape2_core::{ + data::{self, DataSource, LandscapeData}, + datasets::{Datasets, NewDatasetsInput}, + guide::{GuideSource, LandscapeGuide}, + settings::{self, Analytics, LandscapeSettings, LogosViewbox, Osano, SettingsSource}, +}; use qrcode::render::svg; use reqwest::StatusCode; use rust_embed::RustEmbed; -pub(crate) use settings::LandscapeSettings; use std::{ collections::HashMap, ffi::OsStr, @@ -48,15 +52,10 @@ mod api; mod cache; mod clomonitor; mod crunchbase; -mod data; -mod datasets; mod export; mod github; -mod guide; mod logos; mod projects; -mod settings; -mod stats; /// Maximum number of CLOMonitor reports summaries to fetch concurrently. const CLOMONITOR_MAX_CONCURRENCY: usize = 10; @@ -79,24 +78,55 @@ const IMAGES_PATH: &str = "images"; /// Path where the item logos will be written to in the output directory. const LOGOS_PATH: &str = "logos"; +/// Path where the data sources files will be written to in the output dir. +const SOURCES_PATH: &str = "sources"; + /// Maximum number of logos to prepare concurrently. const PREPARE_LOGOS_MAX_CONCURRENCY: usize = 20; /// Embed landscape embeddable views assets into binary. /// (these assets will be built automatically from the build script) #[derive(RustEmbed)] -#[folder = "embed/dist"] +#[folder = "../../ui/embed/dist"] struct EmbedAssets; /// Embed web application assets into binary. /// (these assets will be built automatically from the build script) #[derive(RustEmbed)] -#[folder = "web/dist"] -struct WebAssets; +#[folder = "../../ui/webapp/dist"] +struct WebappAssets; + +/// Build arguments. +#[derive(clap::Args)] +pub struct BuildArgs { + /// Cache directory. + #[arg(long)] + pub cache_dir: Option, + + /// Data source. + #[command(flatten)] + pub data_source: DataSource, + + /// Guide source. + #[command(flatten)] + pub guide_source: GuideSource, + + /// Logos source. + #[command(flatten)] + pub logos_source: LogosSource, + + /// Output directory to write files to. + #[arg(long)] + pub output_dir: PathBuf, + + /// Settings source. + #[command(flatten)] + pub settings_source: SettingsSource, +} /// Build landscape website. #[instrument(skip_all)] -pub(crate) async fn build(args: &BuildArgs) -> Result<()> { +pub async fn build(args: &BuildArgs) -> Result<()> { info!("building landscape website.."); let start = Instant::now(); @@ -115,14 +145,6 @@ pub(crate) async fn build(args: &BuildArgs) -> Result<()> { // Get landscape settings from the source provided let mut settings = LandscapeSettings::new(&args.settings_source).await?; - // Add some extra information to the landscape based on the settings - landscape_data.add_featured_items_data(&settings)?; - landscape_data.add_member_subcategory(&settings.members_category); - landscape_data.add_tags(&settings); - - // Fetch some settings images and update their urls to the local copy - prepare_settings_images(&mut settings, &args.output_dir).await?; - // Prepare guide and copy it to the output directory let guide = prepare_guide(&args.guide_source, &args.output_dir).await?; @@ -135,8 +157,8 @@ pub(crate) async fn build(args: &BuildArgs) -> Result<()> { ) .await?; - // Collect CLOMonitor reports summaries and copy them to the output directory - collect_clomonitor_reports(&cache, &mut landscape_data, &settings, &args.output_dir).await?; + // Fetch some settings images and update their urls to the local copy + prepare_settings_images(&mut settings, &args.output_dir).await?; // Collect data from external services let (crunchbase_data, github_data) = tokio::try_join!( @@ -144,9 +166,16 @@ pub(crate) async fn build(args: &BuildArgs) -> Result<()> { collect_github_data(&cache, &landscape_data) )?; - // Add data collected from external services to the landscape data - landscape_data.add_crunchbase_data(&crunchbase_data)?; - landscape_data.add_github_data(&github_data)?; + // Enrich landscape data with some extra information from the settings and + // external services + landscape_data.add_crunchbase_data(&crunchbase_data); + landscape_data.add_featured_items_data(&settings); + landscape_data.add_github_data(&github_data); + landscape_data.add_member_subcategory(&settings.members_category); + landscape_data.add_tags(&settings); + + // Collect CLOMonitor reports summaries and copy them to the output directory + collect_clomonitor_reports(&cache, &mut landscape_data, &settings, &args.output_dir).await?; // Generate API data files generate_api( @@ -176,9 +205,9 @@ pub(crate) async fn build(args: &BuildArgs) -> Result<()> { // Render index file and write it to the output directory render_index(&settings.analytics, &datasets, &settings.osano, &args.output_dir)?; - // Copy embed and web assets files to the output directory + // Copy embed and web application assets files to the output directory copy_embed_assets(&args.output_dir)?; - copy_web_assets(&args.output_dir)?; + copy_webapp_assets(&args.output_dir)?; // Generate items.csv file generate_items_csv_file(&landscape_data, &args.output_dir)?; @@ -191,6 +220,9 @@ pub(crate) async fn build(args: &BuildArgs) -> Result<()> { prepare_screenshot(*width, &args.output_dir).await?; } + // Copy data sources files to the output directory + copy_data_sources_files(args, &args.output_dir).await?; + let duration = start.elapsed().as_secs_f64(); info!("landscape website built! (took: {:.3}s)", duration); display_success_msg(&args.output_dir.to_string_lossy()); @@ -203,7 +235,7 @@ pub(crate) async fn build(args: &BuildArgs) -> Result<()> { fn check_web_assets() -> Result<()> { debug!("checking web assets are present"); - if !WebAssets::iter().any(|path| path.starts_with("assets/")) { + if !WebappAssets::iter().any(|path| path.starts_with("assets/")) { bail!("web assets not found, please make sure they have been built"); } @@ -280,6 +312,52 @@ async fn collect_clomonitor_reports( Ok(()) } +/// Copy data sources files to the output directory. +#[instrument(skip_all, err)] +async fn copy_data_sources_files(args: &BuildArgs, output_dir: &Path) -> Result<()> { + // Helper function to copy the data source file provided + async fn copy(src_file: &Option, src_url: &Option, dst_file: PathBuf) -> Result<()> { + if let Some(src_file) = src_file { + fs::copy(src_file, dst_file)?; + } else if let Some(src_url) = src_url { + let data = reqwest::get(src_url).await?.bytes().await?; + fs::write(dst_file, data)?; + } + Ok(()) + } + + debug!("copying data sources files to output directory"); + + // Landscape data + let landscape_data_file = output_dir.join(SOURCES_PATH).join("data.yml"); + copy( + &args.data_source.data_file, + &args.data_source.data_url, + landscape_data_file, + ) + .await?; + + // Settings + let settings_file = output_dir.join(SOURCES_PATH).join("settings.yml"); + copy( + &args.settings_source.settings_file, + &args.settings_source.settings_url, + settings_file, + ) + .await?; + + // Guide + let guide_file = output_dir.join(SOURCES_PATH).join("guide.yml"); + copy( + &args.guide_source.guide_file, + &args.guide_source.guide_url, + guide_file, + ) + .await?; + + Ok(()) +} + /// Copy embed assets files to the output directory. #[instrument(skip_all, err)] fn copy_embed_assets(output_dir: &Path) -> Result<()> { @@ -299,19 +377,19 @@ fn copy_embed_assets(output_dir: &Path) -> Result<()> { Ok(()) } -/// Copy web assets files to the output directory. +/// Copy web application assets files to the output directory. #[instrument(skip_all, err)] -fn copy_web_assets(output_dir: &Path) -> Result<()> { - debug!("copying web assets to output directory"); +fn copy_webapp_assets(output_dir: &Path) -> Result<()> { + debug!("copying web application assets to output directory"); - for asset_path in WebAssets::iter() { + for asset_path in WebappAssets::iter() { // The index document is a template that we'll render, so we don't want // to copy it as is. if asset_path == "index.html" || asset_path == ".keep" { continue; } - if let Some(embedded_file) = WebAssets::get(&asset_path) { + if let Some(embedded_file) = WebappAssets::get(&asset_path) { if let Some(parent_path) = Path::new(asset_path.as_ref()).parent() { fs::create_dir_all(output_dir.join(parent_path))?; } @@ -371,7 +449,7 @@ fn generate_api(input: &ApiSources, output_dir: &Path) -> Result<()> { fn generate_datasets(input: &NewDatasetsInput, output_dir: &Path) -> Result { debug!("generating datasets"); - let datasets = Datasets::new(input)?; + let datasets = Datasets::new(input); let datasets_path = output_dir.join(DATASETS_PATH); // Base @@ -412,7 +490,7 @@ fn generate_items_csv_file(landscape_data: &LandscapeData, output_dir: &Path) -> fn generate_projects_files(landscape_data: &LandscapeData, output_dir: &Path) -> Result<()> { debug!("generating projects files"); - let projects: Vec = landscape_data.into(); + let projects = collect_projects(landscape_data); // projects.md let projects_md = ProjectsMd { projects: &projects }.render()?; @@ -537,8 +615,8 @@ async fn prepare_items_logos( } /// Prepare landscape screenshot (in PNG and PDF formats). -#[instrument(skip(output_dir), err)] #[allow(clippy::cast_precision_loss, clippy::items_after_statements)] +#[instrument(skip(output_dir), err)] async fn prepare_screenshot(width: u32, output_dir: &Path) -> Result<()> { debug!("preparing screenshot"); @@ -556,7 +634,7 @@ async fn prepare_screenshot(width: u32, output_dir: &Path) -> Result<()> { let svr_addr_copy = svr_addr.clone(); let landscape_dir = Some(PathBuf::from(&output_dir)); let server = tokio::spawn(async { - let args = ServeArgs { + let args = serve::ServeArgs { addr: svr_addr_copy, graceful_shutdown: false, landscape_dir, @@ -712,29 +790,18 @@ fn setup_output_dir(output_dir: &Path) -> Result<()> { fs::create_dir_all(output_dir)?; } - let api_path = output_dir.join(API_PATH); - if !api_path.exists() { - fs::create_dir(api_path)?; - } - - let datasets_path = output_dir.join(DATASETS_PATH); - if !datasets_path.exists() { - fs::create_dir(datasets_path)?; - } - - let docs_path = output_dir.join(DOCS_PATH); - if !docs_path.exists() { - fs::create_dir(docs_path)?; - } - - let images_path = output_dir.join(IMAGES_PATH); - if !images_path.exists() { - fs::create_dir(images_path)?; - } - - let logos_path = output_dir.join(LOGOS_PATH); - if !logos_path.exists() { - fs::create_dir(logos_path)?; + for path in &[ + API_PATH, + DATASETS_PATH, + DOCS_PATH, + IMAGES_PATH, + LOGOS_PATH, + SOURCES_PATH, + ] { + let path = output_dir.join(path); + if !path.exists() { + fs::create_dir(path)?; + } } Ok(()) diff --git a/crates/cli/src/build/projects.rs b/crates/cli/src/build/projects.rs new file mode 100644 index 00000000..2aaaa0fe --- /dev/null +++ b/crates/cli/src/build/projects.rs @@ -0,0 +1,122 @@ +//! This module defines the functionality to generate the `projects.md` and +//! `projects.csv` files from the information available in the landscape. + +use anyhow::Result; +use askama::Template; +use chrono::NaiveDate; +use landscape2_core::data::{LandscapeData, DATE_FORMAT}; +use serde::{Deserialize, Serialize}; +use std::fs::File; + +/// Project information used to generate the projects.md and projects.csv files. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(crate) struct Project { + pub accepted_at: String, + pub archived_at: String, + pub graduated_at: String, + pub homepage_url: String, + pub incubating_at: String, + pub last_security_audit: String, + pub maturity: String, + pub name: String, + pub num_security_audits: String, + pub sandbox_at: String, + pub tag: String, +} + +/// Collect projects from the landscape data. +pub(crate) fn collect_projects(landscape_data: &LandscapeData) -> Vec { + // Helper closure to format dates + let fmt_date = |date: &Option| { + let Some(date) = date else { + return String::new(); + }; + date.format(DATE_FORMAT).to_string() + }; + + let mut projects: Vec = landscape_data + .items + .iter() + .cloned() + .filter_map(|item| { + // Prepare maturity and tag + let maturity = item.maturity?; + let tag = item.tag?; + + // Prepare sandbox date + let sandbox_at = if item.accepted_at == item.incubating_at { + None + } else { + item.accepted_at + }; + + // Prepare security audits info + let last_security_audit = item.audits.as_ref().and_then(|a| a.last().map(|a| a.date)); + let num_security_audits = item.audits.as_ref().map(Vec::len); + + // Create project instance and return it + let project = Project { + accepted_at: fmt_date(&item.accepted_at), + archived_at: fmt_date(&item.archived_at), + graduated_at: fmt_date(&item.graduated_at), + homepage_url: item.homepage_url, + incubating_at: fmt_date(&item.incubating_at), + maturity: maturity.to_string(), + name: item.name.to_lowercase(), + num_security_audits: num_security_audits.unwrap_or_default().to_string(), + last_security_audit: fmt_date(&last_security_audit), + sandbox_at: fmt_date(&sandbox_at), + tag: tag.to_string(), + }; + Some(project) + }) + .collect(); + + // Sort projects + projects.sort_by(|a, b| a.name.cmp(&b.name)); + + projects +} + +/// Template for the projects.md file. +#[derive(Debug, Clone, Template)] +#[template(path = "projects.md")] +pub(crate) struct ProjectsMd<'a> { + pub projects: &'a [Project], +} + +/// Generate CSV file with some information about each project. +pub(crate) fn generate_projects_csv(mut w: csv::Writer, projects: &[Project]) -> Result<()> { + // Write headers + w.write_record([ + "project_name", + "maturity", + "tag", + "accepted_date", + "sandbox_date", + "incubating_date", + "graduated_date", + "archived_date", + "num_security_audits", + "last_security_audit_date", + ])?; + + // Write one record for each project + for p in projects { + w.write_record([ + &p.name, + &p.maturity, + &p.tag, + &p.accepted_at, + &p.sandbox_at, + &p.incubating_at, + &p.graduated_at, + &p.archived_at, + &p.num_security_audits, + &p.last_security_audit, + ])?; + } + + w.flush()?; + Ok(()) +} diff --git a/crates/cli/src/deploy/mod.rs b/crates/cli/src/deploy/mod.rs new file mode 100644 index 00000000..f5975386 --- /dev/null +++ b/crates/cli/src/deploy/mod.rs @@ -0,0 +1,21 @@ +//! This module defines the functionality of the deploy CLI subcommand. + +use clap::Subcommand; + +pub mod s3; + +/// Deploy command arguments. +#[derive(clap::Args)] +#[command(args_conflicts_with_subcommands = true)] +pub struct DeployArgs { + /// Provider used to deploy the landscape website. + #[command(subcommand)] + pub provider: Provider, +} + +/// Provider used to deploy the landscape website. +#[derive(Subcommand)] +pub enum Provider { + /// Deploy landscape website to AWS S3. + S3(s3::Args), +} diff --git a/src/deploy/s3.rs b/crates/cli/src/deploy/s3.rs similarity index 95% rename from src/deploy/s3.rs rename to crates/cli/src/deploy/s3.rs index adc9459e..94d1524e 100644 --- a/src/deploy/s3.rs +++ b/crates/cli/src/deploy/s3.rs @@ -1,7 +1,6 @@ //! This module defines the functionality of the deploy CLI subcommand for the //! AWS S3 provider. -use crate::S3Args; use anyhow::{bail, format_err, Context, Result}; use aws_sdk_s3::primitives::ByteStream; use futures::stream::{self, StreamExt}; @@ -28,9 +27,21 @@ type Checksum = String; /// Type alias to represent an object's key. type Key = String; +/// AWS S3 provider arguments. +#[derive(clap::Args)] +pub struct Args { + /// Bucket to copy the landscape website files to. + #[arg(long)] + pub bucket: String, + + /// Location of the landscape website files (build subcommand output). + #[arg(long)] + pub landscape_dir: PathBuf, +} + /// Deploy landscape website to AWS S3. #[instrument(skip_all, err)] -pub(crate) async fn deploy(args: &S3Args) -> Result<()> { +pub async fn deploy(args: &Args) -> Result<()> { info!("deploying landscape website.."); let start = Instant::now(); diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 00000000..ba43dcd7 --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,12 @@ +#![warn(clippy::all, clippy::pedantic)] +#![allow( + clippy::doc_markdown, + clippy::blocks_in_conditions, + clippy::module_name_repetitions +)] + +pub mod build; +pub mod deploy; +pub mod new; +pub mod serve; +pub mod validate; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs new file mode 100644 index 00000000..91f9579a --- /dev/null +++ b/crates/cli/src/main.rs @@ -0,0 +1,82 @@ +#![warn(clippy::all, clippy::pedantic)] +#![allow( + clippy::doc_markdown, + clippy::blocks_in_conditions, + clippy::module_name_repetitions +)] + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use landscape2::build::{build, BuildArgs}; +use landscape2::deploy::s3::{self}; +use landscape2::deploy::{DeployArgs, Provider}; +use landscape2::new::{new, NewArgs}; +use landscape2::serve::{serve, ServeArgs}; +use landscape2::validate::{validate_data, validate_guide, validate_settings, Target, ValidateArgs}; + +/// CLI arguments. +#[derive(Parser)] +#[command( + version, + about = "Landscape2 CLI tool + +https://github.com/cncf/landscape2#usage" +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +/// Commands available. +#[derive(Subcommand)] +enum Command { + /// Build landscape website. + Build(BuildArgs), + + /// Deploy landscape website (experimental). + Deploy(DeployArgs), + + /// Create a new landscape from the built-in template. + New(NewArgs), + + /// Serve landscape website. + Serve(ServeArgs), + + /// Validate landscape data sources files. + Validate(ValidateArgs), +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + // Setup logging + match &cli.command { + Command::Build(_) | Command::Deploy(_) | Command::New(_) | Command::Serve(_) => { + if std::env::var_os("RUST_LOG").is_none() { + std::env::set_var("RUST_LOG", "landscape2=debug"); + } + tracing_subscriber::fmt::init(); + } + Command::Validate(_) => {} + } + + // Run command + match &cli.command { + Command::Build(args) => build(args).await?, + Command::Deploy(args) => { + match &args.provider { + Provider::S3(args) => s3::deploy(args).await?, + }; + } + Command::New(args) => new(args)?, + Command::Serve(args) => serve(args).await?, + Command::Validate(args) => match &args.target { + Target::Data(src) => validate_data(src).await?, + Target::Guide(src) => validate_guide(src).await?, + Target::Settings(src) => validate_settings(src).await?, + }, + } + + Ok(()) +} diff --git a/src/new/mod.rs b/crates/cli/src/new/mod.rs similarity index 88% rename from src/new/mod.rs rename to crates/cli/src/new/mod.rs index fc27bb24..a2fbb103 100644 --- a/src/new/mod.rs +++ b/crates/cli/src/new/mod.rs @@ -1,12 +1,11 @@ //! This module defines the functionality of the create CLI subcommand. -use crate::NewArgs; use anyhow::Result; use rust_embed::RustEmbed; use std::{ fs::{self, File}, io::Write, - path::Path, + path::{Path, PathBuf}, time::Instant, }; use tracing::{info, instrument}; @@ -16,9 +15,17 @@ use tracing::{info, instrument}; #[folder = "src/new/template"] struct TemplateFiles; +/// New arguments. +#[derive(clap::Args)] +pub struct NewArgs { + /// Output directory to write files to. + #[arg(long)] + output_dir: PathBuf, +} + /// Create a new landscape from the built-in template. #[instrument(skip_all)] -pub(crate) fn new(args: &NewArgs) -> Result<()> { +pub fn new(args: &NewArgs) -> Result<()> { info!("creating new landscape from the built-in template.."); let start = Instant::now(); diff --git a/src/new/template/data.yml b/crates/cli/src/new/template/data.yml similarity index 100% rename from src/new/template/data.yml rename to crates/cli/src/new/template/data.yml diff --git a/src/new/template/guide.yml b/crates/cli/src/new/template/guide.yml similarity index 100% rename from src/new/template/guide.yml rename to crates/cli/src/new/template/guide.yml diff --git a/src/new/template/logos/cncf.svg b/crates/cli/src/new/template/logos/cncf.svg similarity index 100% rename from src/new/template/logos/cncf.svg rename to crates/cli/src/new/template/logos/cncf.svg diff --git a/src/new/template/settings.yml b/crates/cli/src/new/template/settings.yml similarity index 100% rename from src/new/template/settings.yml rename to crates/cli/src/new/template/settings.yml diff --git a/src/serve/mod.rs b/crates/cli/src/serve/mod.rs similarity index 77% rename from src/serve/mod.rs rename to crates/cli/src/serve/mod.rs index 70b7b617..37aca082 100644 --- a/src/serve/mod.rs +++ b/crates/cli/src/serve/mod.rs @@ -1,6 +1,5 @@ //! This module defines the functionality of the serve CLI subcommand. -use crate::ServeArgs; use anyhow::Result; use axum::{ extract::Request, @@ -9,14 +8,35 @@ use axum::{ response::IntoResponse, Router, }; -use std::{env, net::SocketAddr}; +use std::{env, net::SocketAddr, path::PathBuf}; use tokio::{net::TcpListener, signal}; use tower_http::services::{ServeDir, ServeFile}; use tracing::{info, instrument}; +/// Serve arguments. +#[derive(clap::Args)] +pub struct ServeArgs { + /// Address the web server will listen on. + #[arg(long, default_value = "127.0.0.1:8000")] + pub addr: String, + + /// Whether the server should stop gracefully or not. + #[arg(long, default_value_t = false)] + pub graceful_shutdown: bool, + + /// Location of the landscape website files (build subcommand output). + /// The current path will be used when none is provided. + #[arg(long)] + pub landscape_dir: Option, + + /// Enable silent mode. + #[arg(long, default_value_t = false)] + pub silent: bool, +} + /// Serve landscape website. #[instrument(skip_all)] -pub(crate) async fn serve(args: &ServeArgs) -> Result<()> { +pub async fn serve(args: &ServeArgs) -> Result<()> { // Setup router let landscape_dir = args.landscape_dir.clone().unwrap_or(env::current_dir()?); let index_path = landscape_dir.join("index.html"); @@ -45,7 +65,7 @@ pub(crate) async fn serve(args: &ServeArgs) -> Result<()> { } /// Middleware that sets the cache control header in the response. -pub(crate) async fn set_cache_control_header(req: Request, next: Next) -> impl IntoResponse { +async fn set_cache_control_header(req: Request, next: Next) -> impl IntoResponse { // Prepare header value (based on the request uri) let cache_control = match req.uri().to_string() { u if u.starts_with("/assets/") => "max-age=31536000", diff --git a/src/validate/mod.rs b/crates/cli/src/validate/mod.rs similarity index 50% rename from src/validate/mod.rs rename to crates/cli/src/validate/mod.rs index a00598ee..b5a61604 100644 --- a/src/validate/mod.rs +++ b/crates/cli/src/validate/mod.rs @@ -1,15 +1,39 @@ //! This module defines the functionality of the validate CLI subcommand. -use crate::{ - build::{LandscapeData, LandscapeGuide, LandscapeSettings}, - DataSource, GuideSource, SettingsSource, -}; use anyhow::{Context, Result}; +use clap::Subcommand; +use landscape2_core::{ + data::{DataSource, LandscapeData}, + guide::{GuideSource, LandscapeGuide}, + settings::{LandscapeSettings, SettingsSource}, +}; use tracing::instrument; +/// Validate command arguments. +#[derive(clap::Args)] +#[command(args_conflicts_with_subcommands = true)] +pub struct ValidateArgs { + /// Landscape file to validate. + #[command(subcommand)] + pub target: Target, +} + +/// Landscape file to validate. +#[derive(Subcommand)] +pub enum Target { + /// Validate landscape data file. + Data(DataSource), + + /// Validate landscape guide file. + Guide(GuideSource), + + /// Validate landscape settings file. + Settings(SettingsSource), +} + /// Validate landscape data file. #[instrument(skip_all)] -pub(crate) async fn validate_data(data_source: &DataSource) -> Result<()> { +pub async fn validate_data(data_source: &DataSource) -> Result<()> { LandscapeData::new(data_source) .await .context("the landscape data file provided is not valid")?; @@ -20,7 +44,7 @@ pub(crate) async fn validate_data(data_source: &DataSource) -> Result<()> { /// Validate landscape settings file. #[instrument(skip_all)] -pub(crate) async fn validate_settings(settings_source: &SettingsSource) -> Result<()> { +pub async fn validate_settings(settings_source: &SettingsSource) -> Result<()> { LandscapeSettings::new(settings_source) .await .context("the landscape settings file provided is not valid")?; @@ -31,7 +55,7 @@ pub(crate) async fn validate_settings(settings_source: &SettingsSource) -> Resul /// Validate landscape guide file. #[instrument(skip_all)] -pub(crate) async fn validate_guide(guide_source: &GuideSource) -> Result<()> { +pub async fn validate_guide(guide_source: &GuideSource) -> Result<()> { LandscapeGuide::new(guide_source) .await .context("the landscape guide file provided is not valid")?; diff --git a/templates/projects.md b/crates/cli/templates/projects.md similarity index 100% rename from templates/projects.md rename to crates/cli/templates/projects.md diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 00000000..fd58dbfe --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "landscape2-core" +description = "Landscape2 core functionality" +repository = "https://github.com/cncf/landscape2" +readme = "../../README.md" +version.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true + +[dependencies] +anyhow = { workspace = true } +chrono = { workspace = true } +clap = { workspace = true } +itertools = { workspace = true } +lazy_static = { workspace = true } +markdown = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_yaml = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } diff --git a/src/build/data.rs b/crates/core/src/data.rs similarity index 63% rename from src/build/data.rs rename to crates/core/src/data.rs index 081f3464..73749170 100644 --- a/src/build/data.rs +++ b/crates/core/src/data.rs @@ -7,28 +7,71 @@ //! backwards compatibility, this module provides a `legacy` submodule that //! allows parsing the legacy format and convert it to the new one. -use super::{ - crunchbase::{CrunchbaseData, Organization, CRUNCHBASE_URL}, - github::{self, GithubData}, - settings::{self, LandscapeSettings}, -}; -use crate::DataSource; -use anyhow::{bail, Result}; -use chrono::NaiveDate; -use lazy_static::lazy_static; -use regex::Regex; +use super::settings::{self, LandscapeSettings}; +use crate::util::normalize_name; +use anyhow::{bail, Context, Result}; +use chrono::{DateTime, NaiveDate, Utc}; +use clap::Args; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs, path::Path}; +use std::{ + collections::{BTreeMap, HashMap}, + fs, + path::{Path, PathBuf}, +}; use tracing::{debug, instrument, warn}; -use url::Url; + +mod legacy; /// Format used for dates across the landscape data file. +#[allow(dead_code)] pub const DATE_FORMAT: &str = "%Y-%m-%d"; +/// Type alias to represent a category name. +pub type CategoryName = String; + +/// Type alias to represent some organizations' Crunchbase data. +pub type CrunchbaseData = BTreeMap; + +/// Type alias to represent a crunchbase url. +pub type CrunchbaseUrl = String; + +/// Type alias to represent some repositories' GitHub data. +pub type GithubData = BTreeMap; + +/// Type alias to represent a GitHub repository url. +pub type RepositoryUrl = String; + +/// Type alias to represent a subcategory name. +pub type SubCategoryName = String; + +/// Landscape data source. +#[derive(Args, Default)] +#[group(required = true, multiple = false)] +pub struct DataSource { + /// Landscape data file local path. + #[arg(long)] + pub data_file: Option, + + /// Landscape data file url. + #[arg(long)] + pub data_url: Option, +} + +impl DataSource { + /// Create a new data source from the url provided. + #[must_use] + pub fn new_from_url(url: String) -> Self { + Self { + data_file: None, + data_url: Some(url), + } + } +} + /// Landscape data. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct LandscapeData { +pub struct LandscapeData { pub categories: Vec, pub items: Vec, } @@ -36,7 +79,7 @@ pub(crate) struct LandscapeData { impl LandscapeData { /// Create a new landscape data instance from the source provided. #[instrument(skip_all, err)] - pub(crate) async fn new(src: &DataSource) -> Result { + pub async fn new(src: &DataSource) -> Result { // Try from file if let Some(file) = &src.data_file { debug!(?file, "getting landscape data from file"); @@ -55,10 +98,9 @@ impl LandscapeData { /// Create a new landscape data instance from the file provided. fn new_from_file(file: &Path) -> Result { let raw_data = fs::read_to_string(file)?; - let legacy_data: legacy::LandscapeData = serde_yaml::from_str(&raw_data)?; - legacy_data.validate()?; + let landscape_data = LandscapeData::new_from_raw_data(&raw_data)?; - Ok(LandscapeData::from(legacy_data)) + Ok(landscape_data) } /// Create a new landscape data instance from the url provided. @@ -71,15 +113,24 @@ impl LandscapeData { ); } let raw_data = resp.text().await?; - let legacy_data: legacy::LandscapeData = serde_yaml::from_str(&raw_data)?; + let landscape_data = LandscapeData::new_from_raw_data(&raw_data)?; + + Ok(landscape_data) + } + + /// Create a new landscape data instance from the raw legacy data provided. + fn new_from_raw_data(raw_data: &str) -> Result { + let legacy_data: legacy::LandscapeData = + serde_yaml::from_str(raw_data).context("invalid yaml file")?; legacy_data.validate()?; + let landscape_data = LandscapeData::from(legacy_data); - Ok(LandscapeData::from(legacy_data)) + Ok(landscape_data) } /// Add items Crunchbase data. - #[instrument(skip_all, err)] - pub(crate) fn add_crunchbase_data(&mut self, crunchbase_data: &CrunchbaseData) -> Result<()> { + #[instrument(skip_all)] + pub fn add_crunchbase_data(&mut self, crunchbase_data: &CrunchbaseData) { for item in &mut self.items { if let Some(crunchbase_url) = item.crunchbase_url.as_ref() { if let Some(org_crunchbase_data) = crunchbase_data.get(crunchbase_url) { @@ -87,16 +138,15 @@ impl LandscapeData { } } } - Ok(()) } /// Add featured items information to the landscape data based on the /// settings provided (i.e. graduated and incubating projects must be /// featured and the former displayed first). - #[instrument(skip_all, err)] - pub(crate) fn add_featured_items_data(&mut self, settings: &LandscapeSettings) -> Result<()> { + #[instrument(skip_all)] + pub fn add_featured_items_data(&mut self, settings: &LandscapeSettings) { let Some(rules) = &settings.featured_items else { - return Ok(()); + return; }; for rule in rules { @@ -126,13 +176,11 @@ impl LandscapeData { _ => {} } } - - Ok(()) } /// Add items repositories GitHub data. - #[instrument(skip_all, err)] - pub(crate) fn add_github_data(&mut self, github_data: &GithubData) -> Result<()> { + #[instrument(skip_all)] + pub fn add_github_data(&mut self, github_data: &GithubData) { for item in &mut self.items { // Add GH data to each of the items repositories if item.repositories.is_some() { @@ -156,13 +204,11 @@ impl LandscapeData { item.oss = Some(true); } } - - Ok(()) } /// Add items member subcategory. - #[instrument(skip_all)] - pub(crate) fn add_member_subcategory(&mut self, members_category: &Option) { + #[cfg_attr(feature = "instrument", instrument(skip_all))] + pub fn add_member_subcategory(&mut self, members_category: &Option) { let Some(members_category) = members_category else { return; }; @@ -186,8 +232,8 @@ impl LandscapeData { } /// Add projects items TAG based on the TAGs settings. - #[instrument(skip_all)] - pub(crate) fn add_tags(&mut self, settings: &LandscapeSettings) { + #[cfg_attr(feature = "instrument", instrument(skip_all))] + pub fn add_tags(&mut self, settings: &LandscapeSettings) { let Some(tags) = &settings.tags else { return; }; @@ -400,7 +446,7 @@ impl From for LandscapeData { /// Landscape category. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Category { +pub struct Category { pub name: CategoryName, pub normalized_name: CategoryName, pub subcategories: Vec, @@ -423,22 +469,16 @@ impl From<&settings::Category> for Category { } } -/// Type alias to represent a category name. -pub(crate) type CategoryName = String; - /// Landscape subcategory. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct SubCategory { +pub struct SubCategory { pub name: SubCategoryName, pub normalized_name: SubCategoryName, } -/// Type alias to represent a subcategory name. -pub(crate) type SubCategoryName = String; - /// Landscape item (project, product, member, etc). #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Item { +pub struct Item { pub category: String, pub homepage_url: String, pub id: String, @@ -575,13 +615,14 @@ pub(crate) struct Item { impl Item { /// Get item's description. - #[allow(dead_code)] - pub(crate) fn description(&self) -> Option<&String> { + #[allow(clippy::missing_panics_doc)] + #[must_use] + pub fn description(&self) -> Option<&String> { // Use item's description if available let mut description = self.description.as_ref(); // Otherwise, use primary repository description if available - if description.is_none() || description.unwrap().is_empty() { + if description.is_none() || description.expect("it to be present").is_empty() { description = self.primary_repository().and_then(|r| r.github_data.as_ref().map(|gh| &gh.description)); } @@ -595,8 +636,8 @@ impl Item { } /// Get primary repository if available. - #[allow(dead_code)] - pub(crate) fn primary_repository(&self) -> Option<&Repository> { + #[must_use] + pub fn primary_repository(&self) -> Option<&Repository> { self.repositories .as_ref() .and_then(|repos| repos.iter().find(|r| r.primary.unwrap_or_default())) @@ -613,16 +654,59 @@ impl Item { } } +/// Crunchbase acquisition details. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Acquisition { + #[serde(skip_serializing_if = "Option::is_none")] + pub acquiree_cb_permalink: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub acquiree_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub announced_on: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub price: Option, +} + /// Additional category/subcategory an item can belong to. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct AdditionalCategory { +pub struct AdditionalCategory { pub category: CategoryName, pub subcategory: SubCategoryName, } +/// Commit information. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Commit { + pub ts: Option>, + pub url: String, +} + +/// Contributors information. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Contributors { + pub count: usize, + pub url: String, +} + +/// Crunchbase funding round details. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct FundingRound { + #[serde(skip_serializing_if = "Option::is_none")] + pub amount: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub announced_on: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, +} + /// Landscape item audit information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct ItemAudit { +pub struct ItemAudit { pub date: NaiveDate, #[serde(rename = "type")] pub kind: String, @@ -632,7 +716,7 @@ pub(crate) struct ItemAudit { /// Landscape item featured information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct ItemFeatured { +pub struct ItemFeatured { #[serde(skip_serializing_if = "Option::is_none")] pub label: Option, @@ -642,7 +726,7 @@ pub(crate) struct ItemFeatured { /// Landscape item summary. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct ItemSummary { +pub struct ItemSummary { #[serde(skip_serializing_if = "Option::is_none")] pub business_use_case: Option, @@ -668,322 +752,108 @@ pub(crate) struct ItemSummary { pub use_case: Option, } -/// Repository information. +/// Organization information collected from Crunchbase. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Repository { - pub url: String, +pub struct Organization { + pub generated_at: DateTime, #[serde(skip_serializing_if = "Option::is_none")] - pub branch: Option, - - #[serde(skip_serializing)] - pub github_data: Option, + pub acquisitions: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub primary: Option, -} - -lazy_static! { - static ref VALID_CHARS: Regex = Regex::new(r"[a-z0-9\-\ \+]").expect("exprs in VALID_CHARS to be valid"); - static ref MULTIPLE_HYPHENS: Regex = Regex::new(r"-{2,}").expect("exprs in MULTIPLE_HYPHENS to be valid"); -} - -/// Normalize category, subcategory and item name. -pub(crate) fn normalize_name(value: &str) -> String { - let mut normalized_name = value - .to_lowercase() - .replace(' ', "-") - .chars() - .map(|c| { - if VALID_CHARS.is_match(&c.to_string()) { - c - } else { - '-' - } - }) - .collect::(); - normalized_name = MULTIPLE_HYPHENS.replace(&normalized_name, "-").to_string(); - if let Some(normalized_name_without_suffix) = normalized_name.strip_suffix('-') { - normalized_name = normalized_name_without_suffix.to_string(); - } - normalized_name -} + pub city: Option, -mod legacy { - //! This module defines some types used to parse the landscape data file in - //! legacy format and convert it to the new one. - - use super::{validate_url, ItemAudit}; - use anyhow::{bail, format_err, Context, Result}; - use chrono::NaiveDate; - use lazy_static::lazy_static; - use regex::Regex; - use serde::{Deserialize, Serialize}; - - /// Landscape data (legacy format). - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct LandscapeData { - pub landscape: Vec, - } + #[serde(skip_serializing_if = "Option::is_none")] + pub company_type: Option, - impl LandscapeData { - /// Validate landscape data. - pub(crate) fn validate(&self) -> Result<()> { - for (category_index, category) in self.landscape.iter().enumerate() { - // Check category name - if category.name.is_empty() { - bail!("category [{category_index}] name is required"); - } + #[serde(skip_serializing_if = "Option::is_none")] + pub country: Option, - for (subcategory_index, subcategory) in category.subcategories.iter().enumerate() { - // Used to check for duplicate items within this subcategory - let mut items_seen = Vec::new(); + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, - // Check subcategory name - if subcategory.name.is_empty() { - bail!( - "subcategory [{subcategory_index}] name is required (category: [{}]) ", - category.name - ); - } + #[serde(skip_serializing_if = "Option::is_none")] + pub funding: Option, - for (item_index, item) in subcategory.items.iter().enumerate() { - // Prepare context for errors - let item_id = if item.name.is_empty() { - format!("{item_index}") - } else { - item.name.clone() - }; - let ctx = format!( - "item [{}] is not valid (category: [{}] | subcategory: [{}])", - item_id, category.name, subcategory.name - ); - - // Check name - if item.name.is_empty() { - return Err(format_err!("name is required")).context(ctx); - } - if items_seen.contains(&item.name) { - return Err(format_err!("duplicate item name")).context(ctx); - } - items_seen.push(item.name.clone()); + #[serde(skip_serializing_if = "Option::is_none")] + pub funding_rounds: Option>, - // Check homepage - if item.homepage_url.is_empty() { - return Err(format_err!("hompage_url is required")).context(ctx); - } + #[serde(skip_serializing_if = "Option::is_none")] + pub homepage_url: Option, - // Check logo - if item.logo.is_empty() { - return Err(format_err!("logo is required")).context(ctx); - } + #[serde(skip_serializing_if = "Option::is_none")] + pub categories: Option>, - // Check some values in extra - if let Some(extra) = &item.extra { - // Check tag name - if let Some(tag) = &extra.tag { - if !TAG_NAME.is_match(tag) { - return Err(format_err!( - "invalid tag (must use only lowercase letters and hyphens)" - )) - .context(ctx); - } - } - } + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, - // Check urls - validate_urls(item).context(ctx)?; - } - } - } + #[serde(skip_serializing_if = "Option::is_none")] + pub linkedin_url: Option, - Ok(()) - } - } + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, - /// Landscape category. - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct Category { - pub name: String, - pub subcategories: Vec, - } + #[serde(skip_serializing_if = "Option::is_none")] + pub num_employees_max: Option, - /// Landscape subcategory. - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct SubCategory { - pub name: String, - pub items: Vec, - } + #[serde(skip_serializing_if = "Option::is_none")] + pub num_employees_min: Option, - /// Landscape item (project, product, member, etc). - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct Item { - pub name: String, - pub homepage_url: String, - pub logo: String, - pub additional_repos: Option>, - pub branch: Option, - pub crunchbase: Option, - pub description: Option, - pub enduser: Option, - pub extra: Option, - pub joined: Option, - pub project: Option, - pub repo_url: Option, - pub second_path: Option>, - pub twitter: Option, - pub url_for_bestpractices: Option, - pub unnamed_organization: Option, - } + #[serde(skip_serializing_if = "Option::is_none")] + pub region: Option, - /// Landscape item repository. - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct Repository { - pub repo_url: String, - pub branch: Option, - } + #[serde(skip_serializing_if = "Option::is_none")] + pub stock_exchange: Option, - /// Extra information for a landscape item. - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct ItemExtra { - pub accepted: Option, - pub archived: Option, - pub audits: Option>, - pub annual_review_date: Option, - pub annual_review_url: Option, - pub artwork_url: Option, - pub blog_url: Option, - pub chat_channel: Option, - pub clomonitor_name: Option, - pub dev_stats_url: Option, - pub discord_url: Option, - pub docker_url: Option, - pub github_discussions_url: Option, - pub gitter_url: Option, - pub graduated: Option, - pub incubating: Option, - pub linkedin_url: Option, - pub mailing_list_url: Option, - pub parent_project: Option, - pub slack_url: Option, - pub specification: Option, - pub stack_overflow_url: Option, - pub summary_business_use_case: Option, - pub summary_integration: Option, - pub summary_integrations: Option, - pub summary_intro_url: Option, - pub summary_use_case: Option, - pub summary_personas: Option, - pub summary_release_rate: Option, - pub summary_tags: Option, - pub tag: Option, - pub training_certifications: Option, - pub training_type: Option, - pub youtube_url: Option, - } + #[serde(skip_serializing_if = "Option::is_none")] + pub ticker: Option, - /// Validate the urls of the item provided. - fn validate_urls(item: &Item) -> Result<()> { - // Check urls in item - let homepage_url = Some(item.homepage_url.clone()); - let urls = [ - ("best_practices", &item.url_for_bestpractices), - ("crunchbase", &item.crunchbase), - ("homepage", &homepage_url), - ("repository", &item.repo_url), - ("twitter", &item.twitter), - ]; - for (name, url) in urls { - validate_url(name, url)?; - } + #[serde(skip_serializing_if = "Option::is_none")] + pub twitter_url: Option, +} - // Check additional repositories - if let Some(additional_repos) = &item.additional_repos { - for r in additional_repos { - let repo_url = Some(r.repo_url.clone()); - validate_url("additional_repository", &repo_url)?; - } - } +/// Release information. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Release { + pub ts: Option>, + pub url: String, +} - // Check urls in item extra - if let Some(extra) = &item.extra { - let urls = [ - ("annual_review", &extra.annual_review_url), - ("artwork", &extra.artwork_url), - ("blog", &extra.blog_url), - ("dev_stats", &extra.dev_stats_url), - ("discord", &extra.discord_url), - ("docker", &extra.docker_url), - ("github_discussions", &extra.github_discussions_url), - ("linkedin", &extra.linkedin_url), - ("mailing_list", &extra.mailing_list_url), - ("slack", &extra.slack_url), - ("stack_overflow", &extra.stack_overflow_url), - ("youtube", &extra.youtube_url), - ]; - for (name, url) in urls { - validate_url(name, url)?; - } +/// Repository information. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Repository { + pub url: String, - // Check audits urls - if let Some(audits) = &extra.audits { - for a in audits { - let audit_url = Some(a.url.clone()); - validate_url("audit", &audit_url)?; - } - } - }; + #[serde(skip_serializing_if = "Option::is_none")] + pub branch: Option, - Ok(()) - } + #[serde(skip_serializing)] + pub github_data: Option, - lazy_static! { - /// TAG name regular expression. - pub(crate) static ref TAG_NAME: Regex = Regex::new(r"^[a-z\-]+$").expect("exprs in TAG_NAME to be valid"); - } + #[serde(skip_serializing_if = "Option::is_none")] + pub primary: Option, } -/// Validate the url provided. -pub(crate) fn validate_url(kind: &str, url: &Option) -> Result<()> { - if let Some(url) = url { - let invalid_url = |reason: &str| bail!("invalid {kind} url: {reason}"); +/// Repository information collected from GitHub. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct RepositoryGithubData { + pub contributors: Contributors, + pub description: String, + pub generated_at: DateTime, + pub latest_commit: Commit, + pub participation_stats: Vec, + pub stars: i64, + pub url: String, - // Parse url - let url = match Url::parse(url) { - Ok(url) => url, - Err(err) => return invalid_url(&err.to_string()), - }; + #[serde(skip_serializing_if = "Option::is_none")] + pub first_commit: Option, - // Check scheme - if url.scheme() != "http" && url.scheme() != "https" { - return invalid_url("invalid scheme"); - } + #[serde(skip_serializing_if = "Option::is_none")] + pub languages: Option>, - // Some checks specific to the url kind provided - let check_domain = |domain: &str| { - if url.host_str().is_some_and(|host| !host.ends_with(domain)) { - return invalid_url(&format!("expecting https://{domain}/...")); - } - Ok(()) - }; - match kind { - "crunchbase" => { - if !CRUNCHBASE_URL.is_match(url.as_str()) { - return invalid_url(&format!("expecting: {}", CRUNCHBASE_URL.as_str())); - } - } - "facebook" => return check_domain("facebook.com"), - "flickr" => return check_domain("flickr.com"), - "github" => return check_domain("github.com"), - "instagram" => return check_domain("instagram.com"), - "linkedin" => return check_domain("linkedin.com"), - "stack_overflow" => return check_domain("stackoverflow.com"), - "twitch" => return check_domain("twitch.tv"), - "twitter" => return check_domain("twitter.com"), - "youtube" => return check_domain("youtube.com"), - _ => {} - } - } + #[serde(skip_serializing_if = "Option::is_none")] + pub latest_release: Option, - Ok(()) + #[serde(skip_serializing_if = "Option::is_none")] + pub license: Option, } diff --git a/crates/core/src/data/legacy.rs b/crates/core/src/data/legacy.rs new file mode 100644 index 00000000..10b5cb6e --- /dev/null +++ b/crates/core/src/data/legacy.rs @@ -0,0 +1,232 @@ +//! This module defines some types used to parse the landscape data file in +//! legacy format and convert it to the new one. + +use super::ItemAudit; +use crate::util::validate_url; +use anyhow::{bail, format_err, Context, Result}; +use chrono::NaiveDate; +use lazy_static::lazy_static; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +lazy_static! { + /// TAG name regular expression. + static ref TAG_NAME: Regex = Regex::new(r"^[a-z\-]+$").expect("exprs in TAG_NAME to be valid"); +} + +/// Landscape data (legacy format). +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(super) struct LandscapeData { + pub landscape: Vec, +} + +impl LandscapeData { + /// Validate landscape data. + pub fn validate(&self) -> Result<()> { + for (category_index, category) in self.landscape.iter().enumerate() { + // Check category name + if category.name.is_empty() { + bail!("category [{category_index}] name is required"); + } + + for (subcategory_index, subcategory) in category.subcategories.iter().enumerate() { + // Used to check for duplicate items within this subcategory + let mut items_seen = Vec::new(); + + // Check subcategory name + if subcategory.name.is_empty() { + bail!( + "subcategory [{subcategory_index}] name is required (category: [{}]) ", + category.name + ); + } + + for (item_index, item) in subcategory.items.iter().enumerate() { + // Prepare context for errors + let item_id = if item.name.is_empty() { + format!("{item_index}") + } else { + item.name.clone() + }; + let ctx = format!( + "item [{}] is not valid (category: [{}] | subcategory: [{}])", + item_id, category.name, subcategory.name + ); + + // Check name + if item.name.is_empty() { + return Err(format_err!("name is required")).context(ctx); + } + if items_seen.contains(&item.name) { + return Err(format_err!("duplicate item name")).context(ctx); + } + items_seen.push(item.name.clone()); + + // Check homepage + if item.homepage_url.is_empty() { + return Err(format_err!("hompage_url is required")).context(ctx); + } + + // Check logo + if item.logo.is_empty() { + return Err(format_err!("logo is required")).context(ctx); + } + + // Check some values in extra + if let Some(extra) = &item.extra { + // Check tag name + if let Some(tag) = &extra.tag { + if !TAG_NAME.is_match(tag) { + return Err(format_err!( + "invalid tag (must use only lowercase letters and hyphens)" + )) + .context(ctx); + } + } + } + + // Check urls + validate_urls(item).context(ctx)?; + } + } + } + + Ok(()) + } +} + +/// Landscape category. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(super) struct Category { + pub name: String, + pub subcategories: Vec, +} + +/// Landscape subcategory. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(super) struct SubCategory { + pub name: String, + pub items: Vec, +} + +/// Landscape item (project, product, member, etc). +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(super) struct Item { + pub name: String, + pub homepage_url: String, + pub logo: String, + pub additional_repos: Option>, + pub branch: Option, + pub crunchbase: Option, + pub description: Option, + pub enduser: Option, + pub extra: Option, + pub joined: Option, + pub project: Option, + pub repo_url: Option, + pub second_path: Option>, + pub twitter: Option, + pub url_for_bestpractices: Option, + pub unnamed_organization: Option, +} + +/// Extra information for a landscape item. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(super) struct ItemExtra { + pub accepted: Option, + pub archived: Option, + pub audits: Option>, + pub annual_review_date: Option, + pub annual_review_url: Option, + pub artwork_url: Option, + pub blog_url: Option, + pub chat_channel: Option, + pub clomonitor_name: Option, + pub dev_stats_url: Option, + pub discord_url: Option, + pub docker_url: Option, + pub github_discussions_url: Option, + pub gitter_url: Option, + pub graduated: Option, + pub incubating: Option, + pub linkedin_url: Option, + pub mailing_list_url: Option, + pub parent_project: Option, + pub slack_url: Option, + pub specification: Option, + pub stack_overflow_url: Option, + pub summary_business_use_case: Option, + pub summary_integration: Option, + pub summary_integrations: Option, + pub summary_intro_url: Option, + pub summary_use_case: Option, + pub summary_personas: Option, + pub summary_release_rate: Option, + pub summary_tags: Option, + pub tag: Option, + pub training_certifications: Option, + pub training_type: Option, + pub youtube_url: Option, +} + +/// Landscape item repository. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub(super) struct Repository { + pub repo_url: String, + pub branch: Option, +} + +/// Validate the urls of the item provided. +fn validate_urls(item: &Item) -> Result<()> { + // Check urls in item + let homepage_url = Some(item.homepage_url.clone()); + let urls = [ + ("best_practices", &item.url_for_bestpractices), + ("crunchbase", &item.crunchbase), + ("homepage", &homepage_url), + ("repository", &item.repo_url), + ("twitter", &item.twitter), + ]; + for (name, url) in urls { + validate_url(name, url)?; + } + + // Check additional repositories + if let Some(additional_repos) = &item.additional_repos { + for r in additional_repos { + let repo_url = Some(r.repo_url.clone()); + validate_url("additional_repository", &repo_url)?; + } + } + + // Check urls in item extra + if let Some(extra) = &item.extra { + let urls = [ + ("annual_review", &extra.annual_review_url), + ("artwork", &extra.artwork_url), + ("blog", &extra.blog_url), + ("dev_stats", &extra.dev_stats_url), + ("discord", &extra.discord_url), + ("docker", &extra.docker_url), + ("github_discussions", &extra.github_discussions_url), + ("linkedin", &extra.linkedin_url), + ("mailing_list", &extra.mailing_list_url), + ("slack", &extra.slack_url), + ("stack_overflow", &extra.stack_overflow_url), + ("youtube", &extra.youtube_url), + ]; + for (name, url) in urls { + validate_url(name, url)?; + } + + // Check audits urls + if let Some(audits) = &extra.audits { + for a in audits { + let audit_url = Some(a.url.clone()); + validate_url("audit", &audit_url)?; + } + } + }; + + Ok(()) +} diff --git a/src/build/datasets.rs b/crates/core/src/datasets.rs similarity index 92% rename from src/build/datasets.rs rename to crates/core/src/datasets.rs index 8f94004d..4e88c055 100644 --- a/src/build/datasets.rs +++ b/crates/core/src/datasets.rs @@ -7,15 +7,16 @@ //! consumed by other applications, as they can change at any time. use self::{base::Base, embed::Embed, full::Full}; -use super::{ - crunchbase::CrunchbaseData, github::GithubData, guide::LandscapeGuide, settings::LandscapeSettings, - stats::Stats, LandscapeData, +use crate::{ + data::{CrunchbaseData, GithubData, LandscapeData}, + guide::LandscapeGuide, + settings::LandscapeSettings, + stats::Stats, }; -use anyhow::{Ok, Result}; /// Datasets collection. #[derive(Debug, Clone)] -pub(crate) struct Datasets { +pub struct Datasets { /// #[base] pub base: Base, @@ -25,27 +26,26 @@ pub(crate) struct Datasets { /// #[full] pub full: Full, - /// #[crate::build::stats] + /// #[crate::stats] pub stats: Stats, } impl Datasets { /// Create a new datasets instance. - pub(crate) fn new(i: &NewDatasetsInput) -> Result { - let datasets = Datasets { + #[must_use] + pub fn new(i: &NewDatasetsInput) -> Self { + Datasets { base: Base::new(i.landscape_data, i.settings, i.guide, i.qr_code), embed: Embed::new(i.landscape_data), full: Full::new(i.crunchbase_data, i.github_data, i.landscape_data), stats: Stats::new(i.landscape_data, i.settings, i.crunchbase_data), - }; - - Ok(datasets) + } } } /// Input used to create a new Datasets instance. #[derive(Debug, Clone)] -pub(crate) struct NewDatasetsInput<'a> { +pub struct NewDatasetsInput<'a> { pub crunchbase_data: &'a CrunchbaseData, pub github_data: &'a GithubData, pub guide: &'a Option, @@ -58,8 +58,8 @@ pub(crate) struct NewDatasetsInput<'a> { /// /// This dataset contains the minimal data the web application needs to render /// the initial page and power the features available on it. -mod base { - use crate::build::{ +pub mod base { + use crate::{ data::{self, AdditionalCategory, Category, CategoryName, ItemFeatured, LandscapeData}, guide::LandscapeGuide, settings::{ @@ -70,9 +70,9 @@ mod base { use std::collections::BTreeMap; /// Base dataset information. - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[allow(clippy::struct_field_names)] - pub(crate) struct Base { + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] + pub struct Base { pub finances_available: bool, pub foundation: String, pub qr_code: String, @@ -122,7 +122,8 @@ mod base { impl Base { /// Create a new Base instance from the data and settings provided. - pub(crate) fn new( + #[must_use] + pub fn new( landscape_data: &LandscapeData, settings: &LandscapeSettings, guide: &Option, @@ -202,7 +203,7 @@ mod base { /// Base dataset item information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct Item { + pub struct Item { pub category: String, pub id: String, pub name: String, @@ -250,22 +251,22 @@ mod base { /// /// This dataset contains some information about all the embeddable views that /// can be generated from the information available in the landscape data. -mod embed { +pub mod embed { use super::base::Item; - use crate::build::{data::Category, LandscapeData}; + use crate::data::{Category, LandscapeData}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Embed dataset information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct Embed { + pub struct Embed { #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] pub views: HashMap, } impl Embed { /// Create a new Embed instance from the data provided. - pub(crate) fn new(landscape_data: &LandscapeData) -> Self { + pub fn new(landscape_data: &LandscapeData) -> Self { let mut views = HashMap::new(); for category in &landscape_data.categories { @@ -307,11 +308,11 @@ mod embed { } /// Type alias to represent a embed key. - pub(crate) type EmbedKey = String; + pub type EmbedKey = String; /// Embed view information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct EmbedView { + pub struct EmbedView { pub category: Category, #[serde(skip_serializing_if = "Vec::is_empty")] @@ -324,18 +325,14 @@ mod embed { /// This dataset contains all the information available for the landscape. This /// information is used by the web application to power features that require /// some extra data not available in the base dataset. -mod full { - use crate::build::{ - crunchbase::CrunchbaseData, - data::{Item, LandscapeData}, - github::GithubData, - }; +pub mod full { + use crate::data::{CrunchbaseData, GithubData, Item, LandscapeData}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; /// Full dataset information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] - pub(crate) struct Full { + pub struct Full { #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub crunchbase_data: CrunchbaseData, @@ -348,7 +345,8 @@ mod full { impl Full { /// Create a new Full instance from the landscape data provided. - pub(crate) fn new( + #[must_use] + pub fn new( crunchbase_data: &CrunchbaseData, github_data: &GithubData, landscape_data: &LandscapeData, diff --git a/src/build/guide.rs b/crates/core/src/guide.rs similarity index 88% rename from src/build/guide.rs rename to crates/core/src/guide.rs index 4f05c686..dacdb13a 100644 --- a/src/build/guide.rs +++ b/crates/core/src/guide.rs @@ -1,16 +1,43 @@ //! This module defines the types used to represent the landscape guide content //! that must be provided from a YAML file (guide.yml). -use crate::GuideSource; use anyhow::{bail, format_err, Context, Result}; +use clap::Args; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; -use std::{fs, path::Path}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use tracing::{debug, instrument}; +/// Landscape guide source. +#[derive(Args, Default)] +#[group(required = false, multiple = false)] +pub struct GuideSource { + /// Landscape guide file local path. + #[arg(long)] + pub guide_file: Option, + + /// Landscape guide file url. + #[arg(long)] + pub guide_url: Option, +} + +impl GuideSource { + /// Create a new guide source from the url provided. + #[must_use] + pub fn new_from_url(url: String) -> Self { + Self { + guide_file: None, + guide_url: Some(url), + } + } +} + /// Landscape guide content. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct LandscapeGuide { +pub struct LandscapeGuide { #[serde(skip_serializing_if = "Option::is_none")] pub categories: Option>, } @@ -18,7 +45,7 @@ pub(crate) struct LandscapeGuide { impl LandscapeGuide { /// Create a new landscape guide instance from the source provided. #[instrument(skip_all, err)] - pub(crate) async fn new(src: &GuideSource) -> Result> { + pub async fn new(src: &GuideSource) -> Result> { // Try from file if let Some(file) = &src.guide_file { debug!(?file, "getting landscape guide from file"); @@ -60,7 +87,7 @@ impl LandscapeGuide { /// Create a new landscape guide instance from the YAML string provided. fn new_from_yaml(s: &str) -> Result { // Parse YAML string and validate guide - let mut guide: LandscapeGuide = serde_yaml::from_str(s)?; + let mut guide: LandscapeGuide = serde_yaml::from_str(s).context("invalid yaml file")?; guide.validate().context("the landscape guide file provided is not valid")?; // Convert content fields from markdown to HTML @@ -156,7 +183,7 @@ impl LandscapeGuide { /// Guide category. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Category { +pub struct Category { #[allow(clippy::struct_field_names)] pub category: String, @@ -172,7 +199,7 @@ pub(crate) struct Category { /// Guide subcategory. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Subcategory { +pub struct Subcategory { #[allow(clippy::struct_field_names)] pub subcategory: String, diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 00000000..d387a987 --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,13 @@ +#![warn(clippy::all, clippy::pedantic)] +#![allow( + clippy::doc_markdown, + clippy::blocks_in_conditions, + clippy::module_name_repetitions +)] + +pub mod data; +pub mod datasets; +pub mod guide; +pub mod settings; +pub mod stats; +mod util; diff --git a/src/build/settings.rs b/crates/core/src/settings.rs similarity index 93% rename from src/build/settings.rs rename to crates/core/src/settings.rs index 7eef4666..9f2d1d4d 100644 --- a/src/build/settings.rs +++ b/crates/core/src/settings.rs @@ -7,20 +7,49 @@ //! NOTE: the landscape settings file uses a new format that is not backwards //! compatible with the legacy settings file used by existing landscapes. -use super::data::{normalize_name, validate_url, CategoryName, SubCategoryName}; -use crate::SettingsSource; +use super::data::{CategoryName, SubCategoryName}; +use crate::util::{normalize_name, validate_url}; use anyhow::{bail, format_err, Context, Result}; use chrono::NaiveDate; +use clap::Args; use lazy_static::lazy_static; use regex::Regex; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, fs, path::Path}; +use std::{ + collections::BTreeMap, + fs, + path::{Path, PathBuf}, +}; use tracing::{debug, instrument}; +/// Landscape settings location. +#[derive(Args, Default)] +#[group(required = true, multiple = false)] +pub struct SettingsSource { + /// Landscape settings file local path. + #[arg(long)] + pub settings_file: Option, + + /// Landscape settings file url. + #[arg(long)] + pub settings_url: Option, +} + +impl SettingsSource { + /// Create a new settings source from the url provided. + #[must_use] + pub fn new_from_url(url: String) -> Self { + Self { + settings_file: None, + settings_url: Some(url), + } + } +} + /// Landscape settings. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct LandscapeSettings { +pub struct LandscapeSettings { pub foundation: String, pub url: String, @@ -79,7 +108,7 @@ pub(crate) struct LandscapeSettings { impl LandscapeSettings { /// Create a new landscape settings instance from the source provided. #[instrument(skip_all, err)] - pub(crate) async fn new(src: &SettingsSource) -> Result { + pub async fn new(src: &SettingsSource) -> Result { // Try from file if let Some(file) = &src.settings_file { debug!(?file, "getting landscape settings from file"); @@ -120,7 +149,7 @@ impl LandscapeSettings { /// Create a new landscape settings instance from the raw data provided. fn new_from_raw_data(raw_data: &str) -> Result { - let mut settings: LandscapeSettings = serde_yaml::from_str(raw_data)?; + let mut settings: LandscapeSettings = serde_yaml::from_str(raw_data).context("invalid yaml file")?; settings.validate().context("the landscape settings file provided is not valid")?; settings.footer_text_to_html().context("error converting footer md text to html")?; @@ -455,28 +484,28 @@ impl LandscapeSettings { /// Landscape analytics providers. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Analytics { +pub struct Analytics { #[serde(skip_serializing_if = "Option::is_none")] pub gtm: Option, } /// Landscape category. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Category { +pub struct Category { pub name: CategoryName, pub subcategories: Vec, } lazy_static! { /// RGBA regular expression. - pub(crate) static ref RGBA: Regex = + static ref RGBA: Regex = Regex::new(r"rgba?\(((25[0-5]|2[0-4]\d|1\d{1,2}|\d\d?)\s*,\s*?){2}(25[0-5]|2[0-4]\d|1\d{1,2}|\d\d?)\s*,?\s*([01]\.?\d*?)\)") .expect("exprs in RGBA to be valid"); } /// Colors used across the landscape UI. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Colors { +pub struct Colors { pub color1: String, pub color2: String, pub color3: String, @@ -489,14 +518,14 @@ pub(crate) struct Colors { /// the web application, usually making it larger with some special styling. /// These rules are used to decide which items should be featured. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct FeaturedItemRule { +pub struct FeaturedItemRule { pub field: String, pub options: Vec, } /// Featured item rule option. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct FeaturedItemRuleOption { +pub struct FeaturedItemRuleOption { pub value: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -508,7 +537,7 @@ pub(crate) struct FeaturedItemRuleOption { /// Footer configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Footer { +pub struct Footer { #[serde(skip_serializing_if = "Option::is_none")] pub links: Option, @@ -521,7 +550,7 @@ pub(crate) struct Footer { /// Footer links. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct FooterLinks { +pub struct FooterLinks { #[serde(skip_serializing_if = "Option::is_none")] pub facebook: Option, @@ -558,7 +587,7 @@ pub(crate) struct FooterLinks { /// Google Tag Manager configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct GoogleTagManager { +pub struct GoogleTagManager { #[serde(skip_serializing_if = "Option::is_none")] pub container_id: Option, } @@ -566,7 +595,7 @@ pub(crate) struct GoogleTagManager { /// Grid items size. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] -pub(crate) enum GridItemsSize { +pub enum GridItemsSize { Small, Medium, Large, @@ -575,7 +604,7 @@ pub(crate) enum GridItemsSize { /// Landscape group. A group provides a mechanism to organize sets of /// categories in the web application. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Group { +pub struct Group { pub name: String, pub normalized_name: Option, pub categories: Vec, @@ -583,7 +612,7 @@ pub(crate) struct Group { /// Header configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Header { +pub struct Header { #[serde(skip_serializing_if = "Option::is_none")] pub links: Option, @@ -593,14 +622,14 @@ pub(crate) struct Header { /// Header links. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct HeaderLinks { +pub struct HeaderLinks { #[serde(skip_serializing_if = "Option::is_none")] pub github: Option, } /// Images urls. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Images { +pub struct Images { #[serde(skip_serializing_if = "Option::is_none")] pub favicon: Option, @@ -610,7 +639,7 @@ pub(crate) struct Images { /// Logos viewbox configuration. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub(crate) struct LogosViewbox { +pub struct LogosViewbox { pub adjust: bool, pub exclude: Vec, } @@ -626,17 +655,17 @@ impl Default for LogosViewbox { /// Osano configuration. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Osano { +pub struct Osano { pub customer_id: String, pub customer_configuration_id: String, } /// Type alias to represent a TAG name. -pub(crate) type TagName = String; +pub type TagName = String; /// TAG rule used to set the TAG that owns a project automatically. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct TagRule { +pub struct TagRule { pub category: CategoryName, #[serde(skip_serializing_if = "Option::is_none")] @@ -645,7 +674,7 @@ pub(crate) struct TagRule { /// Upcoming event details. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct UpcomingEvent { +pub struct UpcomingEvent { pub name: String, pub start: NaiveDate, pub end: NaiveDate, @@ -656,7 +685,7 @@ pub(crate) struct UpcomingEvent { /// Default view mode used in the web application. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] -pub(crate) enum ViewMode { +pub enum ViewMode { Grid, Card, } diff --git a/src/build/stats.rs b/crates/core/src/stats.rs similarity index 90% rename from src/build/stats.rs rename to crates/core/src/stats.rs index 6865981e..6968f8d3 100644 --- a/src/build/stats.rs +++ b/crates/core/src/stats.rs @@ -2,11 +2,10 @@ //! as well as the functionality used to prepare them. use super::{ - crunchbase::CrunchbaseData, data::{CategoryName, SubCategoryName}, settings::{LandscapeSettings, TagName}, - LandscapeData, }; +use crate::data::{CrunchbaseData, LandscapeData}; use chrono::{Datelike, Utc}; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -23,28 +22,29 @@ type YearMonth = String; /// Landscape stats. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Stats { +pub struct Stats { /// Foundation members stats. #[serde(skip_serializing_if = "Option::is_none")] - members: Option, + pub members: Option, /// Foundation organizations stats. #[serde(skip_serializing_if = "Option::is_none")] - organizations: Option, + pub organizations: Option, /// Foundation projects stats. #[serde(skip_serializing_if = "Option::is_none")] - projects: Option, + pub projects: Option, /// Repositories stats. #[serde(skip_serializing_if = "Option::is_none")] - repositories: Option, + pub repositories: Option, } impl Stats { /// Create a new Stats instance from the information available in the /// landscape. - pub(crate) fn new( + #[must_use] + pub fn new( landscape_data: &LandscapeData, settings: &LandscapeSettings, crunchbase_data: &CrunchbaseData, @@ -60,21 +60,21 @@ impl Stats { /// Some stats about the foundation's members. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct MembersStats { +pub struct MembersStats { /// Number of members joined per year-month. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - joined_at: BTreeMap, + pub joined_at: BTreeMap, /// Running total of number of members joined per year-month. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - joined_at_rt: BTreeMap, + pub joined_at_rt: BTreeMap, /// Total number of members. - members: u64, + pub members: u64, /// Number of members per subcategory. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - subcategories: BTreeMap, + pub subcategories: BTreeMap, } impl MembersStats { @@ -113,22 +113,22 @@ impl MembersStats { /// Some stats about the organizations in the landscape. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct OrganizationsStats { +pub struct OrganizationsStats { /// Total number of acquisitions per year across all organizations. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - acquisitions: BTreeMap, + pub acquisitions: BTreeMap, /// Total acquisitions price per year across all organizations. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - acquisitions_price: BTreeMap, + pub acquisitions_price: BTreeMap, /// Total number of funding rounds per year across all organizations. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - funding_rounds: BTreeMap, + pub funding_rounds: BTreeMap, /// Total money raised on funding rounds per year across all organizations. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - funding_rounds_money_raised: BTreeMap, + pub funding_rounds_money_raised: BTreeMap, } impl OrganizationsStats { @@ -185,45 +185,45 @@ impl OrganizationsStats { /// Some stats about the landscape projects. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct ProjectsStats { +pub struct ProjectsStats { /// Number of projects accepted per year-month. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - accepted_at: BTreeMap, + pub accepted_at: BTreeMap, /// Running total of number of projects accepted per year-month. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - accepted_at_rt: BTreeMap, + pub accepted_at_rt: BTreeMap, /// Number of security audits per year-month. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - audits: BTreeMap, + pub audits: BTreeMap, /// Running total of number of security audits per year-month. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - audits_rt: BTreeMap, + pub audits_rt: BTreeMap, /// Number of projects per category and subcategory. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - category: BTreeMap, + pub category: BTreeMap, /// Promotions from incubating to graduated per year-month. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - incubating_to_graduated: BTreeMap, + pub incubating_to_graduated: BTreeMap, /// Number of projects per maturity. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - maturity: BTreeMap, + pub maturity: BTreeMap, /// Total number of projects. - projects: u64, + pub projects: u64, /// Promotions from sandbox to incubating per year-month. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - sandbox_to_incubating: BTreeMap, + pub sandbox_to_incubating: BTreeMap, /// Number of projects per TAG. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - tag: BTreeMap, + pub tag: BTreeMap, } impl ProjectsStats { @@ -312,45 +312,45 @@ impl ProjectsStats { /// Some stats about the projects in a category and its subcategories. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct CategoryProjectsStats { +pub struct CategoryProjectsStats { /// Number of projects in the category. - projects: u64, + pub projects: u64, /// Number of projects per subcategory. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - subcategories: BTreeMap, + pub subcategories: BTreeMap, } /// Some stats about the repositories listed in the landscape. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct RepositoriesStats { +pub struct RepositoriesStats { /// Source code bytes. - bytes: u64, + pub bytes: u64, /// Number of contributors. - contributors: u64, + pub contributors: u64, /// Number of repositories where each language is used. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - languages: BTreeMap, + pub languages: BTreeMap, /// Source code bytes written on each language. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - languages_bytes: BTreeMap, + pub languages_bytes: BTreeMap, /// Number of repositories where each license is used. #[serde(skip_serializing_if = "BTreeMap::is_empty")] - licenses: BTreeMap, + pub licenses: BTreeMap, /// Number of commits per week over the last year. #[serde(skip_serializing_if = "Vec::is_empty")] - participation_stats: Vec, + pub participation_stats: Vec, /// Number of repositories. - repositories: u64, + pub repositories: u64, /// Number of stars. - stars: u64, + pub stars: u64, } impl RepositoriesStats { diff --git a/crates/core/src/util.rs b/crates/core/src/util.rs new file mode 100644 index 00000000..8d494f41 --- /dev/null +++ b/crates/core/src/util.rs @@ -0,0 +1,85 @@ +//! This module provides some utility functions. + +use anyhow::{bail, Result}; +use lazy_static::lazy_static; +use regex::Regex; +use url::Url; + +lazy_static! { + /// Crunchbase url regular expression. + static ref CRUNCHBASE_URL: Regex = + Regex::new("^https://www.crunchbase.com/organization/(?P[^/]+)/?$") + .expect("exprs in CRUNCHBASE_URL to be valid"); + + /// Regular expression to match multiple hyphens. + static ref MULTIPLE_HYPHENS: Regex = Regex::new(r"-{2,}").expect("exprs in MULTIPLE_HYPHENS to be valid"); + + /// Characters allowed in normalized names. + static ref VALID_CHARS: Regex = Regex::new(r"[a-z0-9\-\ \+]").expect("exprs in VALID_CHARS to be valid"); +} + +/// Normalize category, subcategory and item name. +pub(crate) fn normalize_name(value: &str) -> String { + let mut normalized_name = value + .to_lowercase() + .replace(' ', "-") + .chars() + .map(|c| { + if VALID_CHARS.is_match(&c.to_string()) { + c + } else { + '-' + } + }) + .collect::(); + normalized_name = MULTIPLE_HYPHENS.replace(&normalized_name, "-").to_string(); + if let Some(normalized_name_without_suffix) = normalized_name.strip_suffix('-') { + normalized_name = normalized_name_without_suffix.to_string(); + } + normalized_name +} + +/// Validate the url provided. +pub(crate) fn validate_url(kind: &str, url: &Option) -> Result<()> { + if let Some(url) = url { + let invalid_url = |reason: &str| bail!("invalid {kind} url: {reason}"); + + // Parse url + let url = match Url::parse(url) { + Ok(url) => url, + Err(err) => return invalid_url(&err.to_string()), + }; + + // Check scheme + if url.scheme() != "http" && url.scheme() != "https" { + return invalid_url("invalid scheme"); + } + + // Some checks specific to the url kind provided + let check_domain = |domain: &str| { + if url.host_str().is_some_and(|host| !host.ends_with(domain)) { + return invalid_url(&format!("expecting https://{domain}/...")); + } + Ok(()) + }; + match kind { + "crunchbase" => { + if !CRUNCHBASE_URL.is_match(url.as_str()) { + return invalid_url(&format!("expecting: {}", CRUNCHBASE_URL.as_str())); + } + } + "facebook" => return check_domain("facebook.com"), + "flickr" => return check_domain("flickr.com"), + "github" => return check_domain("github.com"), + "instagram" => return check_domain("instagram.com"), + "linkedin" => return check_domain("linkedin.com"), + "stack_overflow" => return check_domain("stackoverflow.com"), + "twitch" => return check_domain("twitch.tv"), + "twitter" => return check_domain("twitter.com"), + "youtube" => return check_domain("youtube.com"), + _ => {} + } + } + + Ok(()) +} diff --git a/crates/wasm/overlay/Cargo.toml b/crates/wasm/overlay/Cargo.toml new file mode 100644 index 00000000..a4cb49a9 --- /dev/null +++ b/crates/wasm/overlay/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "landscape2-overlay" +description = "Landscape2 overlay data provider" +repository = "https://github.com/cncf/landscape2" +readme = "../../../README.md" +version.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +homepage.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = { workspace = true } +landscape2-core = { path = "../../core" } +reqwest = { workspace = true, features = ["json"] } +serde = { workspace = true } +serde_json = { workspace = true } +serde-wasm-bindgen = { workspace = true } +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } diff --git a/crates/wasm/overlay/overlay-test.html b/crates/wasm/overlay/overlay-test.html new file mode 100644 index 00000000..1399c8f6 --- /dev/null +++ b/crates/wasm/overlay/overlay-test.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/crates/wasm/overlay/src/lib.rs b/crates/wasm/overlay/src/lib.rs new file mode 100644 index 00000000..24e99257 --- /dev/null +++ b/crates/wasm/overlay/src/lib.rs @@ -0,0 +1,159 @@ +//! This module is in charge of preparing the overlay data. + +use anyhow::{bail, Context, Result}; +use landscape2_core::{ + data::{DataSource, Item, LandscapeData}, + datasets::{base::Base, full::Full}, + guide::{GuideSource, LandscapeGuide}, + settings::{LandscapeSettings, SettingsSource}, + stats::Stats, +}; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +/// Path of the deployed full dataset file. +const FULL_DATASET_PATH: &str = "data/full.json"; + +/// Path of the default data file. +const DEFAULT_DATA_PATH: &str = "sources/data.yml"; + +/// Path of the default settings file. +const DEFAULT_SETTINGS_PATH: &str = "sources/settings.yml"; + +/// Path of the default guide file. +const DEFAULT_GUIDE_PATH: &str = "sources/guide.yml"; + +/// Input data for the get_overlay_data function. +#[derive(Deserialize)] +struct GetOverlayDataInput { + landscape_url: String, + + data_url: Option, + guide_url: Option, + logos_url: Option, + settings_url: Option, +} + +/// Overlay data. +#[derive(Serialize)] +struct OverlayData { + datasets: Datasets, + guide: Option, +} + +/// Datasets included in the overlay data. +#[derive(Serialize)] +struct Datasets { + base: Base, + full: Full, + stats: Stats, +} + +/// Prepare and return the overlay data. +#[wasm_bindgen] +pub async fn get_overlay_data(input: JsValue) -> Result { + // Parse input data + let mut input: GetOverlayDataInput = serde_wasm_bindgen::from_value(input).map_err(to_str)?; + input.landscape_url = input.landscape_url.trim_end_matches('/').to_string(); + + // Get landscape data + let default_data_url = format!("{}/{}", input.landscape_url, DEFAULT_DATA_PATH); + let data_url = input.data_url.unwrap_or(default_data_url); + let landscape_data_src = DataSource::new_from_url(data_url); + let mut landscape_data = LandscapeData::new(&landscape_data_src) + .await + .context("error fetching landscape data") + .map_err(to_str)?; + + // Get landscape settings + let default_settings_url = format!("{}/{}", input.landscape_url, DEFAULT_SETTINGS_PATH); + let settings_url = input.settings_url.unwrap_or(default_settings_url); + let settings_src = SettingsSource::new_from_url(settings_url); + let settings = LandscapeSettings::new(&settings_src) + .await + .context("error fetching settings") + .map_err(to_str)?; + + // Get landscape guide + let default_guide_url = format!("{}/{}", input.landscape_url, DEFAULT_GUIDE_PATH); + let guide_url = input.guide_url.unwrap_or(default_guide_url); + let guide_src = GuideSource::new_from_url(guide_url); + let guide = LandscapeGuide::new(&guide_src).await.context("error fetching guide").map_err(to_str)?; + + // Get Crunchbase and GitHub data from deployed full dataset + let deployed_full_dataset = get_full_dataset(&input.landscape_url).await.map_err(to_str)?; + let deployed_items = deployed_full_dataset.items; + let crunchbase_data = deployed_full_dataset.crunchbase_data; + let github_data = deployed_full_dataset.github_data; + + // Enrich landscape data with some extra information + landscape_data.add_crunchbase_data(&crunchbase_data); + landscape_data.add_featured_items_data(&settings); + landscape_data.add_github_data(&github_data); + landscape_data.add_member_subcategory(&settings.members_category); + landscape_data.add_tags(&settings); + set_clomonitor_report_summary(&mut landscape_data, &deployed_items); + set_logos_url(&mut landscape_data, input.logos_url, &deployed_items); + + // Prepare datasets + let qr_code = String::new(); + let datasets = Datasets { + base: Base::new(&landscape_data, &settings, &guide, &qr_code), + full: Full::new(&crunchbase_data, &github_data, &landscape_data), + stats: Stats::new(&landscape_data, &settings, &crunchbase_data), + }; + + // Prepare overlay data and return it + let overlay_data = OverlayData { datasets, guide }; + let overlay_data_json = serde_json::to_string(&overlay_data).map_err(to_str)?; + + Ok(overlay_data_json) +} + +/// Get landscape currently deployed full dataset. +async fn get_full_dataset(landscape_url: &str) -> Result { + let url = format!("{}/{}", landscape_url, FULL_DATASET_PATH); + let resp = reqwest::get(&url).await.context("error getting full dataset")?; + if resp.status() != StatusCode::OK { + bail!("unexpected status code getting full dataset: {}", resp.status()); + } + let full: Full = resp.json().await?; + Ok(full) +} + +/// Set logos url for all items in the landscape based on the base logos url +/// provided. +fn set_clomonitor_report_summary(landscape_data: &mut LandscapeData, deployed_items: &[Item]) { + for item in &mut landscape_data.items { + item.clomonitor_report_summary = deployed_items + .iter() + .find(|x| x.id == item.id) + .and_then(|x| x.clomonitor_report_summary.clone()); + } +} + +/// Set logos url for all items in the landscape. +/// +/// If a logos url is provided, it will be used to set the logo url for all +/// items. Otherwise, we'll use the current logo url from the deployed items. +fn set_logos_url(landscape_data: &mut LandscapeData, logos_url: Option, deployed_items: &[Item]) { + if let Some(logos_url) = logos_url { + for item in &mut landscape_data.items { + item.logo = format!("{}/{}", logos_url.trim_end_matches('/'), item.logo); + } + } else { + for item in &mut landscape_data.items { + item.logo = deployed_items + .iter() + .find(|x| x.id == item.id) + .map(|x| x.logo.clone()) + .unwrap_or_default(); + } + } +} + +/// Helper function to convert an error to a string. +fn to_str(err: E) -> String { + format!("{:?}", err) +} diff --git a/src/build/projects.rs b/src/build/projects.rs deleted file mode 100644 index 1a0b8c14..00000000 --- a/src/build/projects.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! This module defines the functionality to generate the `projects.md` and -//! `projects.csv` files from the information available in the landscape. - -use super::{data::DATE_FORMAT, LandscapeData}; -use anyhow::Result; -use askama::Template; -use chrono::NaiveDate; -use serde::{Deserialize, Serialize}; -use std::fs::File; - -/// Project information used to generate the projects.md and projects.csv files. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] -pub(crate) struct Project { - pub accepted_at: String, - pub archived_at: String, - pub graduated_at: String, - pub homepage_url: String, - pub incubating_at: String, - pub last_security_audit: String, - pub maturity: String, - pub name: String, - pub num_security_audits: String, - pub sandbox_at: String, - pub tag: String, -} - -impl From<&LandscapeData> for Vec { - fn from(landscape_data: &LandscapeData) -> Self { - // Helper closure to format dates - let fmt_date = |date: &Option| { - let Some(date) = date else { - return String::new(); - }; - date.format(DATE_FORMAT).to_string() - }; - - // Collect projects from landscape data - let mut projects: Vec = landscape_data - .items - .iter() - .cloned() - .filter_map(|item| { - // Prepare maturity and tag - let maturity = item.maturity?; - let tag = item.tag?; - - // Prepare sandbox date - let sandbox_at = if item.accepted_at == item.incubating_at { - None - } else { - item.accepted_at - }; - - // Prepare security audits info - let last_security_audit = item.audits.as_ref().and_then(|a| a.last().map(|a| a.date)); - let num_security_audits = item.audits.as_ref().map(Vec::len); - - // Create project instance and return it - let project = Project { - accepted_at: fmt_date(&item.accepted_at), - archived_at: fmt_date(&item.archived_at), - graduated_at: fmt_date(&item.graduated_at), - homepage_url: item.homepage_url, - incubating_at: fmt_date(&item.incubating_at), - maturity: maturity.to_string(), - name: item.name.to_lowercase(), - num_security_audits: num_security_audits.unwrap_or_default().to_string(), - last_security_audit: fmt_date(&last_security_audit), - sandbox_at: fmt_date(&sandbox_at), - tag: tag.to_string(), - }; - Some(project) - }) - .collect(); - - // Sort projects - projects.sort_by(|a, b| a.name.cmp(&b.name)); - - projects - } -} - -/// Template for the projects.md file. -#[derive(Debug, Clone, Template)] -#[template(path = "projects.md")] -pub(crate) struct ProjectsMd<'a> { - pub projects: &'a [Project], -} - -/// Generate CSV file with some information about each project. -pub(crate) fn generate_projects_csv(mut w: csv::Writer, projects: &[Project]) -> Result<()> { - // Write headers - w.write_record([ - "project_name", - "maturity", - "tag", - "accepted_date", - "sandbox_date", - "incubating_date", - "graduated_date", - "archived_date", - "num_security_audits", - "last_security_audit_date", - ])?; - - // Write one record for each project - for p in projects { - w.write_record([ - &p.name, - &p.maturity, - &p.tag, - &p.accepted_at, - &p.sandbox_at, - &p.incubating_at, - &p.graduated_at, - &p.archived_at, - &p.num_security_audits, - &p.last_security_audit, - ])?; - } - - w.flush()?; - Ok(()) -} diff --git a/src/deploy/mod.rs b/src/deploy/mod.rs deleted file mode 100644 index 76b4117f..00000000 --- a/src/deploy/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! This module defines the functionality of the deploy CLI subcommand. - -pub(crate) mod s3; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 6b02f23a..00000000 --- a/src/main.rs +++ /dev/null @@ -1,243 +0,0 @@ -#![warn(clippy::all, clippy::pedantic)] -#![allow(clippy::doc_markdown, clippy::blocks_in_conditions)] - -use anyhow::Result; -use build::build; -use clap::{Args, Parser, Subcommand}; -use deploy::s3; -use new::new; -use serve::serve; -use std::path::PathBuf; -use validate::{validate_data, validate_guide, validate_settings}; - -mod build; -mod deploy; -mod new; -mod serve; -mod validate; - -/// CLI arguments. -#[derive(Parser)] -#[command( - version, - about = "Landscape2 CLI tool - -https://github.com/cncf/landscape2#usage" -)] -struct Cli { - #[command(subcommand)] - command: Command, -} - -/// Commands available. -#[derive(Subcommand)] -enum Command { - /// Build landscape website. - Build(BuildArgs), - - /// Deploy landscape website (experimental). - Deploy(DeployArgs), - - /// Create a new landscape from the built-in template. - New(NewArgs), - - /// Serve landscape website. - Serve(ServeArgs), - - /// Validate landscape data sources files. - Validate(ValidateArgs), -} - -/// Build command arguments. -#[derive(Args)] -struct BuildArgs { - /// Cache directory. - #[arg(long)] - cache_dir: Option, - - /// Data source. - #[command(flatten)] - data_source: DataSource, - - /// Guide source. - #[command(flatten)] - guide_source: GuideSource, - - /// Logos source. - #[command(flatten)] - logos_source: LogosSource, - - /// Output directory to write files to. - #[arg(long)] - output_dir: PathBuf, - - /// Settings source. - #[command(flatten)] - settings_source: SettingsSource, -} - -/// Landscape data location. -#[derive(Args)] -#[group(required = true, multiple = false)] -struct DataSource { - /// Landscape data file local path. - #[arg(long)] - data_file: Option, - - /// Landscape data file url. - #[arg(long)] - data_url: Option, -} - -/// Landscape guide location. -#[derive(Args)] -#[group(required = false, multiple = false)] -struct GuideSource { - /// Landscape guide file local path. - #[arg(long)] - guide_file: Option, - - /// Landscape guide file url. - #[arg(long)] - guide_url: Option, -} - -/// Landscape logos location. -#[derive(Args, Clone)] -#[group(required = true, multiple = false)] -struct LogosSource { - /// Local path where the logos are stored. - #[arg(long)] - logos_path: Option, - - /// Base URL where the logos are hosted. - #[arg(long)] - logos_url: Option, -} - -/// Landscape settings location. -#[derive(Args)] -#[group(required = true, multiple = false)] -struct SettingsSource { - /// Landscape settings file local path. - #[arg(long)] - settings_file: Option, - - /// Landscape settings file url. - #[arg(long)] - settings_url: Option, -} - -/// Deploy command arguments. -#[derive(Args)] -#[command(args_conflicts_with_subcommands = true)] -struct DeployArgs { - /// Provider used to deploy the landscape website. - #[command(subcommand)] - provider: Provider, -} - -/// Provider used to deploy the landscape website. -#[derive(Subcommand)] -enum Provider { - /// Deploy landscape website to AWS S3. - S3(S3Args), -} - -/// AWS S3 provider arguments. -#[derive(Args)] -struct S3Args { - /// Bucket to copy the landscape website files to. - #[arg(long)] - bucket: String, - - /// Location of the landscape website files (build subcommand output). - #[arg(long)] - landscape_dir: PathBuf, -} - -/// New command arguments. -#[derive(Args)] -struct NewArgs { - /// Output directory to write files to. - #[arg(long)] - output_dir: PathBuf, -} - -/// Serve command arguments. -#[derive(Args)] -struct ServeArgs { - /// Address the web server will listen on. - #[arg(long, default_value = "127.0.0.1:8000")] - addr: String, - - /// Whether the server should stop gracefully or not. - #[arg(long, default_value_t = false)] - graceful_shutdown: bool, - - /// Location of the landscape website files (build subcommand output). - /// The current path will be used when none is provided. - #[arg(long)] - landscape_dir: Option, - - /// Enable silent mode. - #[arg(long, default_value_t = false)] - silent: bool, -} - -/// Validate command arguments. -#[derive(Args)] -#[command(args_conflicts_with_subcommands = true)] -struct ValidateArgs { - /// Landscape file to validate. - #[command(subcommand)] - target: ValidateTarget, -} - -/// Landscape file to validate. -#[derive(Subcommand)] -enum ValidateTarget { - /// Validate landscape data file. - Data(DataSource), - - /// Validate landscape guide file. - Guide(GuideSource), - - /// Validate landscape settings file. - Settings(SettingsSource), -} - -#[tokio::main] -async fn main() -> Result<()> { - let cli = Cli::parse(); - - // Setup logging - match &cli.command { - Command::Build(_) | Command::Deploy(_) | Command::New(_) | Command::Serve(_) => { - if std::env::var_os("RUST_LOG").is_none() { - std::env::set_var("RUST_LOG", "landscape2=debug"); - } - tracing_subscriber::fmt::init(); - } - Command::Validate(_) => {} - } - - // Run command - match &cli.command { - Command::Build(args) => build(args).await?, - Command::Deploy(args) => { - match &args.provider { - Provider::S3(args) => s3::deploy(args).await?, - }; - } - Command::New(args) => new(args)?, - Command::Serve(args) => serve(args).await?, - Command::Validate(args) => match &args.target { - ValidateTarget::Data(src) => validate_data(src).await?, - ValidateTarget::Guide(src) => validate_guide(src).await?, - ValidateTarget::Settings(src) => validate_settings(src).await?, - }, - } - - Ok(()) -} diff --git a/embed/.eslintrc b/ui/embed/.eslintrc similarity index 100% rename from embed/.eslintrc rename to ui/embed/.eslintrc diff --git a/embed/.prettierignore b/ui/embed/.prettierignore similarity index 100% rename from embed/.prettierignore rename to ui/embed/.prettierignore diff --git a/embed/.prettierrc b/ui/embed/.prettierrc similarity index 100% rename from embed/.prettierrc rename to ui/embed/.prettierrc diff --git a/embed/embed.html b/ui/embed/embed.html similarity index 100% rename from embed/embed.html rename to ui/embed/embed.html diff --git a/embed/env.d.ts b/ui/embed/env.d.ts similarity index 100% rename from embed/env.d.ts rename to ui/embed/env.d.ts diff --git a/embed/package.json b/ui/embed/package.json similarity index 100% rename from embed/package.json rename to ui/embed/package.json diff --git a/embed/public/assets/iframeResizer.contentWindow-v4.3.9.min.js b/ui/embed/public/assets/iframeResizer.contentWindow-v4.3.9.min.js similarity index 100% rename from embed/public/assets/iframeResizer.contentWindow-v4.3.9.min.js rename to ui/embed/public/assets/iframeResizer.contentWindow-v4.3.9.min.js diff --git a/embed/src/App.tsx b/ui/embed/src/App.tsx similarity index 100% rename from embed/src/App.tsx rename to ui/embed/src/App.tsx diff --git a/embed/src/common/ExternalLink.tsx b/ui/embed/src/common/ExternalLink.tsx similarity index 100% rename from embed/src/common/ExternalLink.tsx rename to ui/embed/src/common/ExternalLink.tsx diff --git a/embed/src/common/GridItem.tsx b/ui/embed/src/common/GridItem.tsx similarity index 100% rename from embed/src/common/GridItem.tsx rename to ui/embed/src/common/GridItem.tsx diff --git a/embed/src/common/Image.tsx b/ui/embed/src/common/Image.tsx similarity index 100% rename from embed/src/common/Image.tsx rename to ui/embed/src/common/Image.tsx diff --git a/embed/src/common/Loading.tsx b/ui/embed/src/common/Loading.tsx similarity index 100% rename from embed/src/common/Loading.tsx rename to ui/embed/src/common/Loading.tsx diff --git a/embed/src/common/NoData.tsx b/ui/embed/src/common/NoData.tsx similarity index 100% rename from embed/src/common/NoData.tsx rename to ui/embed/src/common/NoData.tsx diff --git a/embed/src/common/StyleView.tsx b/ui/embed/src/common/StyleView.tsx similarity index 100% rename from embed/src/common/StyleView.tsx rename to ui/embed/src/common/StyleView.tsx diff --git a/embed/src/index.tsx b/ui/embed/src/index.tsx similarity index 100% rename from embed/src/index.tsx rename to ui/embed/src/index.tsx diff --git a/embed/src/types.ts b/ui/embed/src/types.ts similarity index 100% rename from embed/src/types.ts rename to ui/embed/src/types.ts diff --git a/embed/src/utils/getUrl.tsx b/ui/embed/src/utils/getUrl.tsx similarity index 100% rename from embed/src/utils/getUrl.tsx rename to ui/embed/src/utils/getUrl.tsx diff --git a/embed/src/vite-env.d.ts b/ui/embed/src/vite-env.d.ts similarity index 100% rename from embed/src/vite-env.d.ts rename to ui/embed/src/vite-env.d.ts diff --git a/embed/src/window.d.ts b/ui/embed/src/window.d.ts similarity index 100% rename from embed/src/window.d.ts rename to ui/embed/src/window.d.ts diff --git a/embed/tsconfig.json b/ui/embed/tsconfig.json similarity index 100% rename from embed/tsconfig.json rename to ui/embed/tsconfig.json diff --git a/embed/tsconfig.node.json b/ui/embed/tsconfig.node.json similarity index 100% rename from embed/tsconfig.node.json rename to ui/embed/tsconfig.node.json diff --git a/embed/vite.config.ts b/ui/embed/vite.config.ts similarity index 100% rename from embed/vite.config.ts rename to ui/embed/vite.config.ts diff --git a/embed/yarn.lock b/ui/embed/yarn.lock similarity index 100% rename from embed/yarn.lock rename to ui/embed/yarn.lock diff --git a/web/.eslintrc b/ui/webapp/.eslintrc similarity index 100% rename from web/.eslintrc rename to ui/webapp/.eslintrc diff --git a/web/.gitignore b/ui/webapp/.gitignore similarity index 100% rename from web/.gitignore rename to ui/webapp/.gitignore diff --git a/web/.prettierrc b/ui/webapp/.prettierrc similarity index 100% rename from web/.prettierrc rename to ui/webapp/.prettierrc diff --git a/web/index.html b/ui/webapp/index.html similarity index 100% rename from web/index.html rename to ui/webapp/index.html diff --git a/web/package.json b/ui/webapp/package.json similarity index 100% rename from web/package.json rename to ui/webapp/package.json diff --git a/web/public/assets/gtm.js b/ui/webapp/public/assets/gtm.js similarity index 100% rename from web/public/assets/gtm.js rename to ui/webapp/public/assets/gtm.js diff --git a/web/src/App.css b/ui/webapp/src/App.css similarity index 97% rename from web/src/App.css rename to ui/webapp/src/App.css index 5ba13344..ba102aa5 100644 --- a/web/src/App.css +++ b/ui/webapp/src/App.css @@ -91,6 +91,10 @@ a, overflow: auto; } + .overlay-active #landscape { + height: 100%; + } + .noScroll-sidebar, .noScroll-modal, .noScroll-loading, diff --git a/ui/webapp/src/App.module.css b/ui/webapp/src/App.module.css new file mode 100644 index 00000000..4fd6f155 --- /dev/null +++ b/ui/webapp/src/App.module.css @@ -0,0 +1,49 @@ +.alertWrapper { + z-index: 1040; +} + +.alert { + font-size: 0.8rem; + border: 0; + border: 1px solid var(--bs-warning-border-subtle); + border-width: 1px 0 1px 0; + height: var(--overlay-alert-height); + cursor: none; +} + +.legend { + font-size: 2.5rem; + margin-top: 2rem !important; +} + +.errorAlert { + width: 800px; + max-width: 90%; + max-height: 80%; + height: 500px; + margin-top: var(--overlay-alert-height); +} + +.errorAlertHeading { + font-size: 1.5rem; +} + +.preWrapper { + margin-top: 2.5rem; + max-height: calc(100% - 9rem); +} + +.pre { + background-color: rgba(0, 0, 0, 0.85); + font-size: 0.8rem; +} + +@media only screen and (max-width: 767.98px) { + .errorAlertHeading { + font-size: 1.2rem; + } + + .legend { + font-size: 1.5rem; + } +} diff --git a/ui/webapp/src/App.tsx b/ui/webapp/src/App.tsx new file mode 100644 index 00000000..709165f4 --- /dev/null +++ b/ui/webapp/src/App.tsx @@ -0,0 +1,170 @@ +import { Route, Router } from '@solidjs/router'; +import isUndefined from 'lodash/isUndefined'; +import range from 'lodash/range'; +import { batch, createEffect, createSignal, on, onMount, Show } from 'solid-js'; + +import styles from './App.module.css'; +import { + EMBED_SETUP_PATH, + EXPLORE_PATH, + FINANCES_PATH, + GUIDE_PATH, + LOGOS_PREVIEW_PATH, + SCREENSHOTS_PATH, + STATS_PATH, +} from './data'; +import Layout from './layout'; +import Loading from './layout/common/Loading'; +import Explore from './layout/explore'; +import Finances from './layout/finances'; +import Guide from './layout/guide'; +import Logos from './layout/logos'; +import NotFound from './layout/notFound'; +import Screenshots from './layout/screenshots'; +import Stats from './layout/stats'; +import { BaseData } from './types'; +import itemsDataGetter from './utils/itemsDataGetter'; +import overlayData from './utils/overlayData'; +import updateAlphaInColor from './utils/updateAlphaInColor'; + +// Colors +let COLOR_1 = 'rgba(0, 107, 204, 1)'; +let COLOR_1_HOVER = 'rgba(0, 107, 204, 0.75)'; +let COLOR_2 = 'rgba(255, 0, 170, 1)'; +let COLOR_3 = 'rgba(96, 149, 214, 1)'; +let COLOR_4 = 'rgba(0, 42, 81, 0.7)'; +let COLOR_5 = 'rgba(1, 107, 204, 0.7)'; +let COLOR_6 = 'rgba(0, 42, 81, 0.7)'; + +const App = () => { + const [isOverlay, setIsOverlay] = createSignal(); + const [data, setData] = createSignal(); + const [loadingOverlay, setLoadingOverlay] = createSignal(false); + const [error, setError] = createSignal(); + + async function fetchOverlayData() { + try { + const data = await overlayData.getOverlayBaseData(); + if (data) { + loadColors(); + setData(data); + } else { + setError('No data found.'); + } + } catch (e) { + setError(e as string); + } + setLoadingOverlay(false); + } + + const loadColors = () => { + if (!isUndefined(window.baseDS) && !isUndefined(window.baseDS.colors)) { + if (!isUndefined(window.baseDS.colors?.color1)) { + COLOR_1 = window.baseDS.colors?.color1; + COLOR_1_HOVER = updateAlphaInColor(COLOR_1, 0.75); + } + if (!isUndefined(window.baseDS.colors?.color2)) { + COLOR_2 = window.baseDS.colors?.color2; + } + if (!isUndefined(window.baseDS.colors?.color3)) { + COLOR_3 = window.baseDS.colors?.color3; + } + if (!isUndefined(window.baseDS.colors?.color4)) { + COLOR_4 = window.baseDS.colors?.color4; + } + if (!isUndefined(window.baseDS.colors?.color5)) { + COLOR_5 = window.baseDS.colors?.color5; + } + if (!isUndefined(window.baseDS.colors?.color6)) { + COLOR_6 = window.baseDS.colors?.color6; + } + + const colors = [COLOR_1, COLOR_2, COLOR_3, COLOR_4, COLOR_5, COLOR_6]; + + range(colors.length).forEach((i: number) => { + document.documentElement.style.setProperty(`--color${i + 1}`, colors[i]); + }); + document.documentElement.style.setProperty('--color1-hover', COLOR_1_HOVER); + } + }; + + createEffect( + on(isOverlay, () => { + if (!isUndefined(isOverlay())) { + if (!isOverlay()) { + itemsDataGetter.init(); + } else { + fetchOverlayData(); + } + } + }) + ); + + onMount(() => { + const isOverlayActive = overlayData.checkIfOverlayInQuery(); + if (!isOverlayActive) { + setData(window.baseDS); + } else { + setLoadingOverlay(true); + } + + batch(() => { + setIsOverlay(isOverlayActive); + loadColors(); + }); + + if (window.Osano) { + window.Osano.cm.addEventListener('osano-cm-initialized', () => { + document.body.classList.add('osano-loaded'); + }); + } + }); + + return ( + <> + {/* Overlay alert */} + +
+
+ Landscape overlay enabled +
+
+
+ + + + +
+ +
+
+ + + } /> + + + + + + } /> + + + + ); +}; + +export default App; diff --git a/web/src/data.ts b/ui/webapp/src/data.ts similarity index 75% rename from web/src/data.ts rename to ui/webapp/src/data.ts index 0ce9a80a..c7cb56b8 100644 --- a/web/src/data.ts +++ b/ui/webapp/src/data.ts @@ -38,10 +38,17 @@ export const CLASSIFY_PARAM = 'classify'; export const SORT_BY_PARAM = 'sort-by'; export const SORT_DIRECTION_PARAM = 'sort-direction'; +// Overlay +export const OVERLAY_DATA_PARAM = 'overlay-data'; +export const OVERLAY_SETTINGS_PARAM = 'overlay-settings'; +export const OVERLAY_GUIDE_PARAM = 'overlay-guide'; +export const OVERLAY_LOGOS_PATH_PARAM = 'overlay-logos'; + export const REGEX_SPACE = / /g; export const REGEX_PLUS = /\+/g; export const REGEX_DASH = /-/g; export const REGEX_UNDERSCORE = /_/g; +export const REGEX_URL = /^https?:\/\//; export const DEFAULT_ZOOM_LEVELS = { [Breakpoint.XXXL]: 5, @@ -57,7 +64,7 @@ export const SMALL_DEVICES_BREAKPOINTS: Breakpoint[] = [Breakpoint.XS, Breakpoin export const DEFAULT_STICKY_MOBILE_NAVBAR_HEIGHT = 50; export const DEFAULT_STICKY_NAVBAR_HEIGHT = 72; export const DEFAULT_TAB = Tab.Explore; -export const DEFAULT_VIEW_MODE: ViewMode = window.baseDS.view_mode || ViewMode.Grid; +export let DEFAULT_VIEW_MODE: ViewMode = window.baseDS ? window.baseDS.view_mode || ViewMode.Grid : ViewMode.Grid; export const DEFAULT_GRID_ITEMS_SIZE = GridItemsSize.Small; export const DEFAULT_FINANCES_KIND = FinancesKind.Funding; export const DEFAULT_CLASSIFY = ClassifyOption.Category; @@ -65,8 +72,10 @@ export const DEFAULT_SORT = SortOption.Name; export const DEFAULT_SORT_DIRECTION = SortDirection.Asc; export const ALL_OPTION = 'all'; -const FOUNDATION = window.baseDS.foundation; -const GRID_SIZE = window.baseDS.grid_items_size || DEFAULT_GRID_ITEMS_SIZE; +export let FOUNDATION = window.baseDS ? window.baseDS.foundation : ''; +export let GRID_SIZE = window.baseDS + ? window.baseDS.grid_items_size || DEFAULT_GRID_ITEMS_SIZE + : DEFAULT_GRID_ITEMS_SIZE; export const ZOOM_LEVELS_PER_SIZE: ZoomLevelsPerSize = { [GridItemsSize.Small]: [ @@ -110,26 +119,11 @@ export const ZOOM_LEVELS_PER_SIZE: ZoomLevelsPerSize = { ], }; -export const ZOOM_LEVELS = ZOOM_LEVELS_PER_SIZE[GRID_SIZE as GridItemsSize]; +export let ZOOM_LEVELS = ZOOM_LEVELS_PER_SIZE[GRID_SIZE as GridItemsSize]; export const COLORS: string[] = ['var(--color5)', 'var(--color6)']; -export const FILTERS: FilterSection[] = [ - { - value: FilterCategory.Maturity, - title: 'Project', - options: [ - { - value: getFoundationNameLabel(), - name: `${FOUNDATION} Projects`, - suboptions: [], - }, - { - value: `non-${getFoundationNameLabel()}`, - name: `Not ${FOUNDATION} Projects`, - }, - ], - }, +export const COMMON_FILTERS: FilterSection[] = [ { value: FilterCategory.License, title: 'License', @@ -170,6 +164,24 @@ export const FILTERS: FilterSection[] = [ ], }, ]; +export let FILTERS: FilterSection[] = [ + { + value: FilterCategory.Maturity, + title: 'Project', + options: [ + { + value: getFoundationNameLabel(), + name: `${FOUNDATION} Projects`, + suboptions: [], + }, + { + value: `non-${getFoundationNameLabel()}`, + name: `Not ${FOUNDATION} Projects`, + }, + ], + }, + ...COMMON_FILTERS, +]; export const FILTER_CATEGORIES_PER_TITLE: FilterCategoriesPerTitle = { [FilterTitle.Project]: [FilterCategory.Maturity, FilterCategory.TAG, FilterCategory.License, FilterCategory.Extra], @@ -192,3 +204,34 @@ export const SORT_OPTION_LABEL = { [SortOption.LatestCommit]: 'Latest commit', [SortOption.Funding]: 'Funding', }; + +interface SettingsValue { + foundationName: string; + gridSize: GridItemsSize; + viewMode: ViewMode; +} + +export const overrideSettings = (values: SettingsValue) => { + FOUNDATION = values.foundationName; + GRID_SIZE = values.gridSize; + DEFAULT_VIEW_MODE = values.viewMode; + ZOOM_LEVELS = ZOOM_LEVELS_PER_SIZE[values.gridSize]; + FILTERS = [ + { + value: FilterCategory.Maturity, + title: 'Project', + options: [ + { + value: getFoundationNameLabel(), + name: `${FOUNDATION} Projects`, + suboptions: [], + }, + { + value: `non-${getFoundationNameLabel()}`, + name: `Not ${FOUNDATION} Projects`, + }, + ], + }, + ...COMMON_FILTERS, + ]; +}; diff --git a/web/src/env.d.ts b/ui/webapp/src/env.d.ts similarity index 100% rename from web/src/env.d.ts rename to ui/webapp/src/env.d.ts diff --git a/web/src/hooks/useBodyScroll.ts b/ui/webapp/src/hooks/useBodyScroll.ts similarity index 100% rename from web/src/hooks/useBodyScroll.ts rename to ui/webapp/src/hooks/useBodyScroll.ts diff --git a/web/src/hooks/useBreakpointDetect.tsx b/ui/webapp/src/hooks/useBreakpointDetect.tsx similarity index 100% rename from web/src/hooks/useBreakpointDetect.tsx rename to ui/webapp/src/hooks/useBreakpointDetect.tsx diff --git a/web/src/hooks/useOutsideClick.ts b/ui/webapp/src/hooks/useOutsideClick.ts similarity index 100% rename from web/src/hooks/useOutsideClick.ts rename to ui/webapp/src/hooks/useOutsideClick.ts diff --git a/web/src/index.tsx b/ui/webapp/src/index.tsx similarity index 100% rename from web/src/index.tsx rename to ui/webapp/src/index.tsx diff --git a/ui/webapp/src/layout/Layout.module.css b/ui/webapp/src/layout/Layout.module.css new file mode 100644 index 00000000..bc0d07cd --- /dev/null +++ b/ui/webapp/src/layout/Layout.module.css @@ -0,0 +1,7 @@ +.container { + min-height: 100vh; +} + +:global(.overlay-active) .container { + margin-top: var(--overlay-alert-height); +} diff --git a/web/src/layout/common/ActiveFiltersList.module.css b/ui/webapp/src/layout/common/ActiveFiltersList.module.css similarity index 100% rename from web/src/layout/common/ActiveFiltersList.module.css rename to ui/webapp/src/layout/common/ActiveFiltersList.module.css diff --git a/web/src/layout/common/ActiveFiltersList.tsx b/ui/webapp/src/layout/common/ActiveFiltersList.tsx similarity index 100% rename from web/src/layout/common/ActiveFiltersList.tsx rename to ui/webapp/src/layout/common/ActiveFiltersList.tsx diff --git a/web/src/layout/common/Badge.module.css b/ui/webapp/src/layout/common/Badge.module.css similarity index 100% rename from web/src/layout/common/Badge.module.css rename to ui/webapp/src/layout/common/Badge.module.css diff --git a/web/src/layout/common/Badge.tsx b/ui/webapp/src/layout/common/Badge.tsx similarity index 100% rename from web/src/layout/common/Badge.tsx rename to ui/webapp/src/layout/common/Badge.tsx diff --git a/web/src/layout/common/ButtonCopyToClipboard.module.css b/ui/webapp/src/layout/common/ButtonCopyToClipboard.module.css similarity index 100% rename from web/src/layout/common/ButtonCopyToClipboard.module.css rename to ui/webapp/src/layout/common/ButtonCopyToClipboard.module.css diff --git a/web/src/layout/common/ButtonCopyToClipboard.tsx b/ui/webapp/src/layout/common/ButtonCopyToClipboard.tsx similarity index 100% rename from web/src/layout/common/ButtonCopyToClipboard.tsx rename to ui/webapp/src/layout/common/ButtonCopyToClipboard.tsx diff --git a/web/src/layout/common/ButtonToTopScroll.module.css b/ui/webapp/src/layout/common/ButtonToTopScroll.module.css similarity index 100% rename from web/src/layout/common/ButtonToTopScroll.module.css rename to ui/webapp/src/layout/common/ButtonToTopScroll.module.css diff --git a/web/src/layout/common/ButtonToTopScroll.tsx b/ui/webapp/src/layout/common/ButtonToTopScroll.tsx similarity index 100% rename from web/src/layout/common/ButtonToTopScroll.tsx rename to ui/webapp/src/layout/common/ButtonToTopScroll.tsx diff --git a/web/src/layout/common/Checkbox.module.css b/ui/webapp/src/layout/common/Checkbox.module.css similarity index 100% rename from web/src/layout/common/Checkbox.module.css rename to ui/webapp/src/layout/common/Checkbox.module.css diff --git a/web/src/layout/common/Checkbox.tsx b/ui/webapp/src/layout/common/Checkbox.tsx similarity index 100% rename from web/src/layout/common/Checkbox.tsx rename to ui/webapp/src/layout/common/Checkbox.tsx diff --git a/web/src/layout/common/CodeBlock.module.css b/ui/webapp/src/layout/common/CodeBlock.module.css similarity index 100% rename from web/src/layout/common/CodeBlock.module.css rename to ui/webapp/src/layout/common/CodeBlock.module.css diff --git a/web/src/layout/common/CodeBlock.tsx b/ui/webapp/src/layout/common/CodeBlock.tsx similarity index 100% rename from web/src/layout/common/CodeBlock.tsx rename to ui/webapp/src/layout/common/CodeBlock.tsx diff --git a/web/src/layout/common/CollapsableText.module.css b/ui/webapp/src/layout/common/CollapsableText.module.css similarity index 100% rename from web/src/layout/common/CollapsableText.module.css rename to ui/webapp/src/layout/common/CollapsableText.module.css diff --git a/web/src/layout/common/CollapsableText.tsx b/ui/webapp/src/layout/common/CollapsableText.tsx similarity index 100% rename from web/src/layout/common/CollapsableText.tsx rename to ui/webapp/src/layout/common/CollapsableText.tsx diff --git a/web/src/layout/common/DownloadDropdown.module.css b/ui/webapp/src/layout/common/DownloadDropdown.module.css similarity index 100% rename from web/src/layout/common/DownloadDropdown.module.css rename to ui/webapp/src/layout/common/DownloadDropdown.module.css diff --git a/web/src/layout/common/DownloadDropdown.tsx b/ui/webapp/src/layout/common/DownloadDropdown.tsx similarity index 100% rename from web/src/layout/common/DownloadDropdown.tsx rename to ui/webapp/src/layout/common/DownloadDropdown.tsx diff --git a/web/src/layout/common/ExternalLink.module.css b/ui/webapp/src/layout/common/ExternalLink.module.css similarity index 100% rename from web/src/layout/common/ExternalLink.module.css rename to ui/webapp/src/layout/common/ExternalLink.module.css diff --git a/web/src/layout/common/ExternalLink.tsx b/ui/webapp/src/layout/common/ExternalLink.tsx similarity index 100% rename from web/src/layout/common/ExternalLink.tsx rename to ui/webapp/src/layout/common/ExternalLink.tsx diff --git a/web/src/layout/common/FiltersInLine.module.css b/ui/webapp/src/layout/common/FiltersInLine.module.css similarity index 100% rename from web/src/layout/common/FiltersInLine.module.css rename to ui/webapp/src/layout/common/FiltersInLine.module.css diff --git a/web/src/layout/common/FiltersInLine.tsx b/ui/webapp/src/layout/common/FiltersInLine.tsx similarity index 100% rename from web/src/layout/common/FiltersInLine.tsx rename to ui/webapp/src/layout/common/FiltersInLine.tsx diff --git a/web/src/layout/common/FoundationBadge.tsx b/ui/webapp/src/layout/common/FoundationBadge.tsx similarity index 76% rename from web/src/layout/common/FoundationBadge.tsx rename to ui/webapp/src/layout/common/FoundationBadge.tsx index d304ef58..94f4476d 100644 --- a/web/src/layout/common/FoundationBadge.tsx +++ b/ui/webapp/src/layout/common/FoundationBadge.tsx @@ -1,8 +1,10 @@ +import { FOUNDATION } from '../../data'; + interface Props { class?: string; } const FoundationBadge = (props: Props) => { - const foundation = window.baseDS.foundation; + const foundation = FOUNDATION; return (
diff --git a/web/src/layout/common/FullScreenModal.module.css b/ui/webapp/src/layout/common/FullScreenModal.module.css similarity index 100% rename from web/src/layout/common/FullScreenModal.module.css rename to ui/webapp/src/layout/common/FullScreenModal.module.css diff --git a/web/src/layout/common/FullScreenModal.tsx b/ui/webapp/src/layout/common/FullScreenModal.tsx similarity index 100% rename from web/src/layout/common/FullScreenModal.tsx rename to ui/webapp/src/layout/common/FullScreenModal.tsx diff --git a/web/src/layout/common/HoverableItem.tsx b/ui/webapp/src/layout/common/HoverableItem.tsx similarity index 100% rename from web/src/layout/common/HoverableItem.tsx rename to ui/webapp/src/layout/common/HoverableItem.tsx diff --git a/web/src/layout/common/Image.tsx b/ui/webapp/src/layout/common/Image.tsx similarity index 55% rename from web/src/layout/common/Image.tsx rename to ui/webapp/src/layout/common/Image.tsx index d31f15c5..efa6413f 100644 --- a/web/src/layout/common/Image.tsx +++ b/ui/webapp/src/layout/common/Image.tsx @@ -1,6 +1,7 @@ import isUndefined from 'lodash/isUndefined'; -import { createSignal } from 'solid-js'; +import { createSignal, onMount, Show } from 'solid-js'; +import { REGEX_URL } from '../../data'; import { SVGIconKind } from '../../types'; import SVGIcon from './SVGIcon'; @@ -9,27 +10,38 @@ interface Props { logo: string; class?: string; enableLazyLoad?: boolean; + width?: string | number; + height?: string | number; } const Image = (props: Props) => { const [error, setError] = createSignal(false); + const [url, setUrl] = createSignal(); + + onMount(() => { + if (REGEX_URL.test(props.logo)) { + setUrl(props.logo); + } else { + setUrl(import.meta.env.MODE === 'development' ? `../../static/${props.logo}` : `./${props.logo}`); + } + }); return ( - <> + {error() ? ( ) : ( {`${props.name} setError(true)} loading={!isUndefined(props.enableLazyLoad) && props.enableLazyLoad ? 'lazy' : undefined} - width="auto" - height="auto" + width={props.width || 'auto'} + height={props.height || 'auto'} /> )} - + ); }; diff --git a/web/src/layout/common/Loading.module.css b/ui/webapp/src/layout/common/Loading.module.css similarity index 100% rename from web/src/layout/common/Loading.module.css rename to ui/webapp/src/layout/common/Loading.module.css diff --git a/web/src/layout/common/Loading.tsx b/ui/webapp/src/layout/common/Loading.tsx similarity index 75% rename from web/src/layout/common/Loading.tsx rename to ui/webapp/src/layout/common/Loading.tsx index ce919e47..0e23b817 100644 --- a/web/src/layout/common/Loading.tsx +++ b/ui/webapp/src/layout/common/Loading.tsx @@ -10,6 +10,8 @@ interface Props { position?: 'fixed' | 'absolute' | 'relative'; transparentBg?: boolean; noWrapper?: boolean; + legend?: string; + legendClass?: string; } const Loading = (props: Props) => { @@ -38,8 +40,13 @@ const Loading = (props: Props) => { [styles.transparentBg]: !isUndefined(props.transparentBg) && props.transparentBg, }} > -
- {getSpinner()} +
+
+ {getSpinner()} +
+ +
{props.legend}
+
diff --git a/web/src/layout/common/MaturityBadge.module.css b/ui/webapp/src/layout/common/MaturityBadge.module.css similarity index 100% rename from web/src/layout/common/MaturityBadge.module.css rename to ui/webapp/src/layout/common/MaturityBadge.module.css diff --git a/web/src/layout/common/MaturityBadge.tsx b/ui/webapp/src/layout/common/MaturityBadge.tsx similarity index 100% rename from web/src/layout/common/MaturityBadge.tsx rename to ui/webapp/src/layout/common/MaturityBadge.tsx diff --git a/web/src/layout/common/Modal.module.css b/ui/webapp/src/layout/common/Modal.module.css similarity index 100% rename from web/src/layout/common/Modal.module.css rename to ui/webapp/src/layout/common/Modal.module.css diff --git a/web/src/layout/common/Modal.tsx b/ui/webapp/src/layout/common/Modal.tsx similarity index 100% rename from web/src/layout/common/Modal.tsx rename to ui/webapp/src/layout/common/Modal.tsx diff --git a/web/src/layout/common/NoData.module.css b/ui/webapp/src/layout/common/NoData.module.css similarity index 100% rename from web/src/layout/common/NoData.module.css rename to ui/webapp/src/layout/common/NoData.module.css diff --git a/web/src/layout/common/NoData.tsx b/ui/webapp/src/layout/common/NoData.tsx similarity index 70% rename from web/src/layout/common/NoData.tsx rename to ui/webapp/src/layout/common/NoData.tsx index fea7c601..957d9d66 100644 --- a/web/src/layout/common/NoData.tsx +++ b/ui/webapp/src/layout/common/NoData.tsx @@ -10,7 +10,7 @@ interface Props { const NoData = (props: Props) => ( diff --git a/web/src/layout/common/Pagination.module.css b/ui/webapp/src/layout/common/Pagination.module.css similarity index 100% rename from web/src/layout/common/Pagination.module.css rename to ui/webapp/src/layout/common/Pagination.module.css diff --git a/web/src/layout/common/Pagination.tsx b/ui/webapp/src/layout/common/Pagination.tsx similarity index 100% rename from web/src/layout/common/Pagination.tsx rename to ui/webapp/src/layout/common/Pagination.tsx diff --git a/web/src/layout/common/SVGIcon.tsx b/ui/webapp/src/layout/common/SVGIcon.tsx similarity index 99% rename from web/src/layout/common/SVGIcon.tsx rename to ui/webapp/src/layout/common/SVGIcon.tsx index 7c68f9fd..70f8eb27 100644 --- a/web/src/layout/common/SVGIcon.tsx +++ b/ui/webapp/src/layout/common/SVGIcon.tsx @@ -1,7 +1,7 @@ import { ValidComponent } from 'solid-js'; import { Dynamic } from 'solid-js/web'; -import { SVGIconKind } from '../../../../web/src/types'; +import { SVGIconKind } from '../../types'; interface Props { class?: string; diff --git a/web/src/layout/common/Searchbar.module.css b/ui/webapp/src/layout/common/Searchbar.module.css similarity index 100% rename from web/src/layout/common/Searchbar.module.css rename to ui/webapp/src/layout/common/Searchbar.module.css diff --git a/web/src/layout/common/Searchbar.tsx b/ui/webapp/src/layout/common/Searchbar.tsx similarity index 96% rename from web/src/layout/common/Searchbar.tsx rename to ui/webapp/src/layout/common/Searchbar.tsx index 5eb8d015..d97ba3e2 100644 --- a/web/src/layout/common/Searchbar.tsx +++ b/ui/webapp/src/layout/common/Searchbar.tsx @@ -266,13 +266,7 @@ const Searchbar = (props: Props) => {
- +
{ const itemId = () => props.itemId; const openStatus = () => props.openStatus; const origin = window.location.origin; - const foundation = window.baseDS.foundation; + const foundation = FOUNDATION; const badgeImage = `https://img.shields.io/badge/${foundation}%20Landscape-5699C6`; const markdownLink = () => `[![${foundation} Landscape](${badgeImage})](${origin}${BASE_PATH}/?item=${itemId()})`; const asciiLink = () => `${origin}${BASE_PATH}/?item=${itemId()}[image:${badgeImage}[${foundation} LANDSCAPE]]`; diff --git a/web/src/layout/common/itemModal/Box.module.css b/ui/webapp/src/layout/common/itemModal/Box.module.css similarity index 100% rename from web/src/layout/common/itemModal/Box.module.css rename to ui/webapp/src/layout/common/itemModal/Box.module.css diff --git a/web/src/layout/common/itemModal/Box.tsx b/ui/webapp/src/layout/common/itemModal/Box.tsx similarity index 100% rename from web/src/layout/common/itemModal/Box.tsx rename to ui/webapp/src/layout/common/itemModal/Box.tsx diff --git a/web/src/layout/common/itemModal/Content.module.css b/ui/webapp/src/layout/common/itemModal/Content.module.css similarity index 100% rename from web/src/layout/common/itemModal/Content.module.css rename to ui/webapp/src/layout/common/itemModal/Content.module.css diff --git a/web/src/layout/common/itemModal/Content.tsx b/ui/webapp/src/layout/common/itemModal/Content.tsx similarity index 98% rename from web/src/layout/common/itemModal/Content.tsx rename to ui/webapp/src/layout/common/itemModal/Content.tsx index 04b42c1a..9f73b9f9 100644 --- a/web/src/layout/common/itemModal/Content.tsx +++ b/ui/webapp/src/layout/common/itemModal/Content.tsx @@ -5,6 +5,7 @@ import isUndefined from 'lodash/isUndefined'; import sortBy from 'lodash/sortBy'; import { createEffect, createSignal, For, Match, on, Show, Switch } from 'solid-js'; +import { FOUNDATION } from '../../../data'; import { AdditionalCategory, Item, Repository, SecurityAudit, SVGIconKind } from '../../../types'; import formatProfitLabel from '../../../utils/formatLabelProfit'; import getItemDescription from '../../../utils/getItemDescription'; @@ -579,15 +580,14 @@ const Content = (props: Props) => { {/* CLOMonitor */} - +
CLOMonitor report summary
{
{ {props.projectName} is a subproject of {parentInfo()!.name} .}> - , a {window.baseDS.foundation} project. + , a {FOUNDATION} project.
diff --git a/web/src/layout/common/itemModal/ParticipationStats.module.css b/ui/webapp/src/layout/common/itemModal/ParticipationStats.module.css similarity index 100% rename from web/src/layout/common/itemModal/ParticipationStats.module.css rename to ui/webapp/src/layout/common/itemModal/ParticipationStats.module.css diff --git a/web/src/layout/common/itemModal/ParticipationStats.tsx b/ui/webapp/src/layout/common/itemModal/ParticipationStats.tsx similarity index 100% rename from web/src/layout/common/itemModal/ParticipationStats.tsx rename to ui/webapp/src/layout/common/itemModal/ParticipationStats.tsx diff --git a/web/src/layout/common/itemModal/RepositoriesSection.module.css b/ui/webapp/src/layout/common/itemModal/RepositoriesSection.module.css similarity index 100% rename from web/src/layout/common/itemModal/RepositoriesSection.module.css rename to ui/webapp/src/layout/common/itemModal/RepositoriesSection.module.css diff --git a/web/src/layout/common/itemModal/RepositoriesSection.tsx b/ui/webapp/src/layout/common/itemModal/RepositoriesSection.tsx similarity index 100% rename from web/src/layout/common/itemModal/RepositoriesSection.tsx rename to ui/webapp/src/layout/common/itemModal/RepositoriesSection.tsx diff --git a/web/src/layout/common/itemModal/index.tsx b/ui/webapp/src/layout/common/itemModal/index.tsx similarity index 100% rename from web/src/layout/common/itemModal/index.tsx rename to ui/webapp/src/layout/common/itemModal/index.tsx diff --git a/web/src/layout/common/zoomModal/ZoomModal.module.css b/ui/webapp/src/layout/common/zoomModal/ZoomModal.module.css similarity index 100% rename from web/src/layout/common/zoomModal/ZoomModal.module.css rename to ui/webapp/src/layout/common/zoomModal/ZoomModal.module.css diff --git a/web/src/layout/common/zoomModal/index.tsx b/ui/webapp/src/layout/common/zoomModal/index.tsx similarity index 100% rename from web/src/layout/common/zoomModal/index.tsx rename to ui/webapp/src/layout/common/zoomModal/index.tsx diff --git a/web/src/layout/explore/Content.tsx b/ui/webapp/src/layout/explore/Content.tsx similarity index 100% rename from web/src/layout/explore/Content.tsx rename to ui/webapp/src/layout/explore/Content.tsx diff --git a/web/src/layout/explore/Explore.module.css b/ui/webapp/src/layout/explore/Explore.module.css similarity index 100% rename from web/src/layout/explore/Explore.module.css rename to ui/webapp/src/layout/explore/Explore.module.css diff --git a/web/src/layout/explore/card/Card.module.css b/ui/webapp/src/layout/explore/card/Card.module.css similarity index 100% rename from web/src/layout/explore/card/Card.module.css rename to ui/webapp/src/layout/explore/card/Card.module.css diff --git a/web/src/layout/explore/card/Card.tsx b/ui/webapp/src/layout/explore/card/Card.tsx similarity index 100% rename from web/src/layout/explore/card/Card.tsx rename to ui/webapp/src/layout/explore/card/Card.tsx diff --git a/web/src/layout/explore/card/CardCategory.module.css b/ui/webapp/src/layout/explore/card/CardCategory.module.css similarity index 100% rename from web/src/layout/explore/card/CardCategory.module.css rename to ui/webapp/src/layout/explore/card/CardCategory.module.css diff --git a/web/src/layout/explore/card/CardTitle.module.css b/ui/webapp/src/layout/explore/card/CardTitle.module.css similarity index 100% rename from web/src/layout/explore/card/CardTitle.module.css rename to ui/webapp/src/layout/explore/card/CardTitle.module.css diff --git a/web/src/layout/explore/card/CardTitle.tsx b/ui/webapp/src/layout/explore/card/CardTitle.tsx similarity index 100% rename from web/src/layout/explore/card/CardTitle.tsx rename to ui/webapp/src/layout/explore/card/CardTitle.tsx diff --git a/web/src/layout/explore/card/Content.module.css b/ui/webapp/src/layout/explore/card/Content.module.css similarity index 100% rename from web/src/layout/explore/card/Content.module.css rename to ui/webapp/src/layout/explore/card/Content.module.css diff --git a/web/src/layout/explore/card/Content.tsx b/ui/webapp/src/layout/explore/card/Content.tsx similarity index 100% rename from web/src/layout/explore/card/Content.tsx rename to ui/webapp/src/layout/explore/card/Content.tsx diff --git a/web/src/layout/explore/card/Menu.module.css b/ui/webapp/src/layout/explore/card/Menu.module.css similarity index 100% rename from web/src/layout/explore/card/Menu.module.css rename to ui/webapp/src/layout/explore/card/Menu.module.css diff --git a/web/src/layout/explore/card/Menu.tsx b/ui/webapp/src/layout/explore/card/Menu.tsx similarity index 100% rename from web/src/layout/explore/card/Menu.tsx rename to ui/webapp/src/layout/explore/card/Menu.tsx diff --git a/web/src/layout/explore/card/index.tsx b/ui/webapp/src/layout/explore/card/index.tsx similarity index 100% rename from web/src/layout/explore/card/index.tsx rename to ui/webapp/src/layout/explore/card/index.tsx diff --git a/web/src/layout/explore/filters/Filters.module.css b/ui/webapp/src/layout/explore/filters/Filters.module.css similarity index 100% rename from web/src/layout/explore/filters/Filters.module.css rename to ui/webapp/src/layout/explore/filters/Filters.module.css diff --git a/web/src/layout/explore/filters/SearchbarSection.module.css b/ui/webapp/src/layout/explore/filters/SearchbarSection.module.css similarity index 100% rename from web/src/layout/explore/filters/SearchbarSection.module.css rename to ui/webapp/src/layout/explore/filters/SearchbarSection.module.css diff --git a/web/src/layout/explore/filters/SearchbarSection.tsx b/ui/webapp/src/layout/explore/filters/SearchbarSection.tsx similarity index 100% rename from web/src/layout/explore/filters/SearchbarSection.tsx rename to ui/webapp/src/layout/explore/filters/SearchbarSection.tsx diff --git a/web/src/layout/explore/filters/index.tsx b/ui/webapp/src/layout/explore/filters/index.tsx similarity index 100% rename from web/src/layout/explore/filters/index.tsx rename to ui/webapp/src/layout/explore/filters/index.tsx diff --git a/web/src/layout/explore/grid/Grid.module.css b/ui/webapp/src/layout/explore/grid/Grid.module.css similarity index 100% rename from web/src/layout/explore/grid/Grid.module.css rename to ui/webapp/src/layout/explore/grid/Grid.module.css diff --git a/web/src/layout/explore/grid/Grid.tsx b/ui/webapp/src/layout/explore/grid/Grid.tsx similarity index 98% rename from web/src/layout/explore/grid/Grid.tsx rename to ui/webapp/src/layout/explore/grid/Grid.tsx index 7392fed0..30722c34 100644 --- a/web/src/layout/explore/grid/Grid.tsx +++ b/ui/webapp/src/layout/explore/grid/Grid.tsx @@ -3,7 +3,7 @@ import isEqual from 'lodash/isEqual'; import isUndefined from 'lodash/isUndefined'; import { createEffect, createSignal, For, on, Show } from 'solid-js'; -import { GUIDE_PATH, ZOOM_LEVELS } from '../../../data'; +import { GRID_SIZE, GUIDE_PATH, ZOOM_LEVELS } from '../../../data'; import { BaseItem, Item, SVGIconKind } from '../../../types'; import calculateGridItemsPerRow from '../../../utils/calculateGridItemsPerRow'; import getNormalizedName from '../../../utils/getNormalizedName'; @@ -88,7 +88,7 @@ export const ItemsList = (props: ItemsListProps) => { }; const Grid = (props: Props) => { - const gridItemsSize = window.baseDS.grid_items_size; + const gridItemsSize = GRID_SIZE; const zoom = useZoomLevel(); const updateActiveSection = useSetVisibleZoom(); const [grid, setGrid] = createSignal(); diff --git a/web/src/layout/explore/grid/GridCategory.module.css b/ui/webapp/src/layout/explore/grid/GridCategory.module.css similarity index 100% rename from web/src/layout/explore/grid/GridCategory.module.css rename to ui/webapp/src/layout/explore/grid/GridCategory.module.css diff --git a/web/src/layout/explore/grid/GridItem.module.css b/ui/webapp/src/layout/explore/grid/GridItem.module.css similarity index 100% rename from web/src/layout/explore/grid/GridItem.module.css rename to ui/webapp/src/layout/explore/grid/GridItem.module.css diff --git a/web/src/layout/explore/grid/GridItem.tsx b/ui/webapp/src/layout/explore/grid/GridItem.tsx similarity index 100% rename from web/src/layout/explore/grid/GridItem.tsx rename to ui/webapp/src/layout/explore/grid/GridItem.tsx diff --git a/web/src/layout/explore/grid/index.tsx b/ui/webapp/src/layout/explore/grid/index.tsx similarity index 95% rename from web/src/layout/explore/grid/index.tsx rename to ui/webapp/src/layout/explore/grid/index.tsx index 2132a765..7bf71c92 100644 --- a/web/src/layout/explore/grid/index.tsx +++ b/ui/webapp/src/layout/explore/grid/index.tsx @@ -94,8 +94,10 @@ const GridCategory = (props: Props) => { createEffect( on(data, () => { - setColorsList(generateColorsArray(Object.keys(data()).length)); - setCatWithItems(getCategoriesWithItems(data())); + if (!isUndefined(data())) { + setColorsList(generateColorsArray(Object.keys(data()).length)); + setCatWithItems(getCategoriesWithItems(data())); + } }) ); diff --git a/web/src/layout/explore/index.tsx b/ui/webapp/src/layout/explore/index.tsx similarity index 100% rename from web/src/layout/explore/index.tsx rename to ui/webapp/src/layout/explore/index.tsx diff --git a/web/src/layout/explore/mobile/Card.module.css b/ui/webapp/src/layout/explore/mobile/Card.module.css similarity index 100% rename from web/src/layout/explore/mobile/Card.module.css rename to ui/webapp/src/layout/explore/mobile/Card.module.css diff --git a/web/src/layout/explore/mobile/Card.tsx b/ui/webapp/src/layout/explore/mobile/Card.tsx similarity index 100% rename from web/src/layout/explore/mobile/Card.tsx rename to ui/webapp/src/layout/explore/mobile/Card.tsx diff --git a/web/src/layout/explore/mobile/ExploreMobileIndex.module.css b/ui/webapp/src/layout/explore/mobile/ExploreMobileIndex.module.css similarity index 100% rename from web/src/layout/explore/mobile/ExploreMobileIndex.module.css rename to ui/webapp/src/layout/explore/mobile/ExploreMobileIndex.module.css diff --git a/web/src/layout/explore/mobile/ExploreMobileIndex.tsx b/ui/webapp/src/layout/explore/mobile/ExploreMobileIndex.tsx similarity index 100% rename from web/src/layout/explore/mobile/ExploreMobileIndex.tsx rename to ui/webapp/src/layout/explore/mobile/ExploreMobileIndex.tsx diff --git a/web/src/layout/explore/mobile/MobileGrid.module.css b/ui/webapp/src/layout/explore/mobile/MobileGrid.module.css similarity index 100% rename from web/src/layout/explore/mobile/MobileGrid.module.css rename to ui/webapp/src/layout/explore/mobile/MobileGrid.module.css diff --git a/web/src/layout/explore/mobile/MobileGrid.tsx b/ui/webapp/src/layout/explore/mobile/MobileGrid.tsx similarity index 100% rename from web/src/layout/explore/mobile/MobileGrid.tsx rename to ui/webapp/src/layout/explore/mobile/MobileGrid.tsx diff --git a/web/src/layout/finances/Finances.module.css b/ui/webapp/src/layout/finances/Finances.module.css similarity index 100% rename from web/src/layout/finances/Finances.module.css rename to ui/webapp/src/layout/finances/Finances.module.css diff --git a/web/src/layout/finances/MobileFilters.module.css b/ui/webapp/src/layout/finances/MobileFilters.module.css similarity index 100% rename from web/src/layout/finances/MobileFilters.module.css rename to ui/webapp/src/layout/finances/MobileFilters.module.css diff --git a/web/src/layout/finances/MobileFilters.tsx b/ui/webapp/src/layout/finances/MobileFilters.tsx similarity index 100% rename from web/src/layout/finances/MobileFilters.tsx rename to ui/webapp/src/layout/finances/MobileFilters.tsx diff --git a/web/src/layout/finances/index.tsx b/ui/webapp/src/layout/finances/index.tsx similarity index 99% rename from web/src/layout/finances/index.tsx rename to ui/webapp/src/layout/finances/index.tsx index 4b3b20d3..0f66f9c4 100644 --- a/web/src/layout/finances/index.tsx +++ b/ui/webapp/src/layout/finances/index.tsx @@ -10,6 +10,7 @@ import { batch, createEffect, createSignal, For, Match, on, onMount, Show, Switc import { DEFAULT_FINANCES_KIND, FINANCES_KIND_PARAM, + FOUNDATION, PAGE_PARAM, REGEX_UNDERSCORE, SMALL_DEVICES_BREAKPOINTS, @@ -264,7 +265,7 @@ const Finances = () => { const removeFilter = (name: FilterCategory, value: string) => { let tmpActiveFilters: string[] = ({ ...activeFilters() }[name] || []).filter((f: string) => f !== value); if (name === FilterCategory.Maturity) { - if (isEqual(tmpActiveFilters, [window.baseDS.foundation.toLowerCase()])) { + if (isEqual(tmpActiveFilters, [FOUNDATION.toLowerCase()])) { tmpActiveFilters = []; } } diff --git a/web/src/layout/guide/Guide.module.css b/ui/webapp/src/layout/guide/Guide.module.css similarity index 100% rename from web/src/layout/guide/Guide.module.css rename to ui/webapp/src/layout/guide/Guide.module.css diff --git a/web/src/layout/guide/SubcategoryExtended.module.css b/ui/webapp/src/layout/guide/SubcategoryExtended.module.css similarity index 100% rename from web/src/layout/guide/SubcategoryExtended.module.css rename to ui/webapp/src/layout/guide/SubcategoryExtended.module.css diff --git a/web/src/layout/guide/SubcategoryExtended.tsx b/ui/webapp/src/layout/guide/SubcategoryExtended.tsx similarity index 97% rename from web/src/layout/guide/SubcategoryExtended.tsx rename to ui/webapp/src/layout/guide/SubcategoryExtended.tsx index e03b9ab0..86b4d41c 100644 --- a/web/src/layout/guide/SubcategoryExtended.tsx +++ b/ui/webapp/src/layout/guide/SubcategoryExtended.tsx @@ -2,6 +2,7 @@ import isUndefined from 'lodash/isUndefined'; import trim from 'lodash/trim'; import { createSignal, For, onMount, Show } from 'solid-js'; +import { FOUNDATION } from '../../data'; import { BaseItem, Item } from '../../types'; import sortItemsByOrderValue from '../../utils/sortItemsByOrderValue'; import styles from './SubcategoryExtended.module.css'; @@ -14,7 +15,7 @@ interface Props { } const SubcategoryExtended = (props: Props) => { - const foundation = window.baseDS.foundation; + const foundation = FOUNDATION; const [items, setItems] = createSignal<(BaseItem | Item)[]>(); const [itemsInTable, setItemsInTable] = createSignal<(BaseItem | Item)[]>(); diff --git a/web/src/layout/guide/SubcategoryGrid.module.css b/ui/webapp/src/layout/guide/SubcategoryGrid.module.css similarity index 100% rename from web/src/layout/guide/SubcategoryGrid.module.css rename to ui/webapp/src/layout/guide/SubcategoryGrid.module.css diff --git a/web/src/layout/guide/SubcategoryGrid.tsx b/ui/webapp/src/layout/guide/SubcategoryGrid.tsx similarity index 100% rename from web/src/layout/guide/SubcategoryGrid.tsx rename to ui/webapp/src/layout/guide/SubcategoryGrid.tsx diff --git a/web/src/layout/guide/ToC.module.css b/ui/webapp/src/layout/guide/ToC.module.css similarity index 100% rename from web/src/layout/guide/ToC.module.css rename to ui/webapp/src/layout/guide/ToC.module.css diff --git a/web/src/layout/guide/ToC.tsx b/ui/webapp/src/layout/guide/ToC.tsx similarity index 100% rename from web/src/layout/guide/ToC.tsx rename to ui/webapp/src/layout/guide/ToC.tsx diff --git a/web/src/layout/guide/index.tsx b/ui/webapp/src/layout/guide/index.tsx similarity index 95% rename from web/src/layout/guide/index.tsx rename to ui/webapp/src/layout/guide/index.tsx index ea0c5d25..2dc763be 100644 --- a/web/src/layout/guide/index.tsx +++ b/ui/webapp/src/layout/guide/index.tsx @@ -70,13 +70,19 @@ const GuideIndex = () => { onMount(() => { async function fetchGuide() { try { - fetch(import.meta.env.MODE === 'development' ? '../../static/data/guide.json' : './data/guide.json') - .then((res) => res.json()) - .then((data) => { - setGuide(data); - prepareToC(data); - setGuideContent(data); - }); + if (!isUndefined(window.guide)) { + setGuide(window.guide); + prepareToC(window.guide); + setGuideContent(window.guide); + } else { + fetch(import.meta.env.MODE === 'development' ? '../../static/data/guide.json' : './data/guide.json') + .then((res) => res.json()) + .then((data) => { + setGuide(data); + prepareToC(data); + setGuideContent(data); + }); + } } catch { setGuide(null); } diff --git a/web/src/layout/index.tsx b/ui/webapp/src/layout/index.tsx similarity index 100% rename from web/src/layout/index.tsx rename to ui/webapp/src/layout/index.tsx diff --git a/web/src/layout/logos/Logos.module.css b/ui/webapp/src/layout/logos/Logos.module.css similarity index 100% rename from web/src/layout/logos/Logos.module.css rename to ui/webapp/src/layout/logos/Logos.module.css diff --git a/web/src/layout/logos/index.tsx b/ui/webapp/src/layout/logos/index.tsx similarity index 100% rename from web/src/layout/logos/index.tsx rename to ui/webapp/src/layout/logos/index.tsx diff --git a/web/src/layout/navigation/EmbedModal.module.css b/ui/webapp/src/layout/navigation/EmbedModal.module.css similarity index 100% rename from web/src/layout/navigation/EmbedModal.module.css rename to ui/webapp/src/layout/navigation/EmbedModal.module.css diff --git a/web/src/layout/navigation/EmbedModal.tsx b/ui/webapp/src/layout/navigation/EmbedModal.tsx similarity index 99% rename from web/src/layout/navigation/EmbedModal.tsx rename to ui/webapp/src/layout/navigation/EmbedModal.tsx index d5b2bb20..49111875 100644 --- a/web/src/layout/navigation/EmbedModal.tsx +++ b/ui/webapp/src/layout/navigation/EmbedModal.tsx @@ -45,6 +45,7 @@ import useBreakpointDetect from '../../hooks/useBreakpointDetect'; import { Category, Subcategory, SVGIconKind } from '../../types'; import capitalizeFirstLetter from '../../utils/capitalizeFirstLetter'; import isExploreSection from '../../utils/isExploreSection'; +import prepareLink from '../../utils/prepareLink'; import rgba2hex from '../../utils/rgba2hex'; import CheckBox from '../common/Checkbox'; import CodeBlock from '../common/CodeBlock'; @@ -292,7 +293,7 @@ const EmbedModal = () => { e.preventDefault(); e.stopPropagation(); setVisibleModal(true); - navigate(EMBED_SETUP_PATH, { + navigate(prepareLink(EMBED_SETUP_PATH), { replace: true, }); }} diff --git a/web/src/layout/navigation/Footer.module.css b/ui/webapp/src/layout/navigation/Footer.module.css similarity index 100% rename from web/src/layout/navigation/Footer.module.css rename to ui/webapp/src/layout/navigation/Footer.module.css diff --git a/web/src/layout/navigation/Footer.tsx b/ui/webapp/src/layout/navigation/Footer.tsx similarity index 87% rename from web/src/layout/navigation/Footer.tsx rename to ui/webapp/src/layout/navigation/Footer.tsx index 72a32f7f..7f8a143e 100644 --- a/web/src/layout/navigation/Footer.tsx +++ b/ui/webapp/src/layout/navigation/Footer.tsx @@ -4,6 +4,7 @@ import { JSXElement, Show } from 'solid-js'; import { SVGIconKind } from '../../types'; import ExternalLink from '../common/ExternalLink'; +import Image from '../common/Image'; import SVGIcon from '../common/SVGIcon'; import styles from './Footer.module.css'; @@ -94,32 +95,17 @@ const Footer = () => { when={!isUndefined(foundationLink())} fallback={
- Logo +
} >
- Logo
diff --git a/web/src/layout/navigation/Header.module.css b/ui/webapp/src/layout/navigation/Header.module.css similarity index 93% rename from web/src/layout/navigation/Header.module.css rename to ui/webapp/src/layout/navigation/Header.module.css index 686861c5..7f5a6dec 100644 --- a/web/src/layout/navigation/Header.module.css +++ b/ui/webapp/src/layout/navigation/Header.module.css @@ -4,6 +4,11 @@ height: 72px; min-height: 72px; position: sticky; + top: 0; +} + +:global(.overlay-active) .navbar { + top: var(--overlay-alert-height); } .logo { diff --git a/web/src/layout/navigation/Header.tsx b/ui/webapp/src/layout/navigation/Header.tsx similarity index 87% rename from web/src/layout/navigation/Header.tsx rename to ui/webapp/src/layout/navigation/Header.tsx index 6c269ded..ddc0b17b 100644 --- a/web/src/layout/navigation/Header.tsx +++ b/ui/webapp/src/layout/navigation/Header.tsx @@ -6,9 +6,11 @@ import { createMemo, Show } from 'solid-js'; import { ALL_OPTION, DEFAULT_VIEW_MODE, EXPLORE_PATH, GUIDE_PATH, SCREENSHOTS_PATH, STATS_PATH } from '../../data'; import { SVGIconKind } from '../../types'; import isExploreSection from '../../utils/isExploreSection'; +import prepareLink from '../../utils/prepareLink'; import scrollToTop from '../../utils/scrollToTop'; import DownloadDropdown from '../common/DownloadDropdown'; import ExternalLink from '../common/ExternalLink'; +import Image from '../common/Image'; import Searchbar from '../common/Searchbar'; import SVGIcon from '../common/SVGIcon'; import { useSetGroupActive } from '../stores/groupActive'; @@ -35,7 +37,7 @@ const Header = () => { }; return ( -
+
@@ -43,19 +45,13 @@ const Header = () => { class="btn btn-link p-0 pe-3 me-2 me-xl-5" onClick={() => { resetDefaultExploreValues(); - navigate(EXPLORE_PATH, { + navigate(prepareLink(EXPLORE_PATH), { state: { from: 'logo-header' }, }); scrollToTop(false); }} > - Landscape logo +
@@ -64,15 +60,7 @@ const Header = () => { when={location.pathname !== SCREENSHOTS_PATH} fallback={ - QR code + } > @@ -85,7 +73,7 @@ const Header = () => { scrollToTop(false); } else { resetDefaultExploreValues(); - navigate(EXPLORE_PATH, { + navigate(prepareLink(EXPLORE_PATH), { state: { from: 'header' }, }); scrollToTop(false); @@ -103,7 +91,7 @@ const Header = () => { if (isActive(GUIDE_PATH)) { scrollToTop(false); } else { - navigate(GUIDE_PATH, { + navigate(prepareLink(GUIDE_PATH), { state: { from: 'header' }, }); scrollToTop(false); @@ -121,7 +109,7 @@ const Header = () => { if (isActive(STATS_PATH)) { scrollToTop(false); } else { - navigate(STATS_PATH, { + navigate(prepareLink(STATS_PATH), { state: { from: 'header' }, }); scrollToTop(false); diff --git a/web/src/layout/navigation/MiniFooter.tsx b/ui/webapp/src/layout/navigation/MiniFooter.tsx similarity index 100% rename from web/src/layout/navigation/MiniFooter.tsx rename to ui/webapp/src/layout/navigation/MiniFooter.tsx diff --git a/web/src/layout/navigation/MobileDropdown.module.css b/ui/webapp/src/layout/navigation/MobileDropdown.module.css similarity index 100% rename from web/src/layout/navigation/MobileDropdown.module.css rename to ui/webapp/src/layout/navigation/MobileDropdown.module.css diff --git a/web/src/layout/navigation/MobileDropdown.tsx b/ui/webapp/src/layout/navigation/MobileDropdown.tsx similarity index 100% rename from web/src/layout/navigation/MobileDropdown.tsx rename to ui/webapp/src/layout/navigation/MobileDropdown.tsx diff --git a/web/src/layout/navigation/MobileHeader.module.css b/ui/webapp/src/layout/navigation/MobileHeader.module.css similarity index 95% rename from web/src/layout/navigation/MobileHeader.module.css rename to ui/webapp/src/layout/navigation/MobileHeader.module.css index 465eab6f..e6bdd216 100644 --- a/web/src/layout/navigation/MobileHeader.module.css +++ b/ui/webapp/src/layout/navigation/MobileHeader.module.css @@ -93,6 +93,10 @@ top: 0px; } +:global(.overlay-active) .isSticky { + top: var(--overlay-alert-height); +} + .mobileBtn { width: 28px !important; height: 28px !important; diff --git a/web/src/layout/navigation/MobileHeader.tsx b/ui/webapp/src/layout/navigation/MobileHeader.tsx similarity index 82% rename from web/src/layout/navigation/MobileHeader.tsx rename to ui/webapp/src/layout/navigation/MobileHeader.tsx index fa4bb23c..0643f9eb 100644 --- a/web/src/layout/navigation/MobileHeader.tsx +++ b/ui/webapp/src/layout/navigation/MobileHeader.tsx @@ -9,6 +9,7 @@ import { SVGIconKind } from '../../types'; import scrollToTop from '../../utils/scrollToTop'; import DownloadDropdown from '../common/DownloadDropdown'; import ExternalLink from '../common/ExternalLink'; +import Image from '../common/Image'; import Searchbar from '../common/Searchbar'; import SVGIcon from '../common/SVGIcon'; import { useSetMobileTOCStatus } from '../stores/mobileTOC'; @@ -54,13 +55,7 @@ const MobileHeader = () => {
@@ -73,13 +68,7 @@ const MobileHeader = () => {
- Landscape logo +
@@ -91,15 +80,7 @@ const MobileHeader = () => { when={location.pathname !== SCREENSHOTS_PATH} fallback={ - QR code + } > diff --git a/web/src/layout/notFound/NotFound.module.css b/ui/webapp/src/layout/notFound/NotFound.module.css similarity index 100% rename from web/src/layout/notFound/NotFound.module.css rename to ui/webapp/src/layout/notFound/NotFound.module.css diff --git a/web/src/layout/notFound/index.tsx b/ui/webapp/src/layout/notFound/index.tsx similarity index 100% rename from web/src/layout/notFound/index.tsx rename to ui/webapp/src/layout/notFound/index.tsx diff --git a/web/src/layout/screenshots/Grid.module.css b/ui/webapp/src/layout/screenshots/Grid.module.css similarity index 100% rename from web/src/layout/screenshots/Grid.module.css rename to ui/webapp/src/layout/screenshots/Grid.module.css diff --git a/web/src/layout/screenshots/Grid.tsx b/ui/webapp/src/layout/screenshots/Grid.tsx similarity index 100% rename from web/src/layout/screenshots/Grid.tsx rename to ui/webapp/src/layout/screenshots/Grid.tsx diff --git a/web/src/layout/screenshots/Screenshots.module.css b/ui/webapp/src/layout/screenshots/Screenshots.module.css similarity index 100% rename from web/src/layout/screenshots/Screenshots.module.css rename to ui/webapp/src/layout/screenshots/Screenshots.module.css diff --git a/web/src/layout/screenshots/index.tsx b/ui/webapp/src/layout/screenshots/index.tsx similarity index 100% rename from web/src/layout/screenshots/index.tsx rename to ui/webapp/src/layout/screenshots/index.tsx diff --git a/web/src/layout/stats/Box.module.css b/ui/webapp/src/layout/stats/Box.module.css similarity index 100% rename from web/src/layout/stats/Box.module.css rename to ui/webapp/src/layout/stats/Box.module.css diff --git a/web/src/layout/stats/Box.tsx b/ui/webapp/src/layout/stats/Box.tsx similarity index 100% rename from web/src/layout/stats/Box.tsx rename to ui/webapp/src/layout/stats/Box.tsx diff --git a/web/src/layout/stats/ChartsGroup.tsx b/ui/webapp/src/layout/stats/ChartsGroup.tsx similarity index 100% rename from web/src/layout/stats/ChartsGroup.tsx rename to ui/webapp/src/layout/stats/ChartsGroup.tsx diff --git a/web/src/layout/stats/CollapsableTable.module.css b/ui/webapp/src/layout/stats/CollapsableTable.module.css similarity index 100% rename from web/src/layout/stats/CollapsableTable.module.css rename to ui/webapp/src/layout/stats/CollapsableTable.module.css diff --git a/web/src/layout/stats/CollapsableTable.tsx b/ui/webapp/src/layout/stats/CollapsableTable.tsx similarity index 100% rename from web/src/layout/stats/CollapsableTable.tsx rename to ui/webapp/src/layout/stats/CollapsableTable.tsx diff --git a/web/src/layout/stats/Content.tsx b/ui/webapp/src/layout/stats/Content.tsx similarity index 100% rename from web/src/layout/stats/Content.tsx rename to ui/webapp/src/layout/stats/Content.tsx diff --git a/web/src/layout/stats/HeatMapChart.tsx b/ui/webapp/src/layout/stats/HeatMapChart.tsx similarity index 100% rename from web/src/layout/stats/HeatMapChart.tsx rename to ui/webapp/src/layout/stats/HeatMapChart.tsx diff --git a/web/src/layout/stats/HorizontalBarChart.module.css b/ui/webapp/src/layout/stats/HorizontalBarChart.module.css similarity index 100% rename from web/src/layout/stats/HorizontalBarChart.module.css rename to ui/webapp/src/layout/stats/HorizontalBarChart.module.css diff --git a/web/src/layout/stats/HorizontalBarChart.tsx b/ui/webapp/src/layout/stats/HorizontalBarChart.tsx similarity index 100% rename from web/src/layout/stats/HorizontalBarChart.tsx rename to ui/webapp/src/layout/stats/HorizontalBarChart.tsx diff --git a/web/src/layout/stats/Stats.module.css b/ui/webapp/src/layout/stats/Stats.module.css similarity index 100% rename from web/src/layout/stats/Stats.module.css rename to ui/webapp/src/layout/stats/Stats.module.css diff --git a/web/src/layout/stats/TimestampLineChart.tsx b/ui/webapp/src/layout/stats/TimestampLineChart.tsx similarity index 100% rename from web/src/layout/stats/TimestampLineChart.tsx rename to ui/webapp/src/layout/stats/TimestampLineChart.tsx diff --git a/web/src/layout/stats/VerticalBarChart.module.css b/ui/webapp/src/layout/stats/VerticalBarChart.module.css similarity index 100% rename from web/src/layout/stats/VerticalBarChart.module.css rename to ui/webapp/src/layout/stats/VerticalBarChart.module.css diff --git a/web/src/layout/stats/VerticalBarChart.tsx b/ui/webapp/src/layout/stats/VerticalBarChart.tsx similarity index 100% rename from web/src/layout/stats/VerticalBarChart.tsx rename to ui/webapp/src/layout/stats/VerticalBarChart.tsx diff --git a/web/src/layout/stats/index.tsx b/ui/webapp/src/layout/stats/index.tsx similarity index 100% rename from web/src/layout/stats/index.tsx rename to ui/webapp/src/layout/stats/index.tsx diff --git a/web/src/layout/stores/activeItem.tsx b/ui/webapp/src/layout/stores/activeItem.tsx similarity index 100% rename from web/src/layout/stores/activeItem.tsx rename to ui/webapp/src/layout/stores/activeItem.tsx diff --git a/web/src/layout/stores/financesData.tsx b/ui/webapp/src/layout/stores/financesData.tsx similarity index 100% rename from web/src/layout/stores/financesData.tsx rename to ui/webapp/src/layout/stores/financesData.tsx diff --git a/web/src/layout/stores/fullData.tsx b/ui/webapp/src/layout/stores/fullData.tsx similarity index 92% rename from web/src/layout/stores/fullData.tsx rename to ui/webapp/src/layout/stores/fullData.tsx index 90bf5edd..681d8333 100644 --- a/web/src/layout/stores/fullData.tsx +++ b/ui/webapp/src/layout/stores/fullData.tsx @@ -3,7 +3,7 @@ import { createContext, createEffect, createSignal, ParentComponent, useContext import itemsDataGetter from '../../utils/itemsDataGetter'; function useFullDataProvider() { - const [fullDataReady, setFullDataReady] = createSignal(false); + const [fullDataReady, setFullDataReady] = createSignal(itemsDataGetter.isReady()); createEffect(() => { itemsDataGetter.subscribe({ diff --git a/web/src/layout/stores/gridWidth.tsx b/ui/webapp/src/layout/stores/gridWidth.tsx similarity index 100% rename from web/src/layout/stores/gridWidth.tsx rename to ui/webapp/src/layout/stores/gridWidth.tsx diff --git a/web/src/layout/stores/groupActive.tsx b/ui/webapp/src/layout/stores/groupActive.tsx similarity index 94% rename from web/src/layout/stores/groupActive.tsx rename to ui/webapp/src/layout/stores/groupActive.tsx index b99bcaa4..23b23da5 100644 --- a/web/src/layout/stores/groupActive.tsx +++ b/ui/webapp/src/layout/stores/groupActive.tsx @@ -6,6 +6,7 @@ import { createContext, createSignal, ParentComponent, useContext } from 'solid- import { ALL_OPTION, BASE_PATH, GROUP_PARAM } from '../../data'; import { Group } from '../../types'; import isExploreSection from '../../utils/isExploreSection'; +import prepareLink from '../../utils/prepareLink'; const getInitialGroupName = (groupParam: string | null): string | undefined => { const navigate = useNavigate(); @@ -22,7 +23,7 @@ const getInitialGroupName = (groupParam: string | null): string | undefined => { return groupParam; } else { if (isExploreSection(location.pathname)) { - navigate(`${BASE_PATH}/?group=${firstGroup}`, { + navigate(prepareLink(BASE_PATH, `group=${firstGroup}`), { replace: true, }); } diff --git a/web/src/layout/stores/guideFile.tsx b/ui/webapp/src/layout/stores/guideFile.tsx similarity index 100% rename from web/src/layout/stores/guideFile.tsx rename to ui/webapp/src/layout/stores/guideFile.tsx diff --git a/web/src/layout/stores/mobileTOC.tsx b/ui/webapp/src/layout/stores/mobileTOC.tsx similarity index 100% rename from web/src/layout/stores/mobileTOC.tsx rename to ui/webapp/src/layout/stores/mobileTOC.tsx diff --git a/web/src/layout/stores/upcomingEventData.tsx b/ui/webapp/src/layout/stores/upcomingEventData.tsx similarity index 100% rename from web/src/layout/stores/upcomingEventData.tsx rename to ui/webapp/src/layout/stores/upcomingEventData.tsx diff --git a/web/src/layout/stores/viewMode.tsx b/ui/webapp/src/layout/stores/viewMode.tsx similarity index 100% rename from web/src/layout/stores/viewMode.tsx rename to ui/webapp/src/layout/stores/viewMode.tsx diff --git a/web/src/layout/stores/visibleZoomSection.tsx b/ui/webapp/src/layout/stores/visibleZoomSection.tsx similarity index 100% rename from web/src/layout/stores/visibleZoomSection.tsx rename to ui/webapp/src/layout/stores/visibleZoomSection.tsx diff --git a/web/src/layout/stores/zoom.tsx b/ui/webapp/src/layout/stores/zoom.tsx similarity index 100% rename from web/src/layout/stores/zoom.tsx rename to ui/webapp/src/layout/stores/zoom.tsx diff --git a/web/src/layout/upcomingEvents/UpcomingEvents.module.css b/ui/webapp/src/layout/upcomingEvents/UpcomingEvents.module.css similarity index 100% rename from web/src/layout/upcomingEvents/UpcomingEvents.module.css rename to ui/webapp/src/layout/upcomingEvents/UpcomingEvents.module.css diff --git a/web/src/layout/upcomingEvents/index.tsx b/ui/webapp/src/layout/upcomingEvents/index.tsx similarity index 100% rename from web/src/layout/upcomingEvents/index.tsx rename to ui/webapp/src/layout/upcomingEvents/index.tsx diff --git a/web/src/styles/cookies.css b/ui/webapp/src/styles/cookies.css similarity index 100% rename from web/src/styles/cookies.css rename to ui/webapp/src/styles/cookies.css diff --git a/web/src/styles/default.scss b/ui/webapp/src/styles/default.scss similarity index 98% rename from web/src/styles/default.scss rename to ui/webapp/src/styles/default.scss index 82ab4457..887fbde2 100644 --- a/web/src/styles/default.scss +++ b/ui/webapp/src/styles/default.scss @@ -73,7 +73,7 @@ $font-family-sans-serif: // @import "../node_modules/bootstrap/scss/breadcrumb"; @import '../node_modules/bootstrap/scss/pagination'; @import '../node_modules/bootstrap/scss/badge'; -// @import "../node_modules/bootstrap/scss/alert"; +@import '../node_modules/bootstrap/scss/alert'; // @import "../node_modules/bootstrap/scss/progress"; // @import "../node_modules/bootstrap/scss/list-group"; @import '../node_modules/bootstrap/scss/close'; diff --git a/web/src/styles/light.scss b/ui/webapp/src/styles/light.scss similarity index 97% rename from web/src/styles/light.scss rename to ui/webapp/src/styles/light.scss index 414b65b8..d661c92e 100644 --- a/web/src/styles/light.scss +++ b/ui/webapp/src/styles/light.scss @@ -2,9 +2,10 @@ --bs-primary: var(--color1); --bs-secondary: var(--color2); --color-font: #38383f; - --color-stats-1: var(--color1); + --overlay-alert-height: 33px; + .modal-dialog-scrollable .modal-body { overscroll-behavior: contain; overflow-x: hidden; diff --git a/web/src/styles/mixins.scss b/ui/webapp/src/styles/mixins.scss similarity index 100% rename from web/src/styles/mixins.scss rename to ui/webapp/src/styles/mixins.scss diff --git a/web/src/types.ts b/ui/webapp/src/types.ts similarity index 100% rename from web/src/types.ts rename to ui/webapp/src/types.ts diff --git a/web/src/utils/calculateAxisValues.ts b/ui/webapp/src/utils/calculateAxisValues.ts similarity index 100% rename from web/src/utils/calculateAxisValues.ts rename to ui/webapp/src/utils/calculateAxisValues.ts diff --git a/web/src/utils/calculateGridItemsPerRow.ts b/ui/webapp/src/utils/calculateGridItemsPerRow.ts similarity index 100% rename from web/src/utils/calculateGridItemsPerRow.ts rename to ui/webapp/src/utils/calculateGridItemsPerRow.ts diff --git a/web/src/utils/calculateGridWidthInPx.ts b/ui/webapp/src/utils/calculateGridWidthInPx.ts similarity index 100% rename from web/src/utils/calculateGridWidthInPx.ts rename to ui/webapp/src/utils/calculateGridWidthInPx.ts diff --git a/web/src/utils/capitalizeFirstLetter.ts b/ui/webapp/src/utils/capitalizeFirstLetter.ts similarity index 100% rename from web/src/utils/capitalizeFirstLetter.ts rename to ui/webapp/src/utils/capitalizeFirstLetter.ts diff --git a/web/src/utils/checkIfCategoryInGroup.ts b/ui/webapp/src/utils/checkIfCategoryInGroup.ts similarity index 100% rename from web/src/utils/checkIfCategoryInGroup.ts rename to ui/webapp/src/utils/checkIfCategoryInGroup.ts diff --git a/ui/webapp/src/utils/checkQueryStringValue.ts b/ui/webapp/src/utils/checkQueryStringValue.ts new file mode 100644 index 00000000..6eb67ebf --- /dev/null +++ b/ui/webapp/src/utils/checkQueryStringValue.ts @@ -0,0 +1,11 @@ +const checkQueryStringValue = (query: string, value: string): boolean => { + const url = new URL(window.location.href); + + if (url.searchParams.has(query)) { + return url.searchParams.get(query) === value; + } else { + return false; + } +}; + +export default checkQueryStringValue; diff --git a/web/src/utils/cleanEmojis.ts b/ui/webapp/src/utils/cleanEmojis.ts similarity index 100% rename from web/src/utils/cleanEmojis.ts rename to ui/webapp/src/utils/cleanEmojis.ts diff --git a/web/src/utils/countVisibleItems.tsx b/ui/webapp/src/utils/countVisibleItems.tsx similarity index 100% rename from web/src/utils/countVisibleItems.tsx rename to ui/webapp/src/utils/countVisibleItems.tsx diff --git a/web/src/utils/cutString.ts b/ui/webapp/src/utils/cutString.ts similarity index 100% rename from web/src/utils/cutString.ts rename to ui/webapp/src/utils/cutString.ts diff --git a/web/src/utils/filterData.ts b/ui/webapp/src/utils/filterData.ts similarity index 100% rename from web/src/utils/filterData.ts rename to ui/webapp/src/utils/filterData.ts diff --git a/web/src/utils/formatLabelProfit.ts b/ui/webapp/src/utils/formatLabelProfit.ts similarity index 100% rename from web/src/utils/formatLabelProfit.ts rename to ui/webapp/src/utils/formatLabelProfit.ts diff --git a/web/src/utils/generateColorsArray.ts b/ui/webapp/src/utils/generateColorsArray.ts similarity index 100% rename from web/src/utils/generateColorsArray.ts rename to ui/webapp/src/utils/generateColorsArray.ts diff --git a/ui/webapp/src/utils/getBasePath.ts b/ui/webapp/src/utils/getBasePath.ts new file mode 100644 index 00000000..7ec7e99a --- /dev/null +++ b/ui/webapp/src/utils/getBasePath.ts @@ -0,0 +1,5 @@ +const getBasePath = () => { + return window.baseDS ? window.baseDS.base_path || '' : ''; +}; + +export default getBasePath; diff --git a/web/src/utils/getCategoriesWithItems.ts b/ui/webapp/src/utils/getCategoriesWithItems.ts similarity index 100% rename from web/src/utils/getCategoriesWithItems.ts rename to ui/webapp/src/utils/getCategoriesWithItems.ts diff --git a/ui/webapp/src/utils/getFoundationNameLabel.ts b/ui/webapp/src/utils/getFoundationNameLabel.ts new file mode 100644 index 00000000..e18d6f4c --- /dev/null +++ b/ui/webapp/src/utils/getFoundationNameLabel.ts @@ -0,0 +1,8 @@ +import { FOUNDATION, REGEX_SPACE } from '../data'; + +const getFoundationNameLabel = (): string => { + const foundation: string = FOUNDATION; + return foundation.toLowerCase().replace(REGEX_SPACE, ''); +}; + +export default getFoundationNameLabel; diff --git a/web/src/utils/getGroupName.ts b/ui/webapp/src/utils/getGroupName.ts similarity index 100% rename from web/src/utils/getGroupName.ts rename to ui/webapp/src/utils/getGroupName.ts diff --git a/web/src/utils/getItemDescription.ts b/ui/webapp/src/utils/getItemDescription.ts similarity index 100% rename from web/src/utils/getItemDescription.ts rename to ui/webapp/src/utils/getItemDescription.ts diff --git a/web/src/utils/getName.ts b/ui/webapp/src/utils/getName.ts similarity index 100% rename from web/src/utils/getName.ts rename to ui/webapp/src/utils/getName.ts diff --git a/web/src/utils/getNormalizedName.ts b/ui/webapp/src/utils/getNormalizedName.ts similarity index 100% rename from web/src/utils/getNormalizedName.ts rename to ui/webapp/src/utils/getNormalizedName.ts diff --git a/web/src/utils/goToElement.ts b/ui/webapp/src/utils/goToElement.ts similarity index 100% rename from web/src/utils/goToElement.ts rename to ui/webapp/src/utils/goToElement.ts diff --git a/web/src/utils/gridCategoryLayout.ts b/ui/webapp/src/utils/gridCategoryLayout.ts similarity index 100% rename from web/src/utils/gridCategoryLayout.ts rename to ui/webapp/src/utils/gridCategoryLayout.ts diff --git a/web/src/utils/isElementInView.ts b/ui/webapp/src/utils/isElementInView.ts similarity index 100% rename from web/src/utils/isElementInView.ts rename to ui/webapp/src/utils/isElementInView.ts diff --git a/web/src/utils/isExploreSection.ts b/ui/webapp/src/utils/isExploreSection.ts similarity index 100% rename from web/src/utils/isExploreSection.ts rename to ui/webapp/src/utils/isExploreSection.ts diff --git a/web/src/utils/isSectionInGuide.ts b/ui/webapp/src/utils/isSectionInGuide.ts similarity index 100% rename from web/src/utils/isSectionInGuide.ts rename to ui/webapp/src/utils/isSectionInGuide.ts diff --git a/web/src/utils/itemsDataGetter.ts b/ui/webapp/src/utils/itemsDataGetter.ts similarity index 86% rename from web/src/utils/itemsDataGetter.ts rename to ui/webapp/src/utils/itemsDataGetter.ts index c12121ca..79f53a85 100644 --- a/web/src/utils/itemsDataGetter.ts +++ b/ui/webapp/src/utils/itemsDataGetter.ts @@ -88,25 +88,37 @@ export class ItemsDataGetter { } // Initialize the data - public init() { + public init(landscapeData?: LandscapeData) { if (!this.ready) { - fetch(import.meta.env.MODE === 'development' ? '../../static/data/full.json' : './data/full.json') - .then((res) => res.json()) - .then((data: LandscapeData) => { - const extendedItems = this.extendItemsData(data.items, data.crunchbase_data, data.github_data); - this.landscapeData = { - ...data, - items: extendedItems, - }; - this.ready = true; - if (this.updateStatus) { - this.updateStatus.updateStatus(true); - } - this.saveData(); - }); + if (landscapeData) { + this.initialDataPreparation(landscapeData); + } else { + fetch(import.meta.env.MODE === 'development' ? '../../static/data/full.json' : './data/full.json') + .then((res) => res.json()) + .then((data: LandscapeData) => { + this.initialDataPreparation(data); + }); + } } } + private initialDataPreparation(data: LandscapeData) { + const extendedItems = this.extendItemsData(data.items, data.crunchbase_data, data.github_data); + this.landscapeData = { + ...data, + items: extendedItems, + }; + this.ready = true; + if (this.updateStatus) { + this.updateStatus.updateStatus(true); + } + this.saveData(); + } + + public isReady(): boolean { + return this.ready; + } + // Get all items public getAll(): Item[] { if (this.ready && this.landscapeData && this.landscapeData.items) { @@ -250,36 +262,38 @@ export class ItemsDataGetter { ): { [key: string]: { [key: string]: (BaseItem | Item)[] } } { const groupedItems: { [key: string]: { [key: string]: (BaseItem | Item)[] } } = {}; - const addItem = (category: string, subcategory: string, item: BaseItem | Item) => { - if (groupedItems[category]) { - if (groupedItems[category][subcategory]) { - groupedItems[category][subcategory].push(item); + if (items && items.length > 0) { + const addItem = (category: string, subcategory: string, item: BaseItem | Item) => { + if (groupedItems[category]) { + if (groupedItems[category][subcategory]) { + groupedItems[category][subcategory].push(item); + } else { + groupedItems[category][subcategory] = [item]; + } } else { - groupedItems[category][subcategory] = [item]; + groupedItems[category] = { [subcategory]: [item] }; } - } else { - groupedItems[category] = { [subcategory]: [item] }; - } - }; + }; - const validateCategory = (category: string): boolean => { - if (!checkIfCategoryInGroup(category, group)) return false; - if (activeCategoryFilters && activeCategoryFilters.length > 0) { - return activeCategoryFilters.includes(category); - } - return true; - }; + const validateCategory = (category: string): boolean => { + if (!checkIfCategoryInGroup(category, group)) return false; + if (activeCategoryFilters && activeCategoryFilters.length > 0) { + return activeCategoryFilters.includes(category); + } + return true; + }; - for (const item of items) { - if (validateCategory(item.category)) { - addItem(item.category, item.subcategory, item); - } - if (item.additional_categories) { - item.additional_categories.forEach((ad: AdditionalCategory) => { - if (validateCategory(ad.category)) { - addItem(ad.category, ad.subcategory, item); - } - }); + for (const item of items) { + if (validateCategory(item.category)) { + addItem(item.category, item.subcategory, item); + } + if (item.additional_categories) { + item.additional_categories.forEach((ad: AdditionalCategory) => { + if (validateCategory(ad.category)) { + addItem(ad.category, ad.subcategory, item); + } + }); + } } } @@ -355,18 +369,20 @@ export class ItemsDataGetter { Object.keys(groupedItems).forEach((group: string) => { data[group] = {}; const items = groupedItems![group]; - items.forEach((item: BaseItem | Item) => { - if (validateCategory(item.category, group)) { - addItem(group, item.category, item.subcategory, item); - } - if (item.additional_categories) { - for (const ac of item.additional_categories) { - if (validateCategory(ac.category, group)) { - addItem(group, ac.category, ac.subcategory, item); + if (!isUndefined(items)) { + items.forEach((item: BaseItem | Item) => { + if (validateCategory(item.category, group)) { + addItem(group, item.category, item.subcategory, item); + } + if (item.additional_categories) { + for (const ac of item.additional_categories) { + if (validateCategory(ac.category, group)) { + addItem(group, ac.category, ac.subcategory, item); + } } } - } - }); + }); + } }); } return data; @@ -459,7 +475,7 @@ export class ItemsDataGetter { ); classifyCardData[activeGroup] = classifyGroup; menuData[activeGroup] = this.getMenuOptions(classifyGroup, classify); - numItems[activeGroup] = groupedItems[activeGroup].length; + numItems[activeGroup] = groupedItems[activeGroup] ? groupedItems[activeGroup].length : 0; } else { const classifiedOption = !isUndefined(this.classifyAndSortOptions) && this.classifyAndSortOptions[group].classify.includes(classify) @@ -522,15 +538,17 @@ export class ItemsDataGetter { if (this.ready && this.landscapeData && this.landscapeData.items) { const allGroupedItems = this.getGroupedData(); const options: string[] = []; - allGroupedItems[group].forEach((i: Item) => { - if (i.repositories) { - i.repositories.forEach((r: Repository) => { - if (r.github_data) { - options.push(r.github_data!.license); - } - }); - } - }); + if (!isUndefined(allGroupedItems[group])) { + allGroupedItems[group].forEach((i: Item) => { + if (i.repositories) { + i.repositories.forEach((r: Repository) => { + if (r.github_data) { + options.push(r.github_data!.license); + } + }); + } + }); + } return uniq(compact(options)); } return []; diff --git a/web/src/utils/itemsIterator.tsx b/ui/webapp/src/utils/itemsIterator.tsx similarity index 100% rename from web/src/utils/itemsIterator.tsx rename to ui/webapp/src/utils/itemsIterator.tsx diff --git a/web/src/utils/nestArray.ts b/ui/webapp/src/utils/nestArray.ts similarity index 100% rename from web/src/utils/nestArray.ts rename to ui/webapp/src/utils/nestArray.ts diff --git a/ui/webapp/src/utils/overlayData.ts b/ui/webapp/src/utils/overlayData.ts new file mode 100644 index 00000000..a7c09522 --- /dev/null +++ b/ui/webapp/src/utils/overlayData.ts @@ -0,0 +1,148 @@ +import isEmpty from 'lodash/isEmpty'; + +import { + BASE_PATH, + DEFAULT_GRID_ITEMS_SIZE, + DEFAULT_VIEW_MODE, + OVERLAY_DATA_PARAM, + OVERLAY_GUIDE_PARAM, + OVERLAY_LOGOS_PATH_PARAM, + OVERLAY_SETTINGS_PARAM, + overrideSettings, +} from '../data'; +import itemsDataGetter from './itemsDataGetter'; + +interface OverlayParams { + data?: string; + settings?: string; + guide?: string; + logosPath?: string; +} + +interface OverlayInput { + landscape_url: string; + data_url?: string; + settings_url?: string; + guide_url?: string; + logos_url?: string; +} + +export class OverlayData { + private isActive: boolean = false; + private params: OverlayParams | undefined = undefined; + private query: URLSearchParams = new URLSearchParams(); + + public checkIfOverlayInQuery() { + const searchParams = new URLSearchParams(window.location.search); + for (const [key] of searchParams.entries()) { + if (key.startsWith('overlay-')) { + this.init(); + return true; + } + } + return false; + } + + public init() { + this.isActive = true; + document.body.classList.add('overlay-active'); + this.saveUrlParams(); + } + + private saveUrlParams() { + const currentSearch = new URLSearchParams(window.location.search); + const dataUrl = currentSearch.get(OVERLAY_DATA_PARAM); + const settingsUrl = currentSearch.get(OVERLAY_SETTINGS_PARAM); + const guideUrl = currentSearch.get(OVERLAY_GUIDE_PARAM); + const logosPathUrl = currentSearch.get(OVERLAY_LOGOS_PATH_PARAM); + + this.params = {}; + + // save url params + if (dataUrl) { + this.params.data = dataUrl; + this.query.set(OVERLAY_DATA_PARAM, dataUrl || ''); + } + if (settingsUrl) { + this.params.settings = settingsUrl; + this.query.set(OVERLAY_SETTINGS_PARAM, settingsUrl); + } + if (guideUrl) { + this.params.guide = guideUrl; + this.query.set(OVERLAY_GUIDE_PARAM, guideUrl); + } + if (logosPathUrl) { + this.params.logosPath = logosPathUrl; + this.query.set(OVERLAY_LOGOS_PATH_PARAM, logosPathUrl); + } + } + + public isActiveOverlay() { + return this.isActive; + } + + public getUrlParams() { + return this.isActive ? this.query.toString() : ''; + } + + private isWasmSupported() { + if (typeof WebAssembly !== 'undefined') { + // WebAssembly is supported + return true; + } else { + // WebAssembly is not supported + return false; + } + } + + public async getOverlayBaseData() { + if (!this.isWasmSupported()) { + return Promise.reject('WebAssembly is not supported in this browser'); + } else { + return import('../../wasm/overlay/landscape2_overlay') + .then(async (obj) => { + await obj.default(); + + const input: OverlayInput = { + landscape_url: `${import.meta.env.MODE === 'development' ? 'http://localhost:8000' : window.location.origin}${BASE_PATH}`, + }; + + if (!isEmpty(this.params)) { + if (this.params.data) { + input.data_url = this.params.data; + } + if (this.params.settings) { + input.settings_url = this.params.settings; + } + if (this.params.guide) { + input.guide_url = this.params.guide; + } + if (this.params.logosPath) { + input.logos_url = this.params.logosPath; + } + } + + const overlayData = await obj.get_overlay_data(input); + + const data = JSON.parse(overlayData); + window.baseDS = data.datasets.base; + window.statsDS = data.datasets.stats; + window.guide = data.guide; + itemsDataGetter.init(data.datasets.full); + overrideSettings({ + foundationName: data.datasets.base.foundation || '', + gridSize: data.datasets.base.grid_items_size || DEFAULT_GRID_ITEMS_SIZE, + viewMode: data.datasets.base.view_mode || DEFAULT_VIEW_MODE, + }); + + return data.datasets.base; + }) + .catch((err) => { + return Promise.reject(err); + }); + } + } +} + +const overlayData = new OverlayData(); +export default overlayData; diff --git a/web/src/utils/prepareFilters.ts b/ui/webapp/src/utils/prepareFilters.ts similarity index 100% rename from web/src/utils/prepareFilters.ts rename to ui/webapp/src/utils/prepareFilters.ts diff --git a/web/src/utils/prepareFinances.tsx b/ui/webapp/src/utils/prepareFinances.tsx similarity index 100% rename from web/src/utils/prepareFinances.tsx rename to ui/webapp/src/utils/prepareFinances.tsx diff --git a/ui/webapp/src/utils/prepareLink.ts b/ui/webapp/src/utils/prepareLink.ts new file mode 100644 index 00000000..480ce2ad --- /dev/null +++ b/ui/webapp/src/utils/prepareLink.ts @@ -0,0 +1,13 @@ +import { isUndefined } from 'lodash'; + +import overlayData from './overlayData'; + +const prepareLink = (path: string, extraQuery?: string): string => { + if (overlayData.isActiveOverlay()) { + return `${path}?${overlayData.getUrlParams()}${!isUndefined(extraQuery) ? `&${extraQuery}` : ''}`; + } else { + return `${path}${!isUndefined(extraQuery) ? `/?${extraQuery}` : ''}`; + } +}; + +export default prepareLink; diff --git a/web/src/utils/prepareMenu.ts b/ui/webapp/src/utils/prepareMenu.ts similarity index 100% rename from web/src/utils/prepareMenu.ts rename to ui/webapp/src/utils/prepareMenu.ts diff --git a/web/src/utils/prettifyBytes.ts b/ui/webapp/src/utils/prettifyBytes.ts similarity index 100% rename from web/src/utils/prettifyBytes.ts rename to ui/webapp/src/utils/prettifyBytes.ts diff --git a/web/src/utils/prettifyNumber.ts b/ui/webapp/src/utils/prettifyNumber.ts similarity index 100% rename from web/src/utils/prettifyNumber.ts rename to ui/webapp/src/utils/prettifyNumber.ts diff --git a/web/src/utils/rgba2hex.ts b/ui/webapp/src/utils/rgba2hex.ts similarity index 100% rename from web/src/utils/rgba2hex.ts rename to ui/webapp/src/utils/rgba2hex.ts diff --git a/web/src/utils/scrollToTop.ts b/ui/webapp/src/utils/scrollToTop.ts similarity index 100% rename from web/src/utils/scrollToTop.ts rename to ui/webapp/src/utils/scrollToTop.ts diff --git a/web/src/utils/sortItems.ts b/ui/webapp/src/utils/sortItems.ts similarity index 100% rename from web/src/utils/sortItems.ts rename to ui/webapp/src/utils/sortItems.ts diff --git a/web/src/utils/sortItemsByOrderValue.ts b/ui/webapp/src/utils/sortItemsByOrderValue.ts similarity index 100% rename from web/src/utils/sortItemsByOrderValue.ts rename to ui/webapp/src/utils/sortItemsByOrderValue.ts diff --git a/web/src/utils/sortMenuOptions.ts b/ui/webapp/src/utils/sortMenuOptions.ts similarity index 100% rename from web/src/utils/sortMenuOptions.ts rename to ui/webapp/src/utils/sortMenuOptions.ts diff --git a/web/src/utils/sortObjectByValue.ts b/ui/webapp/src/utils/sortObjectByValue.ts similarity index 100% rename from web/src/utils/sortObjectByValue.ts rename to ui/webapp/src/utils/sortObjectByValue.ts diff --git a/web/src/utils/sumValues.ts b/ui/webapp/src/utils/sumValues.ts similarity index 100% rename from web/src/utils/sumValues.ts rename to ui/webapp/src/utils/sumValues.ts diff --git a/web/src/utils/updateAlphaInColor.ts b/ui/webapp/src/utils/updateAlphaInColor.ts similarity index 100% rename from web/src/utils/updateAlphaInColor.ts rename to ui/webapp/src/utils/updateAlphaInColor.ts diff --git a/web/src/vite-env.d.ts b/ui/webapp/src/vite-env.d.ts similarity index 100% rename from web/src/vite-env.d.ts rename to ui/webapp/src/vite-env.d.ts diff --git a/web/src/window.d.ts b/ui/webapp/src/window.d.ts similarity index 75% rename from web/src/window.d.ts rename to ui/webapp/src/window.d.ts index 7f50ef0d..320bd966 100644 --- a/web/src/window.d.ts +++ b/ui/webapp/src/window.d.ts @@ -1,9 +1,10 @@ -import { BaseData, Stats } from './types'; +import { BaseData, Guide, Stats } from './types'; declare global { interface Window { baseDS: BaseData; statsDS: Stats; + guide?: Guide; basePath?: string; Osano: { cm: { showDrawer: (t: string) => void; addEventListener: (t: string, f: unknown) => void } }; } diff --git a/web/tsconfig.json b/ui/webapp/tsconfig.json similarity index 94% rename from web/tsconfig.json rename to ui/webapp/tsconfig.json index 39999584..af666a77 100644 --- a/web/tsconfig.json +++ b/ui/webapp/tsconfig.json @@ -21,6 +21,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"], + "include": ["src", "wasm/overlay"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/web/tsconfig.node.json b/ui/webapp/tsconfig.node.json similarity index 100% rename from web/tsconfig.node.json rename to ui/webapp/tsconfig.node.json diff --git a/web/vite.config.ts b/ui/webapp/vite.config.ts similarity index 100% rename from web/vite.config.ts rename to ui/webapp/vite.config.ts diff --git a/web/yarn.lock b/ui/webapp/yarn.lock similarity index 100% rename from web/yarn.lock rename to ui/webapp/yarn.lock diff --git a/web/src/App.tsx b/web/src/App.tsx deleted file mode 100644 index 0cde832d..00000000 --- a/web/src/App.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Route, Router } from '@solidjs/router'; -import isUndefined from 'lodash/isUndefined'; -import range from 'lodash/range'; -import { createSignal, onMount } from 'solid-js'; - -import { - EMBED_SETUP_PATH, - EXPLORE_PATH, - FINANCES_PATH, - GUIDE_PATH, - LOGOS_PREVIEW_PATH, - SCREENSHOTS_PATH, - STATS_PATH, -} from './data'; -import Layout from './layout'; -import Explore from './layout/explore'; -import Finances from './layout/finances'; -import Guide from './layout/guide'; -import Logos from './layout/logos'; -import NotFound from './layout/notFound'; -import Screenshots from './layout/screenshots'; -import Stats from './layout/stats'; -import itemsDataGetter from './utils/itemsDataGetter'; -import updateAlphaInColor from './utils/updateAlphaInColor'; - -// Colors -let COLOR_1 = 'rgba(0, 107, 204, 1)'; -let COLOR_1_HOVER = 'rgba(0, 107, 204, 0.75)'; -let COLOR_2 = 'rgba(255, 0, 170, 1)'; -let COLOR_3 = 'rgba(96, 149, 214, 1)'; -let COLOR_4 = 'rgba(0, 42, 81, 0.7)'; -let COLOR_5 = 'rgba(1, 107, 204, 0.7)'; -let COLOR_6 = 'rgba(0, 42, 81, 0.7)'; - -const App = () => { - const [data] = createSignal(window.baseDS); - - onMount(() => { - if (!isUndefined(window.baseDS.colors)) { - if (!isUndefined(window.baseDS.colors?.color1)) { - COLOR_1 = window.baseDS.colors?.color1; - COLOR_1_HOVER = updateAlphaInColor(COLOR_1, 0.75); - } - if (!isUndefined(window.baseDS.colors?.color2)) { - COLOR_2 = window.baseDS.colors?.color2; - } - if (!isUndefined(window.baseDS.colors?.color3)) { - COLOR_3 = window.baseDS.colors?.color3; - } - if (!isUndefined(window.baseDS.colors?.color4)) { - COLOR_4 = window.baseDS.colors?.color4; - } - if (!isUndefined(window.baseDS.colors?.color5)) { - COLOR_5 = window.baseDS.colors?.color5; - } - if (!isUndefined(window.baseDS.colors?.color6)) { - COLOR_6 = window.baseDS.colors?.color6; - } - } - - const loadColors = () => { - const colors = [COLOR_1, COLOR_2, COLOR_3, COLOR_4, COLOR_5, COLOR_6]; - range(colors.length).forEach((i: number) => { - document.documentElement.style.setProperty(`--color${i + 1}`, colors[i]); - }); - document.documentElement.style.setProperty('--color1-hover', COLOR_1_HOVER); - }; - - loadColors(); - itemsDataGetter.init(); - - if (window.Osano) { - window.Osano.cm.addEventListener('osano-cm-initialized', () => { - document.body.classList.add('osano-loaded'); - }); - } - }); - - return ( - - } /> - - - - - - } /> - - ); -}; - -export default App; diff --git a/web/src/layout/Layout.module.css b/web/src/layout/Layout.module.css deleted file mode 100644 index d44174da..00000000 --- a/web/src/layout/Layout.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.container { - min-height: 100vh; -} diff --git a/web/src/utils/getBasePath.ts b/web/src/utils/getBasePath.ts deleted file mode 100644 index 26906912..00000000 --- a/web/src/utils/getBasePath.ts +++ /dev/null @@ -1,5 +0,0 @@ -const getBasePath = () => { - return window.baseDS.base_path || ''; -}; - -export default getBasePath; diff --git a/web/src/utils/getFoundationNameLabel.ts b/web/src/utils/getFoundationNameLabel.ts deleted file mode 100644 index 490d6f4b..00000000 --- a/web/src/utils/getFoundationNameLabel.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { REGEX_SPACE } from '../data'; - -const getFoundationNameLabel = (): string => { - const FOUNDATION: string = window.baseDS.foundation; - return FOUNDATION.toLowerCase().replace(REGEX_SPACE, ''); -}; - -export default getFoundationNameLabel; From a3f5046ead492de2cfb336e019d120fbb2db35af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Mon, 29 Apr 2024 09:26:05 +0200 Subject: [PATCH 25/55] Provide defaults for some fields in full dataset (#588) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- crates/core/src/datasets.rs | 8 ++++---- crates/wasm/overlay/src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/core/src/datasets.rs b/crates/core/src/datasets.rs index 4e88c055..72ae51e5 100644 --- a/crates/core/src/datasets.rs +++ b/crates/core/src/datasets.rs @@ -37,7 +37,7 @@ impl Datasets { Datasets { base: Base::new(i.landscape_data, i.settings, i.guide, i.qr_code), embed: Embed::new(i.landscape_data), - full: Full::new(i.crunchbase_data, i.github_data, i.landscape_data), + full: Full::new(i.landscape_data, i.crunchbase_data, i.github_data), stats: Stats::new(i.landscape_data, i.settings, i.crunchbase_data), } } @@ -333,10 +333,10 @@ pub mod full { /// Full dataset information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Full { - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub crunchbase_data: CrunchbaseData, - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub github_data: GithubData, #[serde(skip_serializing_if = "Vec::is_empty")] @@ -347,9 +347,9 @@ pub mod full { /// Create a new Full instance from the landscape data provided. #[must_use] pub fn new( + landscape_data: &LandscapeData, crunchbase_data: &CrunchbaseData, github_data: &GithubData, - landscape_data: &LandscapeData, ) -> Self { Full { crunchbase_data: crunchbase_data.clone(), diff --git a/crates/wasm/overlay/src/lib.rs b/crates/wasm/overlay/src/lib.rs index 24e99257..f890fdf3 100644 --- a/crates/wasm/overlay/src/lib.rs +++ b/crates/wasm/overlay/src/lib.rs @@ -100,7 +100,7 @@ pub async fn get_overlay_data(input: JsValue) -> Result { let qr_code = String::new(); let datasets = Datasets { base: Base::new(&landscape_data, &settings, &guide, &qr_code), - full: Full::new(&crunchbase_data, &github_data, &landscape_data), + full: Full::new(&landscape_data, &crunchbase_data, &github_data), stats: Stats::new(&landscape_data, &settings, &crunchbase_data), }; From 5565ca083336ac36b3c282d4a4f025c210462a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Mon, 29 Apr 2024 09:53:06 +0200 Subject: [PATCH 26/55] Use only orgs in data file when processing stats (#589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- crates/core/src/datasets.rs | 2 +- crates/core/src/stats.rs | 29 ++++++++++++++++++----------- crates/wasm/overlay/src/lib.rs | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/core/src/datasets.rs b/crates/core/src/datasets.rs index 72ae51e5..6143b24e 100644 --- a/crates/core/src/datasets.rs +++ b/crates/core/src/datasets.rs @@ -38,7 +38,7 @@ impl Datasets { base: Base::new(i.landscape_data, i.settings, i.guide, i.qr_code), embed: Embed::new(i.landscape_data), full: Full::new(i.landscape_data, i.crunchbase_data, i.github_data), - stats: Stats::new(i.landscape_data, i.settings, i.crunchbase_data), + stats: Stats::new(i.landscape_data, i.settings), } } } diff --git a/crates/core/src/stats.rs b/crates/core/src/stats.rs index 6968f8d3..5015fd61 100644 --- a/crates/core/src/stats.rs +++ b/crates/core/src/stats.rs @@ -5,7 +5,7 @@ use super::{ data::{CategoryName, SubCategoryName}, settings::{LandscapeSettings, TagName}, }; -use crate::data::{CrunchbaseData, LandscapeData}; +use crate::data::LandscapeData; use chrono::{Datelike, Utc}; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -44,14 +44,10 @@ impl Stats { /// Create a new Stats instance from the information available in the /// landscape. #[must_use] - pub fn new( - landscape_data: &LandscapeData, - settings: &LandscapeSettings, - crunchbase_data: &CrunchbaseData, - ) -> Self { + pub fn new(landscape_data: &LandscapeData, settings: &LandscapeSettings) -> Self { Self { members: MembersStats::new(landscape_data, settings), - organizations: OrganizationsStats::new(crunchbase_data), + organizations: OrganizationsStats::new(landscape_data), projects: ProjectsStats::new(landscape_data), repositories: RepositoriesStats::new(landscape_data), } @@ -134,13 +130,22 @@ pub struct OrganizationsStats { impl OrganizationsStats { /// Create a new OrganizationsStats instance from the information available /// in the landscape. - fn new(crunchbase_data: &CrunchbaseData) -> Option { + fn new(landscape_data: &LandscapeData) -> Option { let mut stats = OrganizationsStats::default(); + let mut crunchbase_data_processed = HashSet::new(); // Collect stats from landscape items - for org in crunchbase_data.values() { + for item in &landscape_data.items { + // Check if this crunchbase data has already been processed + if let Some(url) = item.crunchbase_url.as_ref() { + if crunchbase_data_processed.contains(url) { + continue; + } + crunchbase_data_processed.insert(url); + } + // Acquisitions - if let Some(acquisitions) = org.acquisitions.as_ref() { + if let Some(acquisitions) = item.crunchbase_data.as_ref().and_then(|d| d.acquisitions.as_ref()) { for acq in acquisitions { if let Some(announced_on) = acq.announced_on { let year = announced_on.format("%Y").to_string(); @@ -155,7 +160,9 @@ impl OrganizationsStats { } // Funding rounds - if let Some(funding_rounds) = org.funding_rounds.as_ref() { + if let Some(funding_rounds) = + item.crunchbase_data.as_ref().and_then(|d| d.funding_rounds.as_ref()) + { for fr in funding_rounds { if let Some(announced_on) = fr.announced_on { // Only funding rounds in the last 5 years diff --git a/crates/wasm/overlay/src/lib.rs b/crates/wasm/overlay/src/lib.rs index f890fdf3..e2f0eabd 100644 --- a/crates/wasm/overlay/src/lib.rs +++ b/crates/wasm/overlay/src/lib.rs @@ -101,7 +101,7 @@ pub async fn get_overlay_data(input: JsValue) -> Result { let datasets = Datasets { base: Base::new(&landscape_data, &settings, &guide, &qr_code), full: Full::new(&landscape_data, &crunchbase_data, &github_data), - stats: Stats::new(&landscape_data, &settings, &crunchbase_data), + stats: Stats::new(&landscape_data, &settings), }; // Prepare overlay data and return it From 6dc7274fb7246c520683f8976bff3172fa4904c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Mon, 29 Apr 2024 10:35:57 +0200 Subject: [PATCH 27/55] Collect repositories topics from GitHub (#590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- crates/cli/src/build/github.rs | 1 + crates/core/src/data.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/crates/cli/src/build/github.rs b/crates/cli/src/build/github.rs index c9a844b1..d2a638d3 100644 --- a/crates/cli/src/build/github.rs +++ b/crates/cli/src/build/github.rs @@ -161,6 +161,7 @@ async fn collect_repository_data(gh: Object, repo_url: &str) -> Result, pub stars: i64, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub topics: Vec, pub url: String, #[serde(skip_serializing_if = "Option::is_none")] From 48f58cf8c4dbf301c8fe1d40be1d94c526446f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Mon, 29 Apr 2024 13:11:33 +0200 Subject: [PATCH 28/55] Add package manager url field (#592) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- crates/core/src/data.rs | 4 ++++ crates/core/src/data/legacy.rs | 1 + 2 files changed, 5 insertions(+) diff --git a/crates/core/src/data.rs b/crates/core/src/data.rs index a8dbf2b8..4143a7f3 100644 --- a/crates/core/src/data.rs +++ b/crates/core/src/data.rs @@ -397,6 +397,7 @@ impl From for LandscapeData { item.latest_annual_review_url = extra.annual_review_url; item.linkedin_url = extra.linkedin_url; item.mailing_list_url = extra.mailing_list_url; + item.package_manager_url = extra.package_manager_url; item.parent_project = extra.parent_project; item.slack_url = extra.slack_url; item.specification = extra.specification; @@ -576,6 +577,9 @@ pub struct Item { #[serde(skip_serializing_if = "Option::is_none")] pub oss: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub package_manager_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub parent_project: Option, diff --git a/crates/core/src/data/legacy.rs b/crates/core/src/data/legacy.rs index 10b5cb6e..fe0a7b21 100644 --- a/crates/core/src/data/legacy.rs +++ b/crates/core/src/data/legacy.rs @@ -151,6 +151,7 @@ pub(super) struct ItemExtra { pub incubating: Option, pub linkedin_url: Option, pub mailing_list_url: Option, + pub package_manager_url: Option, pub parent_project: Option, pub slack_url: Option, pub specification: Option, From ad84e3eeb693b33006031f627a641c60051a81fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cintia=20S=C3=A1nchez=20Garc=C3=ADa?= Date: Mon, 29 Apr 2024 13:33:08 +0200 Subject: [PATCH 29/55] Fix some issues with filters modal (#593) Signed-off-by: Cintia Sanchez Garcia --- ui/webapp/src/data.ts | 8 +++++++- ui/webapp/src/layout/explore/filters/index.tsx | 13 ++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ui/webapp/src/data.ts b/ui/webapp/src/data.ts index c7cb56b8..a143460a 100644 --- a/ui/webapp/src/data.ts +++ b/ui/webapp/src/data.ts @@ -184,7 +184,13 @@ export let FILTERS: FilterSection[] = [ ]; export const FILTER_CATEGORIES_PER_TITLE: FilterCategoriesPerTitle = { - [FilterTitle.Project]: [FilterCategory.Maturity, FilterCategory.TAG, FilterCategory.License, FilterCategory.Extra], + [FilterTitle.Project]: [ + FilterCategory.Maturity, + FilterCategory.TAG, + FilterCategory.License, + FilterCategory.Category, + FilterCategory.Extra, + ], [FilterTitle.Organization]: [ FilterCategory.Organization, FilterCategory.OrgType, diff --git a/ui/webapp/src/layout/explore/filters/index.tsx b/ui/webapp/src/layout/explore/filters/index.tsx index 8f154668..2391f65e 100644 --- a/ui/webapp/src/layout/explore/filters/index.tsx +++ b/ui/webapp/src/layout/explore/filters/index.tsx @@ -23,6 +23,7 @@ import getFoundationNameLabel from '../../../utils/getFoundationNameLabel'; import getFiltersPerGroup, { FiltersPerGroup } from '../../../utils/prepareFilters'; import Loading from '../../common/Loading'; import Modal from '../../common/Modal'; +import NoData from '../../common/NoData'; import Section from '../../common/Section'; import SVGIcon from '../../common/SVGIcon'; import { useViewMode } from '../../stores/viewMode'; @@ -142,7 +143,14 @@ const Filters = (props: Props) => { const getSectionInPredefinedFilters = (id: FilterCategory): FilterSection | undefined => { const section = FILTERS.find((sec: FilterSection) => sec.value === id); - if (section) { + let availableExtraOptions: boolean = true; + if ([FilterCategory.Maturity, FilterCategory.OrgType, FilterCategory.License].includes(id)) { + const activeOpts = getSection(id); + if (isUndefined(activeOpts)) { + availableExtraOptions = false; + } + } + if (section && availableExtraOptions) { return section; } return; @@ -301,6 +309,9 @@ const Filters = (props: Props) => {
+ + There are no filters available that can be applied to the current set of items +
{FilterTitle.Project}
From 371cebb4c08dfee34b98af04bfd41f88a17c9940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cintia=20S=C3=A1nchez=20Garc=C3=ADa?= Date: Mon, 29 Apr 2024 13:49:40 +0200 Subject: [PATCH 30/55] Add some more options to logos preview (#594) Add some more options to logos preview Closes #526 and #506 Signed-off-by: Cintia Sanchez Garcia --- ui/webapp/src/layout/logos/Logos.module.css | 2 +- ui/webapp/src/layout/logos/index.tsx | 46 ++++++++++++++++++++- ui/webapp/src/utils/itemsDataGetter.ts | 14 +++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/ui/webapp/src/layout/logos/Logos.module.css b/ui/webapp/src/layout/logos/Logos.module.css index 72bead7e..f242e3db 100644 --- a/ui/webapp/src/layout/logos/Logos.module.css +++ b/ui/webapp/src/layout/logos/Logos.module.css @@ -4,7 +4,7 @@ .select { box-shadow: 0 0 0 0.2rem var(--bs-gray-200); - margin: 0 0.2rem; + margin: 0.5rem 0.2rem 0 0.2rem; } .select:focus { diff --git a/ui/webapp/src/layout/logos/index.tsx b/ui/webapp/src/layout/logos/index.tsx index a73045f3..e3168e2d 100644 --- a/ui/webapp/src/layout/logos/index.tsx +++ b/ui/webapp/src/layout/logos/index.tsx @@ -18,6 +18,7 @@ const Logos = () => { const [selectedSuboptionValue, setSelectedSuboptionValue] = createSignal(); const [suboptions, setSuboptions] = createSignal(); const [items, setItems] = createSignal(); + const [hiddenNonPublicOrgs, setHiddenNonPublicOrgs] = createSignal(false); const cleanDuplicatedItems = (itemsList: Item[]): Item[] => { const result: Item[] = []; @@ -35,6 +36,18 @@ const Logos = () => { return result; }; + const cleanItems = (itemsList: Item[]): Item[] => { + let result = cleanDuplicatedItems(itemsList); + + const items = cleanDuplicatedItems(itemsList); + if (hiddenNonPublicOrgs()) { + const nonPublicOrgs = items.filter((item) => !item.name.startsWith('Non-Public Organization')); + result = nonPublicOrgs; + } + + return result; + }; + const filterItems = () => { let list: Item[] = []; if (!isUndefined(selectedGroup())) { @@ -62,6 +75,10 @@ const Logos = () => { list = itemsDataGetter.getItemsByEndUser() || []; break; + case 'allenduser': + list = itemsDataGetter.getAllEndUserItems() || []; + break; + default: break; } @@ -72,7 +89,7 @@ const Logos = () => { break; } } - setItems(cleanDuplicatedItems(list)); + setItems(cleanItems(list)); }; const getOptions = (): FilterOption[] => { @@ -107,6 +124,14 @@ const Logos = () => { }) ); + createEffect( + on(hiddenNonPublicOrgs, () => { + if (!isUndefined(items())) { + filterItems(); + } + }) + ); + return ( <>
@@ -116,6 +141,10 @@ const Logos = () => {
}>
+
+ Select items to display +
+
{(option) => ( @@ -208,6 +237,21 @@ const Logos = () => {
+
+
+ setHiddenNonPublicOrgs(!hiddenNonPublicOrgs())} + checked={hiddenNonPublicOrgs()} + class="form-check-input" + type="checkbox" + role="switch" + id="visibleNonPublicOrgs" + /> + +
+
diff --git a/ui/webapp/src/utils/itemsDataGetter.ts b/ui/webapp/src/utils/itemsDataGetter.ts index 79f53a85..0fd81924 100644 --- a/ui/webapp/src/utils/itemsDataGetter.ts +++ b/ui/webapp/src/utils/itemsDataGetter.ts @@ -527,6 +527,16 @@ export class ItemsDataGetter { } } + // Get all end users + public getAllEndUserItems(): Item[] | undefined { + if (this.ready && this.landscapeData && this.landscapeData.items) { + return this.landscapeData.items.filter( + (i: Item) => + (i.enduser && window.baseDS.members_category === i.category) || i.subcategory === 'End User Supporter' + ); + } + } + // Get maturity options public getMaturityOptions(): string[] { const maturity = window.baseDS.items.map((i: Item) => i.maturity); @@ -613,6 +623,10 @@ export class ItemsDataGetter { value: 'enduser', name: 'End user members', }, + { + value: 'allenduser', + name: 'All end users', + }, ], }); } From ff63207d570e110f5324b6afe840ecf1f492cbfd Mon Sep 17 00:00:00 2001 From: Benjamin Granados <40007659+benjagm@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:53:38 +0200 Subject: [PATCH 31/55] Add JSON Schema as adopter (#599) Signed-off-by: Benjamin Granados --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 0d795831..b6a1910a 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -10,6 +10,7 @@ If you are using **Landscape2**, please consider adding your landscape to this l - [Continuous Delivery Foundation](https://cdf.landscape2.io) - [GraphQL](https://graphql.landscape2.io) - [Hyperledger Foundation](https://dlt.landscape2.io) +- [JSON Schema](https://landscape.json-schema.org) - [LF AI & DATA](https://lfai.landscape2.io) - [LF Energy](https://lfenergy.landscape2.io) - [LF Networking](https://lfnetworking.landscape2.io) From 5afb0589414453d67714eecd113e21c32e26b022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Mon, 29 Apr 2024 18:17:43 +0200 Subject: [PATCH 32/55] Do not skip repository topics serialization (#600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- crates/core/src/data.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/core/src/data.rs b/crates/core/src/data.rs index 4143a7f3..8c862938 100644 --- a/crates/core/src/data.rs +++ b/crates/core/src/data.rs @@ -847,7 +847,6 @@ pub struct RepositoryGithubData { pub latest_commit: Commit, pub participation_stats: Vec, pub stars: i64, - #[serde(skip_serializing_if = "Vec::is_empty")] pub topics: Vec, pub url: String, From 8db5bfa5c6a68bbc6add2a5b943943960174019a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Tue, 30 Apr 2024 09:16:13 +0200 Subject: [PATCH 33/55] Add overlay section to README file (#602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 99302327..db241016 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,27 @@ Some operations like collecting data from external sources or processing a lot o > [!NOTE] > In addition to the customization options available in the embed setup view, it's also possible to embed views using [iframe-resizer](https://github.com/davidjbradshaw/iframe-resizer). This feature can be enabled by adding `iframe-resizer=true` to the embed url ([demo](https://codepen.io/cynthiasg/pen/WNmQjje)). +### Overlay + +**Landscape2** supports applying one or more data source files to an existing landscape at runtime. Any of those files can -and often will- be different than the ones used originally to build the landscape. This feature aims to be the building blocks of a preview system. + +Some examples of it in action (live demos!): + +- [Preview some changes in the settings file (like a Christmas edition theme)](https://landscape.cncf.io/?overlay-settings=https://raw.githubusercontent.com/tegioz/landscape2-overlay-tests/main/settings-christmas.yml) +- [Check out how the CNCF landscape would look like using a 5 years old `landscape.yml` file (Helm was still in incubating!)](https://landscape.cncf.io/?overlay-data=https://raw.githubusercontent.com/cncf/landscape/6ff062c72b7229bbe60fee3aca5f8999624459f2/landscape.yml&overlay-logos=https://raw.githubusercontent.com/cncf/landscape/6ff062c72b7229bbe60fee3aca5f8999624459f2/hosted_logos&overlay-settings=https://raw.githubusercontent.com/tegioz/landscape2-overlay-tests/main/settings-5-years-ago.yml) + +To achieve this, the overlay redoes part of the landscape build process in the browser, reusing the same codebase packed as a WASM module. + +The overlay can be enabled by providing any of the following query parameters to the landscape url: + +- `overlay-data`: *data file url* +- `overlay-settings`: *settings file url* +- `overlay-guide`: *guide file url* +- `overlay-logos`: *logos base url* + +> [!WARNING] +> The overlay feature is still experimental and may not work as expected in all cases. Please report any issues you find! + ## Adopters A list of landscapes built using **landscape2** is available at [ADOPTERS.md](./ADOPTERS.md). Please consider adding yours! From 0d77e4bac29c032b332b9f3067f5eff718b2acf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Tue, 30 Apr 2024 10:57:25 +0200 Subject: [PATCH 34/55] Use default value when deserializing some fields (#604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- crates/core/src/data.rs | 1 + crates/core/src/datasets.rs | 16 +++++++------- crates/core/src/stats.rs | 42 ++++++++++++++++++------------------- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/crates/core/src/data.rs b/crates/core/src/data.rs index 8c862938..406f1776 100644 --- a/crates/core/src/data.rs +++ b/crates/core/src/data.rs @@ -847,6 +847,7 @@ pub struct RepositoryGithubData { pub latest_commit: Commit, pub participation_stats: Vec, pub stars: i64, + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub topics: Vec, pub url: String, diff --git a/crates/core/src/datasets.rs b/crates/core/src/datasets.rs index 6143b24e..20131ba7 100644 --- a/crates/core/src/datasets.rs +++ b/crates/core/src/datasets.rs @@ -80,10 +80,10 @@ pub mod base { #[serde(skip_serializing_if = "Option::is_none")] pub base_path: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub categories: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub categories_overridden: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -95,10 +95,10 @@ pub mod base { #[serde(skip_serializing_if = "Option::is_none")] pub grid_items_size: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub groups: Vec, - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub guide_summary: GuideSummary, #[serde(skip_serializing_if = "Option::is_none")] @@ -107,7 +107,7 @@ pub mod base { #[serde(skip_serializing_if = "Option::is_none")] pub images: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub items: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -260,7 +260,7 @@ pub mod embed { /// Embed dataset information. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Embed { - #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] + #[serde(default, flatten, skip_serializing_if = "HashMap::is_empty")] pub views: HashMap, } @@ -315,7 +315,7 @@ pub mod embed { pub struct EmbedView { pub category: Category, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub items: Vec, } } @@ -339,7 +339,7 @@ pub mod full { #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub github_data: GithubData, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub items: Vec, } diff --git a/crates/core/src/stats.rs b/crates/core/src/stats.rs index 5015fd61..449377b7 100644 --- a/crates/core/src/stats.rs +++ b/crates/core/src/stats.rs @@ -58,18 +58,18 @@ impl Stats { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct MembersStats { /// Number of members joined per year-month. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub joined_at: BTreeMap, /// Running total of number of members joined per year-month. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub joined_at_rt: BTreeMap, /// Total number of members. pub members: u64, /// Number of members per subcategory. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub subcategories: BTreeMap, } @@ -111,19 +111,19 @@ impl MembersStats { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct OrganizationsStats { /// Total number of acquisitions per year across all organizations. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub acquisitions: BTreeMap, /// Total acquisitions price per year across all organizations. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub acquisitions_price: BTreeMap, /// Total number of funding rounds per year across all organizations. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub funding_rounds: BTreeMap, /// Total money raised on funding rounds per year across all organizations. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub funding_rounds_money_raised: BTreeMap, } @@ -194,42 +194,42 @@ impl OrganizationsStats { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct ProjectsStats { /// Number of projects accepted per year-month. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub accepted_at: BTreeMap, /// Running total of number of projects accepted per year-month. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub accepted_at_rt: BTreeMap, /// Number of security audits per year-month. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub audits: BTreeMap, /// Running total of number of security audits per year-month. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub audits_rt: BTreeMap, /// Number of projects per category and subcategory. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub category: BTreeMap, /// Promotions from incubating to graduated per year-month. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub incubating_to_graduated: BTreeMap, /// Number of projects per maturity. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub maturity: BTreeMap, /// Total number of projects. pub projects: u64, /// Promotions from sandbox to incubating per year-month. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub sandbox_to_incubating: BTreeMap, /// Number of projects per TAG. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub tag: BTreeMap, } @@ -324,7 +324,7 @@ pub struct CategoryProjectsStats { pub projects: u64, /// Number of projects per subcategory. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub subcategories: BTreeMap, } @@ -338,19 +338,19 @@ pub struct RepositoriesStats { pub contributors: u64, /// Number of repositories where each language is used. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub languages: BTreeMap, /// Source code bytes written on each language. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub languages_bytes: BTreeMap, /// Number of repositories where each license is used. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub licenses: BTreeMap, /// Number of commits per week over the last year. - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub participation_stats: Vec, /// Number of repositories. From f053576e72426eefa30bee93eadfd052ee8c8ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Tue, 30 Apr 2024 11:17:30 +0200 Subject: [PATCH 35/55] Upgrade CLI tool dependencies (#605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- Cargo.lock | 147 +++++++++++++++++++++++++---------------------------- Cargo.toml | 20 ++++---- README.md | 2 +- 3 files changed, 79 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43a75199..a0746ccc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", "once_cell", "version_check", "zerocopy", @@ -110,9 +109,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arrayref" @@ -185,9 +184,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", @@ -216,9 +215,9 @@ checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "aws-config" -version = "1.1.9" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297b64446175a73987cedc3c438d79b2a654d0fff96f65ff530fbe039347644c" +checksum = "b2a4707646259764ab59fd9a50e9de2e92c637b28b36285d6f6fa030e915fbd9" dependencies = [ "aws-credential-types", "aws-runtime", @@ -247,9 +246,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.1.8" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8587ae17c8e967e4b05a62d495be2fb7701bec52a97f7acfe8a29f938384c8" +checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -259,9 +258,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.1.8" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13dc54b4b49f8288532334bba8f87386a40571c47c37b1304979b556dc613c8" +checksum = "f4963ac9ff2d33a4231b3806c1c69f578f221a9cabb89ad2bde62ce2b442c8a7" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -283,9 +282,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.21.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc075ffee2a40cb1590bed35d7ec953589a564e768fa91947c565425cd569269" +checksum = "7f522b68eb0294c59f7beb0defa30e84fed24ebc50ee219e111d6c33eaea96a8" dependencies = [ "ahash", "aws-credential-types", @@ -318,9 +317,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.18.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019a07902c43b03167ea5df0182f0cb63fae89f9a9682c44d18cf2e4a042cb34" +checksum = "3d70fb493f4183f5102d8a8d0cc9b57aec29a762f55c0e7bf527e0f7177bb408" dependencies = [ "aws-credential-types", "aws-runtime", @@ -340,9 +339,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.18.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c46ee08a48a7f4eaa4ad201dcc1dd537b49c50859d14d4510e00ad9d3f9af2" +checksum = "de3f37549b3e38b7ea5efd419d4d7add6ea1e55223506eb0b4fef9d25e7cc90d" dependencies = [ "aws-credential-types", "aws-runtime", @@ -362,9 +361,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.18.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f752ac730125ca6017f72f9db5ec1772c9ecc664f87aa7507a7d81b023c23713" +checksum = "3b2ff219a5d4b795cd33251c19dbe9c4b401f2b2cbe513e07c76ada644eaf34e" dependencies = [ "aws-credential-types", "aws-runtime", @@ -385,9 +384,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d6f29688a4be9895c0ba8bef861ad0c0dac5c15e9618b9b7a6c233990fc263" +checksum = "58b56f1cbe6fd4d0c2573df72868f20ab1c125ca9c9dbce17927a463433a2e57" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -457,9 +456,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.7" +version = "0.60.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f10fa66956f01540051b0aa7ad54574640f748f9839e843442d99b970d3aff9" +checksum = "4a7de001a1b9a25601016d8057ea16e31a45fdca3751304c8edf4ad72e706c08" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -497,9 +496,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53572b4cd934ee5e8461ad53caa36e9d246aaef42166e3ac539e206a925d330" +checksum = "44e7945379821074549168917e89e60630647e186a69243248f08c6d168b975a" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -523,9 +522,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb2b3a7030dc9a3c9a08ce0b25decea5130e9db19619d4dffbbff34f75fe850" +checksum = "4cc56a5c96ec741de6c5e6bf1ce6948be969d6506dfa9c39cffc284e31e4979b" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -566,18 +565,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.7" +version = "0.60.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872c68cf019c0e4afc5de7753c4f7288ce4b71663212771bf5e4542eb9346ca9" +checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.1.8" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dbf2f3da841a8930f159163175cf6a3d16ddde517c1b0fba7aa776822800f40" +checksum = "5a43b56df2c529fe44cb4d92bd64d0479883fb9608ff62daede4df5405381814" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -779,9 +778,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -838,19 +837,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.52.0", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -1008,11 +994,10 @@ checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f" [[package]] name = "deadpool" -version = "0.11.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c380a837cb8cb747898b1be7fa5ba5b871eb1210f8881d6512946c132617f80" +checksum = "ff0fc28638c21092aba483136debc6e177fff3dace8c835d715866923b03323e" dependencies = [ - "console", "deadpool-runtime", "num_cpus", "tokio", @@ -1156,12 +1141,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "encoding_rs" version = "0.8.33" @@ -1866,7 +1845,7 @@ dependencies = [ "parse_link_header", "qrcode", "regex", - "reqwest 0.12.2", + "reqwest 0.12.4", "rust-embed", "serde", "serde_json", @@ -1893,7 +1872,7 @@ dependencies = [ "lazy_static", "markdown", "regex", - "reqwest 0.12.2", + "reqwest 0.12.4", "serde", "serde_yaml", "tracing", @@ -1906,7 +1885,7 @@ version = "0.8.1" dependencies = [ "anyhow", "landscape2-core", - "reqwest 0.12.2", + "reqwest 0.12.4", "serde", "serde-wasm-bindgen", "serde_json", @@ -1979,9 +1958,9 @@ dependencies = [ [[package]] name = "markdown" -version = "1.0.0-alpha.16" +version = "1.0.0-alpha.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0f0025e8c0d89b84d6dc63e859475e40e8e82ab1a08be0a93ad5731513a508" +checksum = "21e27d6220ce21f80ce5c4201f23a37c6f1ad037c72c9d1ff215c2919605a5d6" dependencies = [ "unicode-id", ] @@ -2619,7 +2598,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", @@ -2638,11 +2617,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.2" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64 0.21.7", + "base64 0.22.0", "bytes", "encoding_rs", "futures-core", @@ -2662,7 +2641,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 2.1.2", "serde", "serde_json", "serde_urlencoded", @@ -2675,7 +2654,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.50.0", + "winreg 0.52.0", ] [[package]] @@ -2892,7 +2871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "schannel", "security-framework", ] @@ -2906,6 +2885,22 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.0", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -3051,9 +3046,9 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] @@ -3071,9 +3066,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", @@ -3093,9 +3088,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -3764,12 +3759,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - [[package]] name = "unsafe-libyaml" version = "0.2.11" diff --git a/Cargo.toml b/Cargo.toml index 325901c3..620eb8e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,18 +15,18 @@ authors = ["Sergio Castaño Arteaga", "Cintia Sanchez Garcia"] homepage = "https://github.com/cncf/landscape2" [workspace.dependencies] -anyhow = "1.0.81" +anyhow = "1.0.82" askama = { version = "0.12.1", features = ["serde-json"] } askama_escape = { version = "0.10.3", features = ["json"] } -async-trait = "0.1.79" -aws-config = "1.1.9" -aws-sdk-s3 = "1.21.0" +async-trait = "0.1.80" +aws-config = "1.2.1" +aws-sdk-s3 = "1.24.0" axum = "0.7.5" base64 = "0.22.0" -chrono = { version = "0.4.37", features = ["serde"] } +chrono = { version = "0.4.38", features = ["serde"] } clap = { version = "4.5.4", features = ["derive"] } csv = "1.3.0" -deadpool = "0.11.0" +deadpool = "0.11.2" dirs = "5.0.1" futures = "0.3.30" headless_chrome = { git = "https://github.com/rust-headless-chrome/rust-headless-chrome", rev = "973ebea" } @@ -35,7 +35,7 @@ imagesize = "0.12.0" itertools = "0.12.1" lazy_static = "1.4.0" leaky-bucket = "1.0.1" -markdown = "1.0.0-alpha.16" +markdown = "1.0.0-alpha.17" md-5 = "0.10.6" mime_guess = "2.0.4" mockall = "0.12.1" @@ -44,10 +44,10 @@ octorust = "0.7.0" parse_link_header = "0.3.3" qrcode = "0.14.0" regex = "1.10.4" -reqwest = { version = "0.12.2", features = ["json", "native-tls-vendored"] } +reqwest = { version = "0.12.4", features = ["json", "native-tls-vendored"] } rust-embed = "8.3.0" -serde = { version = "1.0.197", features = ["derive"] } -serde_json = "1.0.115" +serde = { version = "1.0.199", features = ["derive"] } +serde_json = "1.0.116" serde-wasm-bindgen = "0.6.5" serde_yaml = "0.9.34" sha2 = "0.10.8" diff --git a/README.md b/README.md index db241016..710ff63e 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ Some examples of it in action (live demos!): To achieve this, the overlay redoes part of the landscape build process in the browser, reusing the same codebase packed as a WASM module. -The overlay can be enabled by providing any of the following query parameters to the landscape url: +The overlay can be enabled by providing any of the following query parameters to the landscape url (they can be combined if needed): - `overlay-data`: *data file url* - `overlay-settings`: *settings file url* From 2babe7573e85615945a6ae3440b4a314f972dcc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cintia=20S=C3=A1nchez=20Garc=C3=ADa?= Date: Tue, 30 Apr 2024 12:24:35 +0200 Subject: [PATCH 36/55] Do not display empty groups in group selector (#606) Signed-off-by: Cintia Sanchez Garcia --- ui/webapp/src/App.tsx | 1 + ui/webapp/src/layout/explore/index.tsx | 15 ++++-- ui/webapp/src/layout/navigation/Header.tsx | 5 +- ui/webapp/src/layout/screenshots/index.tsx | 4 +- ui/webapp/src/layout/stores/groupActive.tsx | 13 ++--- ui/webapp/src/utils/itemsDataGetter.ts | 54 ++++++++++++++++----- 6 files changed, 66 insertions(+), 26 deletions(-) diff --git a/ui/webapp/src/App.tsx b/ui/webapp/src/App.tsx index 709165f4..5d8fc7b7 100644 --- a/ui/webapp/src/App.tsx +++ b/ui/webapp/src/App.tsx @@ -103,6 +103,7 @@ const App = () => { onMount(() => { const isOverlayActive = overlayData.checkIfOverlayInQuery(); if (!isOverlayActive) { + itemsDataGetter.prepareGroups(); setData(window.baseDS); } else { setLoadingOverlay(true); diff --git a/ui/webapp/src/layout/explore/index.tsx b/ui/webapp/src/layout/explore/index.tsx index 68043170..4e3865a7 100644 --- a/ui/webapp/src/layout/explore/index.tsx +++ b/ui/webapp/src/layout/explore/index.tsx @@ -114,6 +114,7 @@ const Explore = (props: Props) => { const [sortOptions, setSortOptions] = createSignal(Object.values(SortOption)); const [numItems, setNumItems] = createSignal<{ [key: string]: number }>({}); const [licenseOptions, setLicenseOptions] = createSignal([]); + const activeGroups = () => itemsDataGetter.getGroups(); const checkIfFullDataRequired = (): boolean => { if (viewMode() === ViewMode.Card) { @@ -279,8 +280,8 @@ const Explore = (props: Props) => { updatedSearchParams.delete(CLASSIFY_PARAM); updatedSearchParams.delete(SORT_BY_PARAM); updatedSearchParams.delete(SORT_DIRECTION_PARAM); - if (selectedGroup() === ALL_OPTION && !isUndefined(window.baseDS.groups)) { - const firstGroup = props.initialData.groups![0].normalized_name; + if (selectedGroup() === ALL_OPTION && !isUndefined(activeGroups())) { + const firstGroup = activeGroups()![0]; updatedSearchParams.set(GROUP_PARAM, firstGroup); setSelectedGroup(firstGroup); } @@ -610,7 +611,7 @@ const Explore = (props: Props) => {
- +
GROUP:
@@ -621,6 +622,8 @@ const Explore = (props: Props) => {
{(group: Group) => { + if (!isUndefined(activeGroups()) && !activeGroups()!.includes(group.normalized_name)) + return null; return (
+ +
+ + onChange(InputType.DisplayItemName, value, checked) + } + />
+ +
+ +
+ { + setItemNameSize(parseInt(e.currentTarget.value)); + }} + /> +
+
+
+
-
- - -
-
- - -
-
- -
- - {(t, index) => { - return ( -
- { - onUpdateSpacingType(e.currentTarget.value as SpacingType); - }} - /> - -
- ); + + +
+ +
- - - + + +
+
+ +
+ + {(t, index) => { + return ( +
+ { + onUpdateSpacingType(e.currentTarget.value as SpacingType); + }} + /> + +
+ ); + }} +
+
+ + + { + setItemsSpacing(parseInt(e.currentTarget.value)); + }} + /> + +
+
diff --git a/ui/webapp/src/types.ts b/ui/webapp/src/types.ts index c220d36a..45cc1ec6 100644 --- a/ui/webapp/src/types.ts +++ b/ui/webapp/src/types.ts @@ -85,6 +85,7 @@ export interface Featured { } export interface Item extends BaseItem { + website?: string; accepted_at?: string; homepage_url?: string; artwork_url?: string; From 4c239a3c86841adb1556034ae5dffffcaf34a155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cintia=20S=C3=A1nchez=20Garc=C3=ADa?= Date: Thu, 9 May 2024 14:30:15 +0200 Subject: [PATCH 51/55] Use additional categories on embed views (#623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cintia Sanchez Garcia Signed-off-by: Sergio Castaño Arteaga Co-authored-by: Cintia Sanchez Garcia Co-authored-by: Sergio Castaño Arteaga --- crates/core/src/datasets.rs | 67 +++++++++++++++---- ui/embed/src/App.tsx | 32 +++++++-- .../src/layout/navigation/EmbedModal.tsx | 3 +- ui/webapp/src/utils/itemsDataGetter.ts | 26 +++++++ 4 files changed, 110 insertions(+), 18 deletions(-) diff --git a/crates/core/src/datasets.rs b/crates/core/src/datasets.rs index db52b614..da9958e5 100644 --- a/crates/core/src/datasets.rs +++ b/crates/core/src/datasets.rs @@ -280,7 +280,12 @@ pub mod embed { items: landscape_data .items .iter() - .filter(|i| i.category == category.name) + .filter(|i| { + i.category == category.name + || i.additional_categories + .as_ref() + .map_or(false, |ac| ac.iter().any(|ac| ac.category == category.name)) + }) .map(Item::from) .collect(), }; @@ -299,7 +304,15 @@ pub mod embed { items: landscape_data .items .iter() - .filter(|i| i.category == category.name && i.subcategory == *subcategory.name) + .filter(|i| { + (i.category == category.name && i.subcategory == *subcategory.name) + || i.additional_categories.as_ref().map_or(false, |ac| { + ac.iter().any(|ac| { + ac.category == category.name + && ac.subcategory == *subcategory.name + }) + }) + }) .map(Item::from) .collect(), }; @@ -622,6 +635,10 @@ mod tests { #[test] fn embed_new() { let item = data::Item { + additional_categories: Some(vec![AdditionalCategory { + category: "Category 2".to_string(), + subcategory: "Subcategory 2".to_string(), + }]), category: "Category 1".to_string(), homepage_url: "https://homepage.url".to_string(), id: "id".to_string(), @@ -631,14 +648,24 @@ mod tests { ..Default::default() }; let landscape_data = LandscapeData { - categories: vec![data::Category { - name: "Category 1".to_string(), - normalized_name: "category-1".to_string(), - subcategories: vec![Subcategory { - name: "Subcategory 1".to_string(), - normalized_name: "subcategory-1".to_string(), - }], - }], + categories: vec![ + data::Category { + name: "Category 1".to_string(), + normalized_name: "category-1".to_string(), + subcategories: vec![Subcategory { + name: "Subcategory 1".to_string(), + normalized_name: "subcategory-1".to_string(), + }], + }, + data::Category { + name: "Category 2".to_string(), + normalized_name: "category-2".to_string(), + subcategories: vec![Subcategory { + name: "Subcategory 2".to_string(), + normalized_name: "subcategory-2".to_string(), + }], + }, + ], items: vec![item.clone()], }; let settings = LandscapeSettings { @@ -647,7 +674,7 @@ mod tests { }; let embed = Embed::new(&landscape_data, &settings); - let expected_embed_view = EmbedView { + let expected_embed_view_c1 = EmbedView { foundation: "Foundation".to_string(), category: data::Category { name: "Category 1".to_string(), @@ -659,10 +686,24 @@ mod tests { }, items: vec![(&item).into()], }; + let expected_embed_view_c2 = EmbedView { + foundation: "Foundation".to_string(), + category: data::Category { + name: "Category 2".to_string(), + normalized_name: "category-2".to_string(), + subcategories: vec![Subcategory { + name: "Subcategory 2".to_string(), + normalized_name: "subcategory-2".to_string(), + }], + }, + items: vec![(&item).into()], + }; let expected_embed = embed::Embed { views: vec![ - ("category-1".to_string(), expected_embed_view.clone()), - ("category-1--subcategory-1".to_string(), expected_embed_view), + ("category-1".to_string(), expected_embed_view_c1.clone()), + ("category-1--subcategory-1".to_string(), expected_embed_view_c1), + ("category-2".to_string(), expected_embed_view_c2.clone()), + ("category-2--subcategory-2".to_string(), expected_embed_view_c2), ] .into_iter() .collect(), diff --git a/ui/embed/src/App.tsx b/ui/embed/src/App.tsx index 5aa5d88a..d5ef70d2 100644 --- a/ui/embed/src/App.tsx +++ b/ui/embed/src/App.tsx @@ -139,6 +139,13 @@ const App = () => { const [itemsAlignment, setItemsAlignment] = createSignal(DEFAULT_ITEMS_ALIGNMENT); const [itemsSpacing, setItemsSpacing] = createSignal(); + // Sort items by name alphabetically + const sortItemsByName = (items: BaseItem[]): BaseItem[] => { + return items.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + }; + onMount(() => { const urlParams = new URLSearchParams(window.location.search); const basePathParam = urlParams.get(BASE_PATH_PARAM); @@ -253,6 +260,7 @@ const App = () => { throw new Error('Something went wrong'); }) .then((res) => { + console.log(res); setData(res); }) .catch(() => { @@ -297,7 +305,7 @@ const App = () => { when={displayHeader()} fallback={ { {(subcategory, index) => { - const items = data()!.items.filter((item: BaseItem) => { - return item.category === data()!.category.name && item.subcategory === subcategory.name; - }); + const items = sortItemsByName( + data()!.items.filter((item: BaseItem) => { + let inAdditionalCategory = false; + // Check if category/subcategory is in additional_categories + if (item.additional_categories) { + inAdditionalCategory = item.additional_categories.some((additionalCategory) => { + return ( + additionalCategory.category === data()!.category.name && + additionalCategory.subcategory === subcategory.name + ); + }); + } + + return ( + (item.category === data()!.category.name && item.subcategory === subcategory.name) || + inAdditionalCategory + ); + }) + ); return ( <> diff --git a/ui/webapp/src/layout/navigation/EmbedModal.tsx b/ui/webapp/src/layout/navigation/EmbedModal.tsx index 25ba04c1..54b43b20 100644 --- a/ui/webapp/src/layout/navigation/EmbedModal.tsx +++ b/ui/webapp/src/layout/navigation/EmbedModal.tsx @@ -45,6 +45,7 @@ import useBreakpointDetect from '../../hooks/useBreakpointDetect'; import { Category, Subcategory, SVGIconKind } from '../../types'; import capitalizeFirstLetter from '../../utils/capitalizeFirstLetter'; import isExploreSection from '../../utils/isExploreSection'; +import itemsDataGetter from '../../utils/itemsDataGetter'; import prepareLink from '../../utils/prepareLink'; import rgba2hex from '../../utils/rgba2hex'; import CheckBox from '../common/Checkbox'; @@ -96,7 +97,7 @@ const EmbedModal = () => { const isVisible = createMemo(() => isExploreSection(location.pathname)); const isEmbedSetupActive = () => location.pathname === EMBED_SETUP_PATH; const [visibleModal, setVisibleModal] = createSignal(isEmbedSetupActive()); - const categoriesList = () => sortBy(window.baseDS.categories, ['name']); + const categoriesList = () => sortBy(itemsDataGetter.getCategoriesAndSubcategoriesList(), ['name']); const [subcategoriesList, setSubcategoriesList] = createSignal( sortBy(categoriesList()[0].subcategories, ['name']) ); diff --git a/ui/webapp/src/utils/itemsDataGetter.ts b/ui/webapp/src/utils/itemsDataGetter.ts index 177c94aa..b6936730 100644 --- a/ui/webapp/src/utils/itemsDataGetter.ts +++ b/ui/webapp/src/utils/itemsDataGetter.ts @@ -163,6 +163,32 @@ export class ItemsDataGetter { return undefined; } + // Get categories and subcategories list with items + public getCategoriesAndSubcategoriesList(): Category[] { + const list: { [key: string]: string[] } = {}; + window.baseDS.items.forEach((i: Item) => { + if (list[i.category]) { + list[i.category].push(i.subcategory); + } else { + list[i.category] = [i.subcategory]; + } + }); + const categories: Category[] = []; + window.baseDS.categories.forEach((c: Category) => { + if (list[c.name]) { + const subcategories: Subcategory[] = []; + c.subcategories.forEach((s: Subcategory) => { + if (list[c.name].includes(s.name)) { + subcategories.push(s); + } + }); + categories.push({ ...c, subcategories: subcategories }); + } + }); + + return categories; + } + // Extend items with crunchbase and github data private extendItemsData(items?: Item[], crunchbaseData?: CrunchbaseData, githubData?: GithubData): Item[] { const itemsList: Item[] = []; From 0c30c4a1942f6aff0ad5659e281e12de4309bdca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cintia=20S=C3=A1nchez=20Garc=C3=ADa?= Date: Fri, 10 May 2024 09:35:22 +0200 Subject: [PATCH 52/55] Display other links in item's detail view (#624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Cintia Sanchez Garcia Signed-off-by: Sergio Castaño Arteaga Co-authored-by: Cintia Sanchez Garcia Co-authored-by: Sergio Castaño Arteaga --- crates/core/src/data.rs | 19 +++++ crates/core/src/data/legacy.rs | 73 ++++++++++++++++++- .../common/itemModal/Content.module.css | 7 ++ .../src/layout/common/itemModal/Content.tsx | 25 +++++++ .../common/itemModal/MobileContent.module.css | 11 +++ .../layout/common/itemModal/MobileContent.tsx | 23 ++++++ ui/webapp/src/types.ts | 6 ++ ui/webapp/src/utils/cutString.tsx | 11 +++ 8 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 ui/webapp/src/utils/cutString.tsx diff --git a/crates/core/src/data.rs b/crates/core/src/data.rs index d3887513..2ff4cfea 100644 --- a/crates/core/src/data.rs +++ b/crates/core/src/data.rs @@ -397,6 +397,7 @@ impl From for LandscapeData { item.latest_annual_review_url = extra.annual_review_url; item.linkedin_url = extra.linkedin_url; item.mailing_list_url = extra.mailing_list_url; + item.other_links = extra.other_links; item.package_manager_url = extra.package_manager_url; item.parent_project = extra.parent_project; item.slack_url = extra.slack_url; @@ -581,6 +582,9 @@ pub struct Item { #[serde(skip_serializing_if = "Option::is_none")] pub oss: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub other_links: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub package_manager_url: Option, @@ -752,6 +756,13 @@ pub struct ItemFeatured { pub order: Option, } +/// Landscape item link. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct ItemLink { + pub name: String, + pub url: String, +} + /// Landscape item summary. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct ItemSummary { @@ -1260,6 +1271,10 @@ mod tests { incubating: Some(date), linkedin_url: Some("linkedin_url".to_string()), mailing_list_url: Some("mailing_list_url".to_string()), + other_links: Some(vec![ItemLink { + name: "name".to_string(), + url: "https://link.url".to_string(), + }]), package_manager_url: Some("package_manager_url".to_string()), parent_project: Some("parent_project".to_string()), slack_url: Some("slack_url".to_string()), @@ -1344,6 +1359,10 @@ mod tests { latest_annual_review_url: Some("annual_review_url".to_string()), openssf_best_practices_url: Some("url_for_bestpractices".to_string()), oss: None, + other_links: Some(vec![ItemLink { + name: "name".to_string(), + url: "https://link.url".to_string(), + }]), package_manager_url: Some("package_manager_url".to_string()), parent_project: Some("parent_project".to_string()), repositories: Some(vec![ diff --git a/crates/core/src/data/legacy.rs b/crates/core/src/data/legacy.rs index 290fe8bc..87ae1c4b 100644 --- a/crates/core/src/data/legacy.rs +++ b/crates/core/src/data/legacy.rs @@ -1,7 +1,7 @@ //! This module defines some types used to parse the landscape data file in //! legacy format and convert it to the new one. -use super::ItemAudit; +use super::{ItemAudit, ItemLink}; use crate::util::validate_url; use anyhow::{bail, format_err, Context, Result}; use chrono::NaiveDate; @@ -74,6 +74,18 @@ impl LandscapeData { // Check some values in extra if let Some(extra) = &item.extra { + // Check other links + if let Some(other_links) = &extra.other_links { + for link in other_links { + if link.name.is_empty() { + return Err(format_err!("link name is required")).context(ctx); + } + if link.url.is_empty() { + return Err(format_err!("link url is required")).context(ctx); + } + } + } + // Check tag name if let Some(tag) = &extra.tag { if !TAG_NAME.is_match(tag) { @@ -151,6 +163,7 @@ pub(super) struct ItemExtra { pub incubating: Option, pub linkedin_url: Option, pub mailing_list_url: Option, + pub other_links: Option>, pub package_manager_url: Option, pub parent_project: Option, pub slack_url: Option, @@ -257,6 +270,10 @@ mod tests { ..Default::default() }]), blog_url: Some("https://blog.url".to_string()), + other_links: Some(vec![ItemLink { + name: "link".to_string(), + url: "https://link.url".to_string(), + }]), ..Default::default() }), ..Default::default() @@ -370,6 +387,60 @@ mod tests { landscape.validate().unwrap(); } + #[test] + #[should_panic(expected = "link name is required")] + fn landscape_data_validate_empty_link_name() { + let mut landscape = LandscapeData::default(); + landscape.landscape.push(Category { + name: "Category".to_string(), + subcategories: vec![SubCategory { + name: "Subcategory".to_string(), + items: vec![Item { + name: "Item".to_string(), + homepage_url: "https://example.com".to_string(), + logo: "logo".to_string(), + extra: Some(ItemExtra { + other_links: Some(vec![ItemLink { + name: String::new(), + url: "https://link.url".to_string(), + }]), + ..Default::default() + }), + ..Default::default() + }], + }], + }); + + landscape.validate().unwrap(); + } + + #[test] + #[should_panic(expected = "link url is required")] + fn landscape_data_validate_empty_link_url() { + let mut landscape = LandscapeData::default(); + landscape.landscape.push(Category { + name: "Category".to_string(), + subcategories: vec![SubCategory { + name: "Subcategory".to_string(), + items: vec![Item { + name: "Item".to_string(), + homepage_url: "https://example.com".to_string(), + logo: "logo".to_string(), + extra: Some(ItemExtra { + other_links: Some(vec![ItemLink { + name: "name".to_string(), + url: String::new(), + }]), + ..Default::default() + }), + ..Default::default() + }], + }], + }); + + landscape.validate().unwrap(); + } + #[test] #[should_panic(expected = "invalid tag")] fn landscape_data_validate_invalid_tag() { diff --git a/ui/webapp/src/layout/common/itemModal/Content.module.css b/ui/webapp/src/layout/common/itemModal/Content.module.css index ad617242..2eda9a46 100644 --- a/ui/webapp/src/layout/common/itemModal/Content.module.css +++ b/ui/webapp/src/layout/common/itemModal/Content.module.css @@ -41,6 +41,13 @@ font-size: 0.9rem; } +.otherLink { + font-size: 0.75rem; + color: var(--color4); + max-width: calc(50% - 2rem - 15px); + line-height: 24px; +} + .additionalInfo { font-size: 0.75rem; } diff --git a/ui/webapp/src/layout/common/itemModal/Content.tsx b/ui/webapp/src/layout/common/itemModal/Content.tsx index b2b2bd10..14654177 100644 --- a/ui/webapp/src/layout/common/itemModal/Content.tsx +++ b/ui/webapp/src/layout/common/itemModal/Content.tsx @@ -7,6 +7,7 @@ import { createEffect, createSignal, For, Match, on, Show, Switch } from 'solid- import { FOUNDATION } from '../../../data'; import { AdditionalCategory, Item, Repository, SecurityAudit, SVGIconKind } from '../../../types'; +import cutString from '../../../utils/cutString'; import formatProfitLabel from '../../../utils/formatLabelProfit'; import getItemDescription from '../../../utils/getItemDescription'; import { formatTAGName } from '../../../utils/prepareFilters'; @@ -309,6 +310,30 @@ const Content = (props: Props) => {
{/* Description */}
{description()}
+ + {/* Other links */} + +
+ + {(link, index) => { + return ( + <> + + {cutString(link.name, 30)} + + +
·
+
+ + ); + }} +
+
+
+ {/* Additional categories */}
Additional categories
diff --git a/ui/webapp/src/layout/common/itemModal/MobileContent.module.css b/ui/webapp/src/layout/common/itemModal/MobileContent.module.css index 6fb3d293..8a014e29 100644 --- a/ui/webapp/src/layout/common/itemModal/MobileContent.module.css +++ b/ui/webapp/src/layout/common/itemModal/MobileContent.module.css @@ -74,6 +74,17 @@ font-size: 0.9rem; } +.otherLink { + max-width: calc(100% - 2rem - 15px); + font-size: 0.65rem; + color: var(--color4); + line-height: 16px; +} + +.dot { + line-height: 1; +} + .section { font-size: 0.8rem; } diff --git a/ui/webapp/src/layout/common/itemModal/MobileContent.tsx b/ui/webapp/src/layout/common/itemModal/MobileContent.tsx index 6178cd7a..a6689b27 100644 --- a/ui/webapp/src/layout/common/itemModal/MobileContent.tsx +++ b/ui/webapp/src/layout/common/itemModal/MobileContent.tsx @@ -105,6 +105,29 @@ const MobileContent = (props: Props) => {
+ {/* Other links */} + +
+ + {(link, index) => { + return ( + <> + + {cutString(link.name, 30)} + + +
·
+
+ + ); + }} +
+
+
+ {/* Additional categories */}
Additional categories
diff --git a/ui/webapp/src/types.ts b/ui/webapp/src/types.ts index 45cc1ec6..e93a1d78 100644 --- a/ui/webapp/src/types.ts +++ b/ui/webapp/src/types.ts @@ -120,6 +120,12 @@ export interface Item extends BaseItem { linkedin_url?: string; audits?: SecurityAudit[]; parent_project?: string; + other_links?: OtherLink[]; +} + +export interface OtherLink { + name: string; + url: string; } export interface SecurityAudit { diff --git a/ui/webapp/src/utils/cutString.tsx b/ui/webapp/src/utils/cutString.tsx new file mode 100644 index 00000000..ccad78fa --- /dev/null +++ b/ui/webapp/src/utils/cutString.tsx @@ -0,0 +1,11 @@ +const MAX_LENGTH: number = 20; + +const cutString = (str: string, length: number = MAX_LENGTH): string => { + if (str.length <= length) { + return str; + } else { + return `${str.substring(0, length)}...`; + } +}; + +export default cutString; From 230df0793b1c759075a922cb2ec7914a83d7dafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Mon, 13 May 2024 08:37:22 +0200 Subject: [PATCH 53/55] Bump version to 0.9.0 (#625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- .github/workflows/release.yml | 8 +++---- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++ Cargo.lock | 6 ++--- Cargo.toml | 6 ++--- README.md | 4 ++-- ui/embed/package.json | 2 +- ui/webapp/package.json | 2 +- 7 files changed, 55 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72d4dfc0..2caf662e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.1/cargo-dist-installer.sh | sh" # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. @@ -166,7 +166,7 @@ jobs: submodules: recursive - name: Install cargo-dist shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.1/cargo-dist-installer.sh | sh" # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -211,7 +211,7 @@ jobs: with: submodules: recursive - name: Install cargo-dist - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.3/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.1/cargo-dist-installer.sh | sh" # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 @@ -251,7 +251,7 @@ jobs: repository: "cncf/homebrew-landscape2" token: ${{ secrets.HOMEBREW_TAP_TOKEN }} # So we have access to the formula - - name: Fetch local artifacts + - name: Fetch homebrew formulae uses: actions/download-artifact@v4 with: pattern: artifacts-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 85e80647..a49120db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.0] - 2024-05-13 + +### Added + +- Experimental overlay (#587) +- Improve search (#619) +- New embed card style (#622) +- Repository selector to item details view (#581) +- Not open source filter (#583) +- Repositories good first issues link (#582) +- Display parent project in item details view (#570, #568) +- Display loading spinner in controls bar (#579) +- Display package manager link in item details view (#609, 592) +- Display other links in item's detail view (#624) +- Organization region in items details view (#617) +- Make default view mode configurable (#574, #572) +- Make logos viewbox adjustments configurable (#577) +- Hide stats link when there are no stats available (#607) +- Open item details modal from zoom view (#620) +- Do not display empty groups in group selector (#606) +- Collect repositories topics from GitHub (#590) +- Some more options to logos preview (#594) +- Some tests to core crate (#613) + +### Fixed + +- Some issues in mobile devices (#612) +- Some issues with filters modal (#593) +- Badge url issue (#608) +- Participation stats legend in item details view (#616) +- Use additional categories on embed views (#623) +- Do not raise errors detecting mime type on deploy (#573) + +### Changed + +- Check logos checksum while deploying (#578) +- Use only orgs in data file when processing stats (#589) +- Use custom octorust client (#615) +- Upgrade CLI tool dependencies (#605) +- Upgrade web application and embed dependencies (#610) + ## [0.8.1] - 2024-04-11 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 1a95a900..92acd593 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1840,7 +1840,7 @@ dependencies = [ [[package]] name = "landscape2" -version = "0.8.1" +version = "0.9.0" dependencies = [ "anyhow", "askama", @@ -1891,7 +1891,7 @@ dependencies = [ [[package]] name = "landscape2-core" -version = "0.8.1" +version = "0.9.0" dependencies = [ "anyhow", "chrono", @@ -1912,7 +1912,7 @@ dependencies = [ [[package]] name = "landscape2-overlay" -version = "0.8.1" +version = "0.9.0" dependencies = [ "anyhow", "landscape2-core", diff --git a/Cargo.toml b/Cargo.toml index 5f1f1236..a1dbfc36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ ] [workspace.package] -version = "0.8.1" +version = "0.9.0" license = "Apache-2.0" edition = "2021" rust-version = "1.77" @@ -85,9 +85,9 @@ lto = "thin" # Config for 'cargo dist' [workspace.metadata.dist] # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.13.3" +cargo-dist-version = "0.14.1" # CI backends to support -ci = ["github"] +ci = "github" # The installers to generate for each app installers = ["shell", "powershell", "homebrew"] # A GitHub repo to push Homebrew formulas to diff --git a/README.md b/README.md index 5b1637af..553c29c2 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,13 @@ brew install cncf/landscape2/landscape2 #### Install via shell script ```text -curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cncf/landscape2/releases/download/v0.8.1/landscape2-installer.sh | sh +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/cncf/landscape2/releases/download/v0.9.0/landscape2-installer.sh | sh ``` #### Install via powershell script ```text -irm https://github.com/cncf/landscape2/releases/download/v0.8.1/landscape2-installer.ps1 | iex +irm https://github.com/cncf/landscape2/releases/download/v0.9.0/landscape2-installer.ps1 | iex ``` ### Container image diff --git a/ui/embed/package.json b/ui/embed/package.json index ad9d2f55..ea145aed 100644 --- a/ui/embed/package.json +++ b/ui/embed/package.json @@ -1,7 +1,7 @@ { "name": "embed", "private": true, - "version": "0.8.1", + "version": "0.9.0", "type": "module", "scripts": { "build": "tsc && vite build", diff --git a/ui/webapp/package.json b/ui/webapp/package.json index a5a47832..448ea247 100644 --- a/ui/webapp/package.json +++ b/ui/webapp/package.json @@ -1,7 +1,7 @@ { "name": "landscape", "private": true, - "version": "0.8.1", + "version": "0.9.0", "type": "module", "scripts": { "build": "tsc && vite build", From 5381876f06c45da300c548cd390aadf698998027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Mon, 13 May 2024 09:38:57 +0200 Subject: [PATCH 54/55] Update release workflow (#626) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- .github/workflows/release.yml | 3 +++ Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2caf662e..025f1470 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,6 +125,9 @@ jobs: - name: Install dependencies run: | ${{ matrix.packages_install }} + - name: Install wasm-pack + run: | + cargo install wasm-pack - name: Build artifacts run: | # Actually do builds and make zips and whatnot diff --git a/Cargo.toml b/Cargo.toml index a1dbfc36..39128c9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ installers = ["shell", "powershell", "homebrew"] # A GitHub repo to push Homebrew formulas to tap = "cncf/homebrew-landscape2" # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] # Publish jobs to run in CI publish-jobs = ["homebrew"] # Publish jobs to run in CI From 453484f372e26e23e196d53f017b7eadeb455299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Mon, 13 May 2024 09:57:33 +0200 Subject: [PATCH 55/55] Update cargo dist config (allow-dirty) (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 39128c9c..36fc7c08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,8 @@ lto = "thin" # Config for 'cargo dist' [workspace.metadata.dist] +# Allow customization of release workflow +allow-dirty = ["ci"] # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) cargo-dist-version = "0.14.1" # CI backends to support