From 7c7dbefe89f169546a7a4fd71b2218ee40bbd120 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sat, 14 Oct 2023 01:36:22 +0200 Subject: [PATCH] Split catalog explorer into multiple files --- web/src/core/usecases/catalogExplorer.ts | 356 ------------------ .../core/usecases/catalogExplorer/index.ts | 3 + .../usecases/catalogExplorer/selectors.ts | 116 ++++++ .../core/usecases/catalogExplorer/state.ts | 131 +++++++ .../core/usecases/catalogExplorer/thunks.ts | 102 +++++ 5 files changed, 352 insertions(+), 356 deletions(-) delete mode 100644 web/src/core/usecases/catalogExplorer.ts create mode 100644 web/src/core/usecases/catalogExplorer/index.ts create mode 100644 web/src/core/usecases/catalogExplorer/selectors.ts create mode 100644 web/src/core/usecases/catalogExplorer/state.ts create mode 100644 web/src/core/usecases/catalogExplorer/thunks.ts diff --git a/web/src/core/usecases/catalogExplorer.ts b/web/src/core/usecases/catalogExplorer.ts deleted file mode 100644 index ea3346b63..000000000 --- a/web/src/core/usecases/catalogExplorer.ts +++ /dev/null @@ -1,356 +0,0 @@ -import type { Thunks } from "../core"; -import type { PayloadAction } from "@reduxjs/toolkit"; -import { createSlice } from "@reduxjs/toolkit"; -import type { Catalog } from "../ports/OnyxiaApi"; -import { id } from "tsafe/id"; -import { assert } from "tsafe/assert"; -import type { State as RootState } from "../core"; -import { waitForDebounceFactory } from "core/tools/waitForDebounce"; -import { createUsecaseContextApi } from "redux-clean-architecture"; -import { exclude } from "tsafe/exclude"; -import { compareVersions } from "compare-versions"; -import { createSelector } from "@reduxjs/toolkit"; - -type State = State.NotFetched | State.Ready; - -namespace State { - export type NotFetched = { - stateDescription: "not fetched"; - isFetching: boolean; - }; - - export type Ready = { - stateDescription: "ready"; - "~internal": { - catalogs: Catalog[]; - selectedCatalogId: string; - }; - doShowOnlyHighlighted: boolean; - search: string; - }; -} - -export const name = "catalogExplorer"; - -export const { reducer, actions } = createSlice({ - name, - "initialState": id( - id({ - "stateDescription": "not fetched", - "isFetching": false - }) - ), - "reducers": { - "catalogsFetching": state => { - assert(state.stateDescription === "not fetched"); - state.isFetching = true; - }, - "catalogsFetched": ( - _state, - { - payload - }: PayloadAction<{ - selectedCatalogId: string; - catalogs: Catalog[]; - }> - ) => { - const { selectedCatalogId, catalogs } = payload; - const highlightedCharts = - catalogs?.find(catalog => catalog.id === selectedCatalogId) - ?.highlightedCharts || []; - - return id({ - "stateDescription": "ready", - "~internal": { catalogs, selectedCatalogId }, - "doShowOnlyHighlighted": - getAreConditionMetForOnlyShowingHighlightedPackaged({ - "highlightedChartsLength": highlightedCharts.length, - catalogs, - selectedCatalogId - }), - "search": "" - }); - }, - "changeSelectedCatalogue": ( - state, - { payload }: PayloadAction<{ selectedCatalogId: string }> - ) => { - const { selectedCatalogId } = payload; - - assert(state.stateDescription === "ready"); - - if (state["~internal"].selectedCatalogId === selectedCatalogId) { - return; - } - - state["~internal"].selectedCatalogId = selectedCatalogId; - const catalogs = state["~internal"].catalogs; - const highlightedCharts = - catalogs?.find(catalog => catalog.id === selectedCatalogId) - ?.highlightedCharts || []; - state.doShowOnlyHighlighted = - state.search === "" && - getAreConditionMetForOnlyShowingHighlightedPackaged({ - "highlightedChartsLength": highlightedCharts.length, - "catalogs": catalogs, - selectedCatalogId - }); - }, - "setSearch": (state, { payload }: PayloadAction<{ search: string }>) => { - const { search } = payload; - - assert(state.stateDescription === "ready"); - - state.search = search; - - const selectedCatalogId = state["~internal"].selectedCatalogId; - const catalogs = state["~internal"].catalogs; - const highlightedCharts = - catalogs?.find(catalog => catalog.id === selectedCatalogId) - ?.highlightedCharts || []; - - state.doShowOnlyHighlighted = - search === "" && - getAreConditionMetForOnlyShowingHighlightedPackaged({ - "highlightedChartsLength": highlightedCharts.length, - "catalogs": catalogs, - "selectedCatalogId": selectedCatalogId - }); - }, - "setDoShowOnlyHighlightedToFalse": state => { - assert(state.stateDescription === "ready"); - - state.doShowOnlyHighlighted = false; - } - } -}); - -export const thunks = { - "fetchCatalogs": - ( - params: - | { - isCatalogIdInUrl: true; - catalogId: string; - } - | { - isCatalogIdInUrl: false; - onAutoSelectCatalogId: (params: { - selectedCatalogId: string; - }) => void; - } - ) => - async (...args) => { - const [dispatch, , { onyxiaApi }] = args; - - dispatch(actions.catalogsFetching()); - - const { catalogs } = await (async () => { - let catalogs = await onyxiaApi.getCatalogs(); - - catalogs = JSON.parse(JSON.stringify(catalogs)); - - return { catalogs }; - })(); - - catalogs.forEach(catalog => - catalog.charts.forEach( - chart => - chart.versions.sort((v1, v2) => - compareVersions(v2.version, v1.version) - ) - // Descending Order - ) - ); - - const selectedCatalogId = params.isCatalogIdInUrl - ? params.catalogId - : filterProductionCatalogs(catalogs)[0].id; - - dispatch( - actions.catalogsFetched({ - catalogs, - selectedCatalogId - }) - ); - - if (!params.isCatalogIdInUrl) { - params.onAutoSelectCatalogId({ selectedCatalogId }); - } - }, - "setSearch": - (params: { search: string }) => - async (...args) => { - const { search } = params; - const [dispatch, getState, extra] = args; - - const { waitForSearchDebounce } = getContext(extra); - - await waitForSearchDebounce(); - - if (getState().catalogExplorer.stateDescription !== "ready") { - return; - } - - dispatch(actions.setSearch({ search })); - }, - "revealAllPackages": - () => - (...args) => { - const [dispatch] = args; - dispatch(actions.setDoShowOnlyHighlightedToFalse()); - }, - "changeSelectedCatalogId": - (params: { catalogId: string }) => - (...args) => { - const { catalogId } = params; - const [dispatch, getState] = args; - - if (getState().catalogExplorer.stateDescription !== "ready") { - return; - } - - dispatch(actions.changeSelectedCatalogue({ "selectedCatalogId": catalogId })); - } -} satisfies Thunks; - -const { getContext } = createUsecaseContextApi(() => { - const { waitForDebounce } = waitForDebounceFactory({ "delay": 500 }); - return { - "waitForSearchDebounce": waitForDebounce - }; -}); - -export const selectors = (() => { - function getPackageWeightFactory(params: { highlightedCharts: string[] }) { - const { highlightedCharts } = params; - - function getPackageWeight(packageName: string) { - const indexHiglightedCharts = highlightedCharts.findIndex( - v => v.toLowerCase() === packageName.toLowerCase() - ); - return indexHiglightedCharts !== -1 - ? highlightedCharts.length - indexHiglightedCharts - : 0; - } - - return { getPackageWeight }; - } - - const readyState = (rootState: RootState) => { - const state = rootState[name]; - - if (state.stateDescription !== "ready") { - return undefined; - } - - return state; - }; - - const filteredPackages = createSelector(readyState, state => { - if (state === undefined) { - return undefined; - } - - const { - doShowOnlyHighlighted, - search, - "~internal": { selectedCatalogId } - } = state; - - const catalogs = state["~internal"].catalogs; - const highlightedCharts = - catalogs?.find(catalog => catalog.id === selectedCatalogId) - ?.highlightedCharts || []; - const { getPackageWeight } = getPackageWeightFactory({ highlightedCharts }); - const catalog = catalogs - .filter( - ({ id, status }) => - id === selectedCatalogId || (state.search !== "" && status === "PROD") - ) - .map(catalog => - catalog.charts.map(chart => ({ - "packageDescription": chart.versions[0].description, - "packageHomeUrl": chart.versions[0].home, - "packageName": chart.name, - "packageIconUrl": chart.versions[0].icon, - "catalogId": catalog.id - })) - ) - .reduce((accumulator, packages) => accumulator.concat(packages), []) - .sort( - (a, b) => - getPackageWeight(b.packageName) - getPackageWeight(a.packageName) - ); - - const packages = catalog - .slice( - 0, - doShowOnlyHighlighted && search === "" - ? highlightedCharts.length - : undefined - ) - .filter(({ packageName, packageDescription }) => - [packageName, packageDescription] - .map(str => str.toLowerCase().includes(search.toLowerCase())) - .includes(true) - ); - - return { - packages, - "notShownCount": search !== "" ? 0 : catalog.length - packages.length - }; - }); - - const selectedCatalog = createSelector(readyState, state => { - if (state === undefined) { - return undefined; - } - - const { selectedCatalogId } = state["~internal"]; - - const catalog = state["~internal"].catalogs.find( - ({ id }) => id === selectedCatalogId - ); - - assert(catalog !== undefined); - - const { charts: _, ...rest } = catalog; - - return rest; - }); - - const productionCatalogs = createSelector(readyState, state => { - if (state === undefined) { - return undefined; - } - - return filterProductionCatalogs(state["~internal"].catalogs); - }); - - return { filteredPackages, selectedCatalog, productionCatalogs }; -})(); - -function getAreConditionMetForOnlyShowingHighlightedPackaged(params: { - highlightedChartsLength: number; - catalogs: Catalog[]; - selectedCatalogId: string; -}) { - const { highlightedChartsLength, catalogs, selectedCatalogId } = params; - - const totalPackageCount = catalogs.find(({ id }) => id === selectedCatalogId)!.charts - .length; - - return highlightedChartsLength !== 0 && totalPackageCount > 5; -} - -function filterProductionCatalogs( - catalogs: Catalog[] -): (Omit & { status: "PROD" })[] { - return catalogs - .map(({ status, ...rest }) => - status === "PROD" ? { ...rest, status } : undefined - ) - .filter(exclude(undefined)) - .map(({ charts: _, ...rest }) => rest); -} diff --git a/web/src/core/usecases/catalogExplorer/index.ts b/web/src/core/usecases/catalogExplorer/index.ts new file mode 100644 index 000000000..3f3843384 --- /dev/null +++ b/web/src/core/usecases/catalogExplorer/index.ts @@ -0,0 +1,3 @@ +export * from "./state"; +export * from "./selectors"; +export * from "./thunks"; diff --git a/web/src/core/usecases/catalogExplorer/selectors.ts b/web/src/core/usecases/catalogExplorer/selectors.ts new file mode 100644 index 000000000..177772118 --- /dev/null +++ b/web/src/core/usecases/catalogExplorer/selectors.ts @@ -0,0 +1,116 @@ +import type { Catalog } from "core/ports/OnyxiaApi"; +import { assert } from "tsafe/assert"; +import type { State as RootState } from "core/core"; +import { exclude } from "tsafe/exclude"; +import { createSelector } from "@reduxjs/toolkit"; +import { name } from "./state"; + +function getPackageWeightFactory(params: { highlightedCharts: string[] }) { + const { highlightedCharts } = params; + + function getPackageWeight(packageName: string) { + const indexHiglightedCharts = highlightedCharts.findIndex( + v => v.toLowerCase() === packageName.toLowerCase() + ); + return indexHiglightedCharts !== -1 + ? highlightedCharts.length - indexHiglightedCharts + : 0; + } + + return { getPackageWeight }; +} + +const readyState = (rootState: RootState) => { + const state = rootState[name]; + + if (state.stateDescription !== "ready") { + return undefined; + } + + return state; +}; + +const filteredPackages = createSelector(readyState, state => { + if (state === undefined) { + return undefined; + } + + const { doShowOnlyHighlighted, search, selectedCatalogId } = state; + + const { catalogs } = state; + const highlightedCharts = + catalogs?.find(catalog => catalog.id === selectedCatalogId)?.highlightedCharts || + []; + const { getPackageWeight } = getPackageWeightFactory({ highlightedCharts }); + const catalog = catalogs + .filter( + ({ id, status }) => + id === selectedCatalogId || (state.search !== "" && status === "PROD") + ) + .map(catalog => + catalog.charts.map(chart => ({ + "packageDescription": chart.versions[0].description, + "packageHomeUrl": chart.versions[0].home, + "packageName": chart.name, + "packageIconUrl": chart.versions[0].icon, + "catalogId": catalog.id + })) + ) + .reduce((accumulator, packages) => accumulator.concat(packages), []) + .sort( + (a, b) => getPackageWeight(b.packageName) - getPackageWeight(a.packageName) + ); + + const packages = catalog + .slice( + 0, + doShowOnlyHighlighted && search === "" ? highlightedCharts.length : undefined + ) + .filter(({ packageName, packageDescription }) => + [packageName, packageDescription] + .map(str => str.toLowerCase().includes(search.toLowerCase())) + .includes(true) + ); + + return { + packages, + "notShownCount": search !== "" ? 0 : catalog.length - packages.length + }; +}); + +const selectedCatalog = createSelector(readyState, state => { + if (state === undefined) { + return undefined; + } + + const { selectedCatalogId } = state; + + const catalog = state.catalogs.find(({ id }) => id === selectedCatalogId); + + assert(catalog !== undefined); + + const { charts: _, ...rest } = catalog; + + return rest; +}); + +const productionCatalogs = createSelector(readyState, state => { + if (state === undefined) { + return undefined; + } + + return filterProductionCatalogs(state.catalogs); +}); + +export const selectors = { filteredPackages, selectedCatalog, productionCatalogs }; + +export function filterProductionCatalogs( + catalogs: Catalog[] +): (Omit & { status: "PROD" })[] { + return catalogs + .map(({ status, ...rest }) => + status === "PROD" ? { ...rest, status } : undefined + ) + .filter(exclude(undefined)) + .map(({ charts: _, ...rest }) => rest); +} diff --git a/web/src/core/usecases/catalogExplorer/state.ts b/web/src/core/usecases/catalogExplorer/state.ts new file mode 100644 index 000000000..afa40ddde --- /dev/null +++ b/web/src/core/usecases/catalogExplorer/state.ts @@ -0,0 +1,131 @@ +import type { PayloadAction } from "@reduxjs/toolkit"; +import { createSlice } from "@reduxjs/toolkit"; +import type { Catalog } from "core/ports/OnyxiaApi"; +import { id } from "tsafe/id"; +import { assert } from "tsafe/assert"; + +export type State = State.NotFetched | State.Ready; + +export namespace State { + export type NotFetched = { + stateDescription: "not fetched"; + isFetching: boolean; + }; + + export type Ready = { + stateDescription: "ready"; + catalogs: Catalog[]; + selectedCatalogId: string; + doShowOnlyHighlighted: boolean; + search: string; + }; +} + +export const name = "catalogExplorer"; + +export const { reducer, actions } = createSlice({ + name, + "initialState": id( + id({ + "stateDescription": "not fetched", + "isFetching": false + }) + ), + "reducers": { + "catalogsFetching": state => { + assert(state.stateDescription === "not fetched"); + state.isFetching = true; + }, + "catalogsFetched": ( + _state, + { + payload + }: PayloadAction<{ + selectedCatalogId: string; + catalogs: Catalog[]; + }> + ) => { + const { selectedCatalogId, catalogs } = payload; + const highlightedCharts = + catalogs.find(catalog => catalog.id === selectedCatalogId) + ?.highlightedCharts || []; + + return id({ + "stateDescription": "ready", + catalogs, + selectedCatalogId, + "doShowOnlyHighlighted": + getAreConditionMetForOnlyShowingHighlightedPackaged({ + "highlightedChartsLength": highlightedCharts.length, + catalogs, + selectedCatalogId + }), + "search": "" + }); + }, + "changeSelectedCatalogue": ( + state, + { payload }: PayloadAction<{ selectedCatalogId: string }> + ) => { + const { selectedCatalogId } = payload; + + assert(state.stateDescription === "ready"); + + if (state.selectedCatalogId === selectedCatalogId) { + return; + } + + state.selectedCatalogId = selectedCatalogId; + const catalogs = state.catalogs; + const highlightedCharts = + catalogs.find(catalog => catalog.id === selectedCatalogId) + ?.highlightedCharts || []; + state.doShowOnlyHighlighted = + state.search === "" && + getAreConditionMetForOnlyShowingHighlightedPackaged({ + "highlightedChartsLength": highlightedCharts.length, + "catalogs": catalogs, + selectedCatalogId + }); + }, + "setSearch": (state, { payload }: PayloadAction<{ search: string }>) => { + const { search } = payload; + + assert(state.stateDescription === "ready"); + + state.search = search; + + const selectedCatalogId = state.selectedCatalogId; + const catalogs = state.catalogs; + const highlightedCharts = + catalogs?.find(catalog => catalog.id === selectedCatalogId) + ?.highlightedCharts || []; + + state.doShowOnlyHighlighted = + search === "" && + getAreConditionMetForOnlyShowingHighlightedPackaged({ + "highlightedChartsLength": highlightedCharts.length, + "catalogs": catalogs, + "selectedCatalogId": selectedCatalogId + }); + }, + "setDoShowOnlyHighlightedToFalse": state => { + assert(state.stateDescription === "ready"); + + state.doShowOnlyHighlighted = false; + } + } +}); + +export function getAreConditionMetForOnlyShowingHighlightedPackaged(params: { + highlightedChartsLength: number; + catalogs: Catalog[]; + selectedCatalogId: string; +}) { + const { highlightedChartsLength, catalogs, selectedCatalogId } = params; + + const totalPackageCount = catalogs.find(({ id }) => id === selectedCatalogId)!.charts + .length; + + return highlightedChartsLength !== 0 && totalPackageCount > 5; +} diff --git a/web/src/core/usecases/catalogExplorer/thunks.ts b/web/src/core/usecases/catalogExplorer/thunks.ts new file mode 100644 index 000000000..44b9eeddc --- /dev/null +++ b/web/src/core/usecases/catalogExplorer/thunks.ts @@ -0,0 +1,102 @@ +import type { Thunks } from "core/core"; +import { waitForDebounceFactory } from "core/tools/waitForDebounce"; +import { createUsecaseContextApi } from "redux-clean-architecture"; +import { compareVersions } from "compare-versions"; +import { actions, name } from "./state"; +import { filterProductionCatalogs } from "./selectors"; + +export const thunks = { + "fetchCatalogs": + ( + params: + | { + isCatalogIdInUrl: true; + catalogId: string; + } + | { + isCatalogIdInUrl: false; + onAutoSelectCatalogId: (params: { + selectedCatalogId: string; + }) => void; + } + ) => + async (...args) => { + const [dispatch, , { onyxiaApi }] = args; + + dispatch(actions.catalogsFetching()); + + const { catalogs } = await (async () => { + let catalogs = await onyxiaApi.getCatalogs(); + + catalogs = JSON.parse(JSON.stringify(catalogs)); + + return { catalogs }; + })(); + + catalogs.forEach(catalog => + catalog.charts.forEach( + chart => + chart.versions.sort((v1, v2) => + compareVersions(v2.version, v1.version) + ) + // Descending Order + ) + ); + + const selectedCatalogId = params.isCatalogIdInUrl + ? params.catalogId + : filterProductionCatalogs(catalogs)[0].id; + + dispatch( + actions.catalogsFetched({ + catalogs, + selectedCatalogId + }) + ); + + if (!params.isCatalogIdInUrl) { + params.onAutoSelectCatalogId({ selectedCatalogId }); + } + }, + "setSearch": + (params: { search: string }) => + async (...args) => { + const { search } = params; + const [dispatch, getState, extra] = args; + + const { waitForSearchDebounce } = getContext(extra); + + await waitForSearchDebounce(); + + if (getState()[name].stateDescription !== "ready") { + return; + } + + dispatch(actions.setSearch({ search })); + }, + "revealAllPackages": + () => + (...args) => { + const [dispatch] = args; + dispatch(actions.setDoShowOnlyHighlightedToFalse()); + }, + "changeSelectedCatalogId": + (params: { catalogId: string }) => + (...args) => { + const { catalogId } = params; + const [dispatch, getState] = args; + + if (getState().catalogExplorer.stateDescription !== "ready") { + return; + } + + dispatch(actions.changeSelectedCatalogue({ "selectedCatalogId": catalogId })); + } +} satisfies Thunks; + +const { getContext } = createUsecaseContextApi(() => { + const { waitForDebounce } = waitForDebounceFactory({ "delay": 500 }); + return { + "waitForSearchDebounce": waitForDebounce + }; +});