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 <[email protected]>
AuthorDate: Tue May 16 13:33:43 2023 +0200

    KOGITO-8153: Create an extensible Samples page (now Catalog) (#1635)
    
    Co-authored-by: Guilherme Caponetto 
<[email protected]>
---
 .../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 />
-              &nbsp;&nbsp;<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 />
+                &nbsp;&nbsp;<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: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to