You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kie.apache.org by po...@apache.org on 2023/09/21 14:30:12 UTC
[incubator-kie-tools-temporary-rnd-do-not-use] 20/27: KOGITO-8153: Create an extensible Samples page (now Catalog) (#1635)
This is an automated email from the ASF dual-hosted git repository.
porcelli pushed a commit to branch KOGITO-8015-feature-preview
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools-temporary-rnd-do-not-use.git
commit 15fcd99b0b8b64c72c3b1f1ba3eac9d0a3d6dbb3
Author: Fabrizio Antonangeli <fa...@gmail.com>
AuthorDate: Tue May 16 13:33:43 2023 +0200
KOGITO-8153: Create an extensible Samples page (now Catalog) (#1635)
Co-authored-by: Guilherme Caponetto <63...@users.noreply.github.com>
---
.../src/home/sample/SampleCard.tsx | 192 ++++++++++++-------
.../src/home/sample/SampleCardSkeleton.css | 9 -
.../src/home/sample/Showcase.tsx | 209 +++++++++++++--------
.../src/home/sample/hooks/SampleContext.tsx | 148 ++++++++++++++-
.../src/home/sample/sampleApi.ts | 118 +++++++-----
5 files changed, 461 insertions(+), 215 deletions(-)
diff --git a/packages/serverless-logic-web-tools/src/home/sample/SampleCard.tsx b/packages/serverless-logic-web-tools/src/home/sample/SampleCard.tsx
index 31475546ff..aa81206b59 100644
--- a/packages/serverless-logic-web-tools/src/home/sample/SampleCard.tsx
+++ b/packages/serverless-logic-web-tools/src/home/sample/SampleCard.tsx
@@ -14,19 +14,19 @@
* limitations under the License.
*/
-import * as React from "react";
-import { useMemo, useRef, useEffect } from "react";
-import { Card, CardTitle, CardFooter, CardBody } from "@patternfly/react-core/dist/js/components/Card";
-import { Grid, GridItem } from "@patternfly/react-core/dist/js/layouts/Grid";
-import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button";
-import { useRoutes } from "../../navigation/Hooks";
-import { Link } from "react-router-dom";
-import { Text } from "@patternfly/react-core/dist/js/components/Text";
+import { Button, Modal, ModalVariant, Skeleton } from "@patternfly/react-core/dist/js";
+import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/js/components/Card";
import { Label, LabelProps } from "@patternfly/react-core/dist/js/components/Label";
-import { FolderIcon, FileIcon, MonitoringIcon } from "@patternfly/react-icons/dist/js/icons";
-import { Sample, SampleCategory } from "./sampleApi";
+import { Text } from "@patternfly/react-core/dist/js/components/Text";
import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip";
import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye";
+import { Grid, GridItem } from "@patternfly/react-core/dist/js/layouts/Grid";
+import { FileIcon, FolderIcon, MonitoringIcon, SearchPlusIcon } from "@patternfly/react-icons/dist/js/icons";
+import * as React from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { useHistory } from "react-router-dom";
+import { useRoutes } from "../../navigation/Hooks";
+import { Sample, SampleCategory } from "./sampleApi";
const tagMap: Record<SampleCategory, { label: string; icon: React.ComponentClass; color: LabelProps["color"] }> = {
["serverless-workflow"]: {
@@ -46,72 +46,120 @@ const tagMap: Record<SampleCategory, { label: string; icon: React.ComponentClass
},
};
-export function SampleCard(props: { sample: Sample }) {
- const routes = useRoutes();
- const imgRef = useRef<HTMLImageElement>(null);
- const tag = useMemo(() => tagMap[props.sample.definition.category], [props.sample.definition.category]);
+function SampleSvgImg(props: {
+ sample: Sample;
+ svgBlob: string | undefined;
+ height?: string;
+ width?: string;
+ maxWidth?: string;
+ maxHeight?: string;
+}) {
+ const [svgUrl, setSvgUrl] = useState("");
+ const { height, width, maxWidth = "100%", maxHeight } = props;
useEffect(() => {
- const blob = new Blob([props.sample.svgContent], { type: "image/svg+xml" });
+ const blob = new Blob([props.svgBlob || ""], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
- imgRef.current!.addEventListener("load", () => URL.revokeObjectURL(url), { once: true });
- imgRef.current!.src = url;
- }, [props.sample.svgContent]);
+ setSvgUrl(url);
+ return () => {
+ URL.revokeObjectURL(url);
+ };
+ }, [props.svgBlob]);
+
+ if (!svgUrl || !props.svgBlob) {
+ return <Skeleton height={height} width="100%" style={{ maxHeight: "80%" }} />;
+ }
+
+ return (
+ <img style={{ height, width, maxWidth, maxHeight }} src={svgUrl} alt={`SVG for sample ${props.sample.sampleId}`} />
+ );
+}
+
+export function SampleCard(props: { sample: Sample; cover: string | undefined }) {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const routes = useRoutes();
+ const tag = useMemo(() => tagMap[props.sample.definition.category], [props.sample.definition.category]);
+ const history = useHistory();
+
+ const onCardClick = useCallback(() => {
+ history.push({
+ pathname: routes.sampleShowcase.path({}),
+ search: routes.sampleShowcase.queryString({ sampleId: props.sample.sampleId }),
+ });
+ }, [props.sample, history, routes]);
+
+ const handleModalToggle = useCallback((e?) => {
+ e?.stopPropagation();
+ setIsModalOpen((prevState) => !prevState);
+ }, []);
return (
- <Card isCompact={true} isFullHeight={true}>
- <Grid style={{ height: "100%" }}>
- <GridItem
- lg={6}
- style={{ overflow: "hidden", textAlign: "center", verticalAlign: "middle", position: "relative" }}
- >
- <div style={{ position: "absolute", bottom: "16px", right: 0, left: 0, margin: "auto" }}>
- <Label color={tag.color}>
- <tag.icon />
- <b>{tag.label}</b>
- </Label>
- </div>
- <Bullseye style={{ padding: "0px 8px 30px 8px" }}>
- <img
- style={{ height: "370px", maxWidth: "100%" }}
- ref={imgRef}
- alt={`SVG for sample ${props.sample.sampleId}`}
- />
- </Bullseye>
- </GridItem>
- <GridItem lg={6} style={{ display: "flex", flexDirection: "column" }}>
- <CardTitle data-ouia-component-type="sample-title">{props.sample.definition.title}</CardTitle>
- <CardBody isFilled={true}>
- <Tooltip content={<div>{props.sample.definition.description}</div>}>
- <Text
- component="p"
- style={{
- display: "-webkit-box",
- WebkitBoxOrient: "vertical",
- WebkitLineClamp: 5,
- overflow: "hidden",
- textOverflow: "ellipsis",
- whiteSpace: "pre-wrap",
- }}
- >
- {props.sample.definition.description}
- </Text>
- </Tooltip>
- </CardBody>
- <CardFooter style={{ alignItems: "baseline" }}>
- <Link
- to={{
- pathname: routes.sampleShowcase.path({}),
- search: routes.sampleShowcase.queryString({ sampleId: props.sample.sampleId }),
- }}
- >
- <Button variant={ButtonVariant.tertiary} ouiaId={props.sample.sampleId + `-try-swf-sample-button`}>
- Try it out!
- </Button>
- </Link>
- </CardFooter>
- </GridItem>
- </Grid>
- </Card>
+ <>
+ <Card isCompact={true} isFullHeight={true} onClick={onCardClick} isSelectable>
+ <Grid style={{ height: "100%" }}>
+ <GridItem
+ lg={6}
+ style={{ overflow: "hidden", textAlign: "center", verticalAlign: "middle", position: "relative" }}
+ >
+ <div style={{ position: "absolute", bottom: "16px", right: 0, left: 0, margin: "auto" }}>
+ {props.cover && (
+ <Button
+ type="button"
+ onClick={handleModalToggle}
+ isLarge
+ variant="plain"
+ style={
+ {
+ "--pf-c-button--PaddingLeft": "0",
+ "--pf-c-button--PaddingRight": "0",
+ marginTop: "-16px",
+ marginLeft: "80%",
+ } as React.CSSProperties
+ }
+ >
+ <SearchPlusIcon size="sm" />
+ </Button>
+ )}
+ <Label color={tag.color}>
+ <tag.icon />
+ <b>{tag.label}</b>
+ </Label>
+ </div>
+ <Bullseye style={{ padding: "0px 8px 30px 8px" }}>
+ <SampleSvgImg sample={props.sample} svgBlob={props.cover} height="370px" />
+ </Bullseye>
+ </GridItem>
+ <GridItem lg={6} style={{ display: "flex", flexDirection: "column" }}>
+ <CardTitle data-ouia-component-type="sample-title">{props.sample.definition.title}</CardTitle>
+ <CardBody isFilled={true}>
+ <Tooltip content={<div>{props.sample.definition.description}</div>}>
+ <Text
+ component="p"
+ style={{
+ display: "-webkit-box",
+ WebkitBoxOrient: "vertical",
+ WebkitLineClamp: 5,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "pre-wrap",
+ }}
+ >
+ {props.sample.definition.description}
+ </Text>
+ </Tooltip>
+ </CardBody>
+ </GridItem>
+ </Grid>
+ </Card>
+ <Modal
+ title={props.sample.definition.title}
+ variant={ModalVariant.large}
+ isOpen={isModalOpen}
+ onClose={handleModalToggle}
+ style={{ textAlign: "center" }}
+ >
+ <SampleSvgImg sample={props.sample} svgBlob={props.cover} height="670px" />
+ </Modal>
+ </>
);
}
diff --git a/packages/serverless-logic-web-tools/src/home/sample/SampleCardSkeleton.css b/packages/serverless-logic-web-tools/src/home/sample/SampleCardSkeleton.css
index cc7433efff..3c9c1fa459 100644
--- a/packages/serverless-logic-web-tools/src/home/sample/SampleCardSkeleton.css
+++ b/packages/serverless-logic-web-tools/src/home/sample/SampleCardSkeleton.css
@@ -14,15 +14,6 @@
* limitations under the License.
*/
-.sample-card-skeleton--gallery {
- overflow-x: auto;
- grid-auto-flow: column;
- grid-auto-columns: minmax(calc(100% / 3.1 - 16px), 1fr);
- padding-bottom: 8px;
- padding-right: var(--pf-c-page__main-section--xl--PaddingRight);
- height: 430px;
-}
-
.sample-card-skeleton--grid {
height: 100%;
}
diff --git a/packages/serverless-logic-web-tools/src/home/sample/Showcase.tsx b/packages/serverless-logic-web-tools/src/home/sample/Showcase.tsx
index e1839b9c36..52303c80b2 100644
--- a/packages/serverless-logic-web-tools/src/home/sample/Showcase.tsx
+++ b/packages/serverless-logic-web-tools/src/home/sample/Showcase.tsx
@@ -14,28 +14,29 @@
* limitations under the License.
*/
-import * as React from "react";
-import { useEffect, useState, useCallback, useMemo } from "react";
-import { TextContent, Text } from "@patternfly/react-core/dist/js/components/Text";
-import { SampleCard } from "./SampleCard";
-import { Gallery } from "@patternfly/react-core/dist/js/layouts/Gallery";
-import { Sample, SampleCategory } from "./sampleApi";
-import { SampleCardSkeleton } from "./SampleCardSkeleton";
-import { SamplesLoadError } from "./SamplesLoadError";
-import { useSampleDispatch } from "./hooks/SampleContext";
-import { SearchInput } from "@patternfly/react-core/dist/js/components/SearchInput";
-import { PageSection } from "@patternfly/react-core/dist/js/components/Page";
-import { EmptyState, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState";
-import { Title } from "@patternfly/react-core/dist/js/components/Title";
-import { CubesIcon } from "@patternfly/react-icons/dist/js/icons/cubes-icon";
+import { Pagination, PaginationVariant, PerPageOptions, Skeleton } from "@patternfly/react-core/dist/js";
import {
Dropdown,
DropdownItem,
DropdownSeparator,
DropdownToggle,
} from "@patternfly/react-core/dist/js/components/Dropdown";
+import { EmptyState, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState";
+import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page";
+import { SearchInput } from "@patternfly/react-core/dist/js/components/SearchInput";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text";
+import { Title } from "@patternfly/react-core/dist/js/components/Title";
+import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/js/components/Toolbar";
+import { Gallery } from "@patternfly/react-core/dist/js/layouts/Gallery";
+import { CubesIcon } from "@patternfly/react-icons/dist/js/icons/cubes-icon";
+import * as React from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { FileLabel } from "../../workspace/components/FileLabel";
-import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex";
+import { useSampleDispatch } from "./hooks/SampleContext";
+import { Sample, SampleCategory, SampleCoversHashtable } from "./sampleApi";
+import { SampleCard } from "./SampleCard";
+import { SampleCardSkeleton } from "./SampleCardSkeleton";
+import { SamplesLoadError } from "./SamplesLoadError";
const SAMPLE_PRIORITY: Record<SampleCategory, number> = {
["serverless-workflow"]: 1,
@@ -50,18 +51,49 @@ const LABEL_MAP: Record<SampleCategory, JSX.Element> = {
};
const ALL_CATEGORIES_LABEL = "All categories";
-
+const CARDS_PER_PAGE = 9;
const CATEGORY_ARRAY = Object.keys(SAMPLE_PRIORITY) as SampleCategory[];
+export const SAMPLE_CARDS_PER_PAGE_OPTIONS: PerPageOptions[] = [
+ {
+ title: `${CARDS_PER_PAGE}`,
+ value: CARDS_PER_PAGE,
+ },
+];
+
export function Showcase() {
const sampleDispatch = useSampleDispatch();
const [loading, setLoading] = useState<boolean>(true);
const [samples, setSamples] = useState<Sample[]>([]);
+ const [sampleCovers, setSampleCovers] = useState<SampleCoversHashtable>({});
const [sampleLoadingError, setSampleLoadingError] = useState("");
const [searchFilter, setSearchFilter] = useState("");
const [categoryFilter, setCategoryFilter] = useState<SampleCategory | undefined>();
+ const [page, setPage] = React.useState(1);
const [isCategoryFilterDropdownOpen, setCategoryFilterDropdownOpen] = useState(false);
+ const visibleSamples = useMemo(
+ () => samples.slice((page - 1) * CARDS_PER_PAGE, page * CARDS_PER_PAGE),
+ [samples, page]
+ );
+
+ const samplesCount = useMemo(() => samples.length, [samples]);
+
+ const filterResultMessage = useMemo(() => {
+ if (samplesCount === 0) {
+ return;
+ }
+ const isPlural = samplesCount > 1;
+ return `Showing ${samplesCount} sample${isPlural ? "s" : ""}`;
+ }, [samplesCount]);
+
+ const selectedCategory = useMemo(() => {
+ if (categoryFilter) {
+ return LABEL_MAP[categoryFilter];
+ }
+ return ALL_CATEGORIES_LABEL;
+ }, [categoryFilter]);
+
const onSearch = useCallback(
async (args: { searchValue: string; category?: SampleCategory }) => {
if (args.searchValue === searchFilter && args.category === categoryFilter) {
@@ -91,20 +123,10 @@ export function Showcase() {
});
}, [sampleDispatch]);
- const filterResultMessage = useMemo(() => {
- if (samples.length === 0) {
- return;
- }
- const isPlural = samples.length > 1;
- return `Showing ${samples.length} sample${isPlural ? "s" : ""}`;
- }, [samples.length]);
-
- const selectedCategory = useMemo(() => {
- if (categoryFilter) {
- return LABEL_MAP[categoryFilter];
- }
- return ALL_CATEGORIES_LABEL;
- }, [categoryFilter]);
+ useEffect(() => {
+ sampleDispatch.getSampleCovers({ samples: visibleSamples, prevState: sampleCovers }).then(setSampleCovers);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [visibleSamples, sampleDispatch]);
const categoryFilterDropdownItems = useMemo(
() => [
@@ -127,17 +149,20 @@ export function Showcase() {
[onSearch, searchFilter]
);
+ const onSetPage = useCallback((_e, v) => {
+ setPage(v);
+ }, []);
+
return (
- <>
- {sampleLoadingError && <SamplesLoadError errors={[sampleLoadingError]} />}
- {!sampleLoadingError && (
- <>
- <TextContent>
- <Text component="h1">Samples Showcase</Text>
- </TextContent>
- <br />
- <Flex flexWrap={{ default: "wrap" }}>
- <FlexItem style={{ marginRight: 0 }}>
+ <Page>
+ <PageSection variant={"light"}>
+ <TextContent>
+ <Text component={TextVariants.h1}>Samples Catalog</Text>
+ <Text component={TextVariants.p}>Try one of our samples to start defining your model.</Text>
+ </TextContent>
+ <Toolbar style={{ paddingBottom: "0" }}>
+ <ToolbarContent style={{ paddingLeft: "0", paddingRight: "0", paddingBottom: "0" }}>
+ <ToolbarItem variant="search-filter">
<SearchInput
value={""}
type={"search"}
@@ -150,8 +175,8 @@ export function Showcase() {
e.stopPropagation();
}}
/>
- </FlexItem>
- <FlexItem>
+ </ToolbarItem>
+ <ToolbarItem>
<Dropdown
style={{ backgroundColor: "white" }}
onSelect={() => setCategoryFilterDropdownOpen(false)}
@@ -166,46 +191,74 @@ export function Showcase() {
}
isOpen={isCategoryFilterDropdownOpen}
/>
- </FlexItem>
- <FlexItem>
+ </ToolbarItem>
+ <ToolbarItem>
{filterResultMessage && (
<TextContent>
<Text>{filterResultMessage}</Text>
</TextContent>
)}
- </FlexItem>
- </Flex>
- <br />
- {loading && <SampleCardSkeleton numberOfCards={4} />}
- {!loading && samples.length === 0 && (
- <PageSection variant={"light"} isFilled={true} style={{ marginRight: "25px" }}>
- <EmptyState style={{ height: "350px" }}>
- <EmptyStateIcon icon={CubesIcon} />
- <Title headingLevel="h4" size="lg">
- {"None of the available samples matched this search"}
- </Title>
- </EmptyState>
- </PageSection>
- )}
- {!loading && samples.length > 0 && (
- <Gallery
- hasGutter={true}
- minWidths={{ sm: "calc(100%/3.1 - 16px)", default: "100%" }}
- style={{
- overflowX: "auto",
- gridAutoFlow: "column",
- gridAutoColumns: "minmax(calc(100%/3.1 - 16px),1fr)",
- paddingBottom: "8px",
- paddingRight: "var(--pf-c-page__main-section--xl--PaddingRight)",
- }}
- >
- {samples.map((sample) => (
- <SampleCard sample={sample} key={`sample-${sample.sampleId}`} />
- ))}
- </Gallery>
- )}
- </>
- )}
- </>
+ </ToolbarItem>
+ <ToolbarItem variant="pagination">
+ {loading && <Skeleton width="200px" />}
+ {!loading && (
+ <Pagination
+ isCompact
+ itemCount={samplesCount}
+ onSetPage={onSetPage}
+ page={page}
+ perPage={CARDS_PER_PAGE}
+ perPageOptions={SAMPLE_CARDS_PER_PAGE_OPTIONS}
+ variant="top"
+ />
+ )}
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ </PageSection>
+
+ <PageSection isFilled>
+ {sampleLoadingError && <SamplesLoadError errors={[sampleLoadingError]} />}
+ {!sampleLoadingError && (
+ <>
+ {loading && <SampleCardSkeleton numberOfCards={6} />}
+ {!loading && samplesCount === 0 && (
+ <PageSection variant={"light"} isFilled={true} style={{ marginRight: "25px" }}>
+ <EmptyState style={{ height: "350px" }}>
+ <EmptyStateIcon icon={CubesIcon} />
+ <Title headingLevel="h4" size="lg">
+ {"None of the available samples matched this search"}
+ </Title>
+ </EmptyState>
+ </PageSection>
+ )}
+ {!loading && samplesCount > 0 && (
+ <>
+ <Gallery hasGutter={true} minWidths={{ sm: "calc(100%/3.1 - 16px)", default: "100%" }}>
+ {visibleSamples.map((sample) => (
+ <SampleCard
+ sample={sample}
+ key={`sample-${sample.sampleId}`}
+ cover={sampleCovers[sample.sampleId]}
+ />
+ ))}
+ </Gallery>
+ <br />
+ <Pagination
+ itemCount={samplesCount}
+ onSetPage={onSetPage}
+ page={page}
+ perPage={CARDS_PER_PAGE}
+ perPageComponent="button"
+ perPageOptions={SAMPLE_CARDS_PER_PAGE_OPTIONS}
+ variant={PaginationVariant.bottom}
+ widgetId="bottom-example"
+ />
+ </>
+ )}
+ </>
+ )}
+ </PageSection>
+ </Page>
);
}
diff --git a/packages/serverless-logic-web-tools/src/home/sample/hooks/SampleContext.tsx b/packages/serverless-logic-web-tools/src/home/sample/hooks/SampleContext.tsx
index f8fdbd7e4c..65204accd1 100644
--- a/packages/serverless-logic-web-tools/src/home/sample/hooks/SampleContext.tsx
+++ b/packages/serverless-logic-web-tools/src/home/sample/hooks/SampleContext.tsx
@@ -20,17 +20,42 @@ import { LocalFile } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/Loc
import * as React from "react";
import { useContext, useMemo, useCallback, useState } from "react";
import { useSettingsDispatch } from "../../../settings/SettingsContext";
-import { fetchSampleDefinitions, fetchSampleFiles, Sample, SampleCategory } from "../sampleApi";
+import {
+ fetchSampleCover,
+ fetchSampleDefinitions,
+ fetchSampleFiles,
+ Sample,
+ SampleCategory,
+ SampleCoversHashtable,
+} from "../sampleApi";
import { decoder, encoder } from "@kie-tools-core/workspaces-git-fs/dist/encoderdecoder/EncoderDecoder";
import Fuse from "fuse.js";
const SAMPLE_DEFINITIONS_CACHE_FILE_PATH = "/definitions.json";
+const SAMPLE_COVERS_CACHE_FILE_PATH = "/covers.json";
const SAMPLES_FS_MOUNT_POINT = `lfs_v1__samples__${process.env.WEBPACK_REPLACE__version!}`;
const SEARCH_KEYS = ["definition.category", "definition.title", "definition.description"];
export interface SampleDispatchContextType {
getSamples(args: { categoryFilter?: SampleCategory; searchFilter?: string }): Promise<Sample[]>;
getSampleFiles(sampleId: string): Promise<LocalFile[]>;
+ /**
+ * Gets the cover image for a sample from cache, or loads it from the API and caches it.
+ * @param args.sample The sample to get the cover image for.
+ * @param args.noCacheWriting A flag indicating whether to write the loaded cover image to cache.
+ * @returns The cover image as a base64-encoded string.
+ */
+ getSampleCover(args: { sample: Sample; noCacheWriting?: boolean }): Promise<string | undefined>;
+ /**
+ * Gets the cover images for an array of samples from cache, or loads them from the API and caches them.
+ * @param args.samples An array of samples to get the cover images for.
+ * @param args.prevState The previous state of the entities being loaded
+ * @returns An object containing the sample IDs as keys and the cover images as base64-encoded strings.
+ */
+ getSampleCovers(args: {
+ samples: Sample[];
+ prevState: { [key: string]: string | undefined };
+ }): Promise<SampleCoversHashtable>;
}
export const SampleDispatchContext = React.createContext<SampleDispatchContextType>({} as any);
@@ -43,23 +68,81 @@ export function SampleContextProvider(props: React.PropsWithChildren<{}>) {
const sampleStorageService = useMemo(() => new LfsStorageService(), []);
const [allSampleDefinitions, setAllSampleDefinitions] = useState<Sample[]>();
-
- const loadCache = useCallback(
- async (args: { path: string; loadFn: () => Promise<any> }) => {
+ const [allSampleCovers, setAllSampleCovers] = useState<{ [sampleId: string]: string }>({});
+
+ /**
+ * Retrieves the contents of a cache file
+ *
+ * @param args.path The path of the cache file to retrieve.
+ * @returns The JSON-parsed contents of the cache file, or `null` if the file doesn't exist.
+ */
+ const getCacheContent = useCallback(
+ async (args: { path: string }) => {
const storageFile = await sampleStorageService.getFile(fs, args.path);
if (storageFile) {
const cacheContent = decoder.decode(await storageFile.getFileContents());
return JSON.parse(cacheContent);
}
- const content = await args.loadFn();
+ return null;
+ },
+ [sampleStorageService, fs]
+ );
+
+ /**
+ * Adds or updates the contents of a cache file
+ *
+ * @param args.path The path of the cache file to retrieve.
+ * @param args.content The content to add in the cache file.
+ * @returns The content stored in the cache file.
+ */
+ const addCacheContent = useCallback(
+ async (args: { path: string; content: any }) => {
const cacheFile = new LfsStorageFile({
path: args.path,
- getFileContents: async () => encoder.encode(JSON.stringify(content)),
+ getFileContents: async () => encoder.encode(JSON.stringify(args.content)),
});
- await sampleStorageService.createFiles(fs, [cacheFile]);
- return content;
+ await sampleStorageService.createOrOverwriteFile(fs, cacheFile);
+ return args.content;
},
- [fs, sampleStorageService]
+ [sampleStorageService, fs]
+ );
+
+ const loadCache = useCallback(
+ async (args: { path: string; loadFn: () => Promise<any> }) => {
+ const cacheContent = await getCacheContent(args);
+
+ if (cacheContent) {
+ return cacheContent;
+ }
+ const content = await args.loadFn();
+
+ return await addCacheContent({ ...args, content });
+ },
+ [getCacheContent, addCacheContent]
+ );
+
+ /**
+ * Loads an entity from cache if available, otherwise loads it from the provided load function and saves it to cache.
+ * @param args.path The path of the cache file.
+ * @param args.id The unique identifier for the entity being loaded.
+ * @param args.noCacheWriting A flag indicating whether to write the loaded entity to cache.
+ * @param args.loadFn The function to use to load the entity if it is not found in cache.
+ * @returns The loaded entity.
+ */
+ const loadCacheEntity = useCallback(
+ async (args: { path: string; id: string; noCacheWriting?: boolean; loadFn: () => Promise<any> }) => {
+ const cacheContent = (await getCacheContent(args)) || {};
+ if (cacheContent[args.id]) {
+ return cacheContent[args.id];
+ }
+
+ cacheContent[args.id] = await args.loadFn();
+
+ return !args.noCacheWriting
+ ? (await addCacheContent({ ...args, content: cacheContent }))[args.id]
+ : cacheContent[args.id];
+ },
+ [getCacheContent, addCacheContent]
);
const getSamples = useCallback(
@@ -95,12 +178,57 @@ export function SampleContextProvider(props: React.PropsWithChildren<{}>) {
[allSampleDefinitions, loadCache, settingsDispatch.github.octokit]
);
+ const getSampleCover = useCallback(
+ async (args: { sample: Sample; noCacheWriting?: boolean }) => {
+ const cachedCover = allSampleCovers[args.sample.sampleId];
+
+ if (!cachedCover) {
+ const cover = await loadCacheEntity({
+ path: SAMPLE_COVERS_CACHE_FILE_PATH,
+ id: args.sample.sampleId,
+ loadFn: async () => fetchSampleCover({ octokit: settingsDispatch.github.octokit, sample: args.sample }),
+ noCacheWriting: args.noCacheWriting,
+ });
+
+ allSampleCovers[args.sample.sampleId] = cover;
+ setAllSampleCovers(allSampleCovers);
+ return cover;
+ } else {
+ return cachedCover;
+ }
+ },
+ [settingsDispatch.github.octokit, loadCacheEntity, allSampleCovers]
+ );
+
+ const getSampleCovers = useCallback(
+ async (args: { samples: Sample[]; prevState: { [key: string]: string } }) => {
+ if (!args.samples.length) {
+ return {};
+ }
+
+ const covers = (
+ await Promise.all(
+ args.samples.map(async (sample) => ({
+ [sample.sampleId]: await getSampleCover({ ...args, sample, noCacheWriting: true }),
+ }))
+ )
+ ).reduce((acc, curr) => ({ ...acc, ...curr }), args.prevState || {});
+
+ const cacheContent = await getCacheContent({ path: SAMPLE_COVERS_CACHE_FILE_PATH });
+ return await addCacheContent({ path: SAMPLE_COVERS_CACHE_FILE_PATH, content: { ...covers, ...cacheContent } });
+ },
+ [getSampleCover, addCacheContent, getCacheContent]
+ );
+
const getSampleFiles = useCallback(
async (sampleId: string) => fetchSampleFiles({ octokit: settingsDispatch.github.octokit, sampleId }),
[settingsDispatch.github.octokit]
);
- const dispatch = useMemo(() => ({ getSamples, getSampleFiles }), [getSamples, getSampleFiles]);
+ const dispatch = useMemo(
+ () => ({ getSamples, getSampleFiles, getSampleCover, getSampleCovers }),
+ [getSamples, getSampleFiles, getSampleCover, getSampleCovers]
+ );
return <SampleDispatchContext.Provider value={dispatch}>{props.children}</SampleDispatchContext.Provider>;
}
diff --git a/packages/serverless-logic-web-tools/src/home/sample/sampleApi.ts b/packages/serverless-logic-web-tools/src/home/sample/sampleApi.ts
index e4054706dc..7a8efb27ba 100644
--- a/packages/serverless-logic-web-tools/src/home/sample/sampleApi.ts
+++ b/packages/serverless-logic-web-tools/src/home/sample/sampleApi.ts
@@ -39,7 +39,10 @@ type GitHubFileInfo = GitHubRepoInfo & { path: string };
export type SampleCategory = "serverless-workflow" | "serverless-decision" | "dashbuilder";
+const SUPPORTING_FILES_FOLDER = join(".github", "supporting-files");
+
const SAMPLE_DEFINITION_FILE = "definition.json";
+const SAMPLE_DEFINITIONS_FILE = join(SUPPORTING_FILES_FOLDER, "sample-definitions.json");
const SAMPLE_TEMPLATE_FOLDER = "template";
@@ -49,6 +52,18 @@ const SAMPLE_FOLDER = "samples";
type SampleStatus = "ok" | "out of date" | "deprecated";
+interface SampleSocial {
+ network: string;
+ id: string;
+}
+
+interface SampleAuthor {
+ name: string;
+ email: string;
+ github: string;
+ social: SampleSocial[];
+}
+
interface SampleDefinition {
category: SampleCategory;
status: SampleStatus;
@@ -56,6 +71,12 @@ interface SampleDefinition {
description: string;
cover: string;
tags: string[];
+ type: string;
+ dependencies: string[];
+ related_to: string[];
+ resources: string[];
+ authors: SampleAuthor[];
+ sample_path: string;
}
interface ContentData {
@@ -73,14 +94,17 @@ interface ContentData {
export type Sample = {
sampleId: string;
definition: SampleDefinition;
- svgContent: string;
+};
+
+export type SampleCoversHashtable = {
+ [sampleId: string]: string;
};
export const KIE_SAMPLES_REPO: GitHubFileInfo = {
owner: "kiegroup",
repo: "kie-samples",
ref: process.env["WEBPACK_REPLACE__samplesRepositoryRef"]!,
- path: "samples",
+ path: SAMPLE_FOLDER,
};
async function fetchFileContent(args: { octokit: Octokit; fileInfo: GitHubFileInfo }): Promise<string | undefined> {
@@ -143,56 +167,56 @@ async function listSampleDefinitionFiles(args: {
.map((folder) => ({ sampleId: folder.name, definitionPath: join(folder.path, SAMPLE_DEFINITION_FILE) }));
}
+/**
+ * fetch the Sample Definitions file from the repository.
+ * @param args.octokit An instance of the Octokit GitHub API client.
+ * @returns an array of Samples
+ */
export async function fetchSampleDefinitions(octokit: Octokit): Promise<Sample[]> {
- const sampleDefinitionFiles = await listSampleDefinitionFiles({
+ const fileContent = await fetchFileContent({
octokit,
- repoInfo: { ...KIE_SAMPLES_REPO },
+ fileInfo: {
+ ...KIE_SAMPLES_REPO,
+ path: SAMPLE_DEFINITIONS_FILE,
+ },
});
- const samples = (
- await Promise.all(
- sampleDefinitionFiles.map(async (definitionFile) => {
- const fileContent = await fetchFileContent({
- octokit,
- fileInfo: {
- ...KIE_SAMPLES_REPO,
- path: definitionFile.definitionPath,
- },
- });
-
- if (!fileContent) {
- console.error(`Could not read sample definition for ${definitionFile.sampleId}`);
- return null;
- }
-
- const definition = JSON.parse(fileContent) as SampleDefinition;
- const svgContent = await fetchFileContent({
- octokit,
- fileInfo: {
- ...KIE_SAMPLES_REPO,
- path: join("samples", definitionFile.sampleId, definition.cover),
- },
- });
-
- if (!svgContent) {
- console.error(`Could not read sample svg for ${definitionFile.sampleId}`);
- return null;
- }
-
- return {
- sampleId: definitionFile.sampleId,
- definition,
- svgContent,
- };
- })
- )
- ).filter((sample) => sample !== null) as Sample[];
+ if (!fileContent) {
+ console.error(`Could not read sample definitions`);
+ return [];
+ }
+
+ const definitions = JSON.parse(fileContent) as SampleDefinition[];
+
+ return definitions.map((definition) => ({
+ sampleId: definition.sample_path.replace(new RegExp(`^${SAMPLE_FOLDER}/`), ""),
+ definition,
+ }));
+}
- if (samples.length === 0) {
- throw new Error("No samples could be loaded.");
+/**
+ * Fetches the cover of a given sample using the GitHub API.
+ * @param args.octokit An instance of the Octokit GitHub API client.
+ * @param args.sample The sample object for which the cover is being fetched.
+ * @returns The content of the SVG cover file for the sample, or undefined if it could not be fetched.
+ */
+export async function fetchSampleCover(args: { octokit: Octokit; sample: Sample }): Promise<string | undefined> {
+ const { sample } = args;
+
+ const svgContent = await fetchFileContent({
+ octokit: args.octokit,
+ fileInfo: {
+ ...KIE_SAMPLES_REPO,
+ path: join(SAMPLE_FOLDER, sample.sampleId, sample.definition.cover),
+ },
+ });
+
+ if (!svgContent) {
+ console.error(`Could not read sample svg for ${sample.sampleId}`);
+ return;
}
- return samples;
+ return svgContent;
}
export async function fetchSampleFiles(args: { octokit: Octokit; sampleId: string }): Promise<LocalFile[]> {
@@ -209,7 +233,9 @@ export async function fetchSampleFiles(args: { octokit: Octokit; sampleId: strin
}
const sampleFiles = sampleFolderFiles
- .filter((file) => file.name !== SAMPLE_DEFINITION_FILE && extname(file.name) !== SVG_EXTENSION)
+ .filter(
+ (file) => file.name !== SAMPLE_DEFINITION_FILE && extname(file.name) !== SVG_EXTENSION && file.type === "file"
+ )
.map(async (file) => {
const fileContent = await fetchFileContent({
octokit: args.octokit,
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org