瀏覽代碼

[POR-2189] add image tag loading state (#4137)

Feroze Mohideen 2 年之前
父節點
當前提交
560e902ae5

+ 60 - 53
dashboard/src/main/home/app-dashboard/image-settings/ImageList.tsx

@@ -1,15 +1,17 @@
 import React, { useState } from "react";
+import _ from "lodash";
 import styled from "styled-components";
 
-import { integrationList } from "shared/common";
-import addCircle from "assets/add-circle.png";
 import Loading from "components/Loading";
-import { ImageType } from "./types";
-import SearchBar from "components/SearchBar";
 import Link from "components/porter/Link";
-import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
-import _ from "lodash";
+import Text from "components/porter/Text";
+import SearchBar from "components/SearchBar";
+
+import { integrationList } from "shared/common";
+import addCircle from "assets/add-circle.png";
+
+import { type ImageType } from "./types";
 
 type Props = {
   loading: boolean;
@@ -17,58 +19,64 @@ type Props = {
   setSelectedImage: (x: ImageType) => void;
 };
 
-const ImageList: React.FC<Props> = ({
-  setSelectedImage,
-  loading,
-  images,
-}) => {
+const ImageList: React.FC<Props> = ({ setSelectedImage, loading, images }) => {
   const [searchFilter, setSearchFilter] = useState<string>("");
 
-  const renderImageList = () => {
+  const renderImageList = (): JSX.Element => {
     if (loading) {
       return (
         <LoadingWrapper>
-          <Loading message={"Loading all images linked to your project"}/>
+          <Loading message={"Loading all images linked to your project"} />
         </LoadingWrapper>
       );
     } else if (images.length === 0 && !searchFilter) {
-      return <LoadingWrapper>
-        <Text color="helper">No linked images found.</Text>
-        <Spacer y={0.5} />
-        <div>
-          <Link to={"/integrations/registry"}>Configure linked image registries</Link>, or provide the URL of a public image (e.g. "nginx") to continue.
-        </div>
-      </LoadingWrapper>;
+      return (
+        <LoadingWrapper>
+          <Text color="helper">No linked images found.</Text>
+          <Spacer y={0.5} />
+          <div>
+            <Link to={"/integrations/registry"}>
+              Configure linked image registries
+            </Link>
+            , or provide the URL of a public image (e.g. &ldquo;nginx&rdquo;) to
+            continue.
+          </div>
+        </LoadingWrapper>
+      );
     }
 
     const sortedImages = searchFilter
       ? images
-        .filter((img) =>
-          img.uri.toLowerCase().includes(searchFilter.toLowerCase())
-        )
-        .sort((a, b) => {
-          const aIndex = a.uri.toLowerCase().indexOf(searchFilter.toLowerCase());
-          const bIndex = b.uri.toLowerCase().indexOf(searchFilter.toLowerCase());
-          return aIndex - bIndex;
-        })
+          .filter((img) =>
+            img.uri.toLowerCase().includes(searchFilter.toLowerCase())
+          )
+          .sort((a, b) => {
+            const aIndex = a.uri
+              .toLowerCase()
+              .indexOf(searchFilter.toLowerCase());
+            const bIndex = b.uri
+              .toLowerCase()
+              .indexOf(searchFilter.toLowerCase());
+            return aIndex - bIndex;
+          })
       : images.sort((a, b) => {
-        const mostRecentTagA = _.maxBy(a.artifacts, (artifact) => {
-          return new Date(artifact.updated_at ?? "").getTime();
-        });
-        const mostRecentTagB = _.maxBy(b.artifacts, (artifact) => {
-          return new Date(artifact.updated_at ?? "").getTime();
+          const mostRecentTagA = _.maxBy(a.artifacts, (artifact) => {
+            return new Date(artifact.updated_at ?? "").getTime();
+          });
+          const mostRecentTagB = _.maxBy(b.artifacts, (artifact) => {
+            return new Date(artifact.updated_at ?? "").getTime();
+          });
+          if (!mostRecentTagA) {
+            return 1;
+          }
+          if (!mostRecentTagB) {
+            return -1;
+          }
+          return (
+            new Date(mostRecentTagB.updated_at ?? "").getTime() -
+            new Date(mostRecentTagA.updated_at ?? "").getTime()
+          );
         });
-        if (!mostRecentTagA) {
-          return 1;
-        }
-        if (!mostRecentTagB) {
-          return -1;
-        }
-        return (
-          new Date(mostRecentTagB.updated_at ?? "").getTime() -
-          new Date(mostRecentTagA.updated_at ?? "").getTime()
-        );
-      });
 
     const imageCards = sortedImages.map((image: ImageType, i: number) => {
       return (
@@ -78,12 +86,15 @@ const ImageList: React.FC<Props> = ({
             setSelectedImage(image);
           }}
         >
-          <img src={integrationList["dockerhub"].icon} />
+          <img src={integrationList.dockerhub.icon} />
           {image.uri}
         </ImageItem>
       );
     });
-    if (searchFilter !== "" && !images.some((image) => image.uri === searchFilter)) {
+    if (
+      searchFilter !== "" &&
+      !images.some((image) => image.uri === searchFilter)
+    ) {
       imageCards.push(
         <ImageItem
           key={images.length}
@@ -95,11 +106,11 @@ const ImageList: React.FC<Props> = ({
           }}
         >
           <img src={addCircle} />
-          {`Use image URL: \"${searchFilter}\"`}
+          {`Use image URL: "${searchFilter}"`}
         </ImageItem>
       );
     }
-    return imageCards;
+    return <>{imageCards}</>;
   };
 
   return (
@@ -109,9 +120,7 @@ const ImageList: React.FC<Props> = ({
         disabled={loading}
         prompt={"Search images..."}
       />
-      <ExpandedWrapper>
-        {renderImageList()}
-      </ExpandedWrapper>
+      <ExpandedWrapper>{renderImageList()}</ExpandedWrapper>
     </>
   );
 };
@@ -165,5 +174,3 @@ const ExpandedWrapper = styled.div`
   background: #ffffff11;
   overflow-y: auto;
 `;
-
-

+ 120 - 115
dashboard/src/main/home/app-dashboard/image-settings/ImageSettings.tsx

@@ -1,131 +1,136 @@
 import React, { useEffect, useState } from "react";
 import { useQuery } from "@tanstack/react-query";
-import api from "shared/api";
-import Text from "components/porter/Text";
-import Spacer from "components/porter/Spacer";
 import styled from "styled-components";
-import Input from "components/porter/Input";
 import { z } from "zod";
+
+import Input from "components/porter/Input";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+import api from "shared/api";
+
 import ImageList from "./ImageList";
 import TagList from "./TagList";
-import { ImageType, imageValidator } from "./types";
+import { imageValidator, type ImageType } from "./types";
 
 type Props = {
-    projectId: number;
-    imageUri: string;
-    imageTag: string;
-    setImageUri: (uri: string) => void;
-    setImageTag: (tag: string) => void;
-    resetImageInfo: () => void;
+  projectId: number;
+  imageUri: string;
+  imageTag: string;
+  setImageUri: (uri: string) => void;
+  setImageTag: (tag: string) => void;
+  resetImageInfo: () => void;
 };
 
 const ImageSettings: React.FC<Props> = ({
-    projectId,
-    imageUri,
-    imageTag,
-    setImageUri,
-    setImageTag,
-    resetImageInfo,
+  projectId,
+  imageUri,
+  imageTag,
+  setImageUri,
+  setImageTag,
+  resetImageInfo,
 }) => {
-    const [images, setImages] = useState<ImageType[]>([]);
-    const [selectedImage, setSelectedImage] = useState<ImageType | undefined>(undefined);
-    const resp = useQuery(
-        ["getImages", projectId],
-        async () => {
-            const res = await api.images("<token>", {}, { project_id: projectId });
-            return await z.object({ images: z.array(imageValidator) }).parseAsync(res.data);
-        },
-        {
-            refetchOnWindowFocus: false,
-        }
-    );
-    
-    useEffect(() => {
-        if (resp.isSuccess) {
-            const images = resp.data.images;
-            setImages(images);
-            if (imageUri) {
-                setSelectedImage(images.find((image) => image.uri === imageUri));
-            }
-        }
-    }, [resp]);
+  const [images, setImages] = useState<ImageType[]>([]);
+  const [selectedImage, setSelectedImage] = useState<ImageType | undefined>(
+    undefined
+  );
+  const resp = useQuery(
+    ["getImages", projectId],
+    async () => {
+      const res = await api.images("<token>", {}, { project_id: projectId });
+      return await z
+        .object({ images: z.array(imageValidator) })
+        .parseAsync(res.data);
+    },
+    {
+      refetchOnWindowFocus: false,
+    }
+  );
+
+  useEffect(() => {
+    if (resp.isSuccess) {
+      const images = resp.data.images;
+      setImages(images);
+      if (imageUri) {
+        setSelectedImage(images.find((image) => image.uri === imageUri));
+      }
+    }
+  }, [resp]);
 
-    return (
-        <div>
-            <Text size={16}>Image settings</Text>
-            <Spacer y={0.5} />
-            {!imageUri && (
-                <>
-                    <Text color="helper">Specify your image URL.</Text>
-                    <Spacer y={0.5} />
-                    <ExpandedWrapper>
-                        <ImageList
-                            setSelectedImage={(image: ImageType) => {
-                                setSelectedImage(image);
-                                setImageUri(image.uri);
-                            }}
-                            images={images}
-                            loading={resp.isLoading}
-                        />
-                    </ExpandedWrapper>
-                    <DarkMatter antiHeight="-4px" />
-                    <Spacer y={0.3} />
-                </>
-            )}
-            {imageUri && (
-                <>
-                    <Input
-                        disabled={true}
-                        label="Image URL:"
-                        width="100%"
-                        value={selectedImage?.uri ?? imageUri}
-                        placeholder=""
-                    />
-                    <BackButton
-                        width="170px"
-                        onClick={resetImageInfo}
-                    >
-                        <i className="material-icons">keyboard_backspace</i>
-                        Select image URL
-                    </BackButton>
-                    <Spacer y={1} />
-                    {imageTag ? (
-                        <>
-                            <Input
-                                disabled={true}
-                                label="Image tag:"
-                                type="text"
-                                width="100%"
-                                value={imageTag}
-                                setValue={() => { }}
-                                placeholder=""
-                            />
-                            <BackButton
-                                width="170px"
-                                onClick={() => {
-                                    setImageTag("")
-                                }}
-                            >
-                                <i className="material-icons">keyboard_backspace</i>
-                                Select image tag
-                            </BackButton>
-                        </>
-                    ) : (
-                        <>
-                            <Text color="helper">Specify your image tag.</Text>
-                            <Spacer y={0.5} />
-                            <ExpandedWrapper>
-                                <TagList
-                                    selectedImage={selectedImage}
-                                    setSelectedTag={setImageTag}
-                                />
-                            </ExpandedWrapper>
-                        </>
-                    )}
-                </>
-            )}
-        </div>
-    );
+  return (
+    <div>
+      <Text size={16}>Image settings</Text>
+      <Spacer y={0.5} />
+      {!imageUri && (
+        <>
+          <Text color="helper">Specify your image URL.</Text>
+          <Spacer y={0.5} />
+          <ExpandedWrapper>
+            <ImageList
+              setSelectedImage={(image: ImageType) => {
+                setSelectedImage(image);
+                setImageUri(image.uri);
+              }}
+              images={images}
+              loading={resp.isLoading}
+            />
+          </ExpandedWrapper>
+          <DarkMatter antiHeight="-4px" />
+          <Spacer y={0.3} />
+        </>
+      )}
+      {imageUri && (
+        <>
+          <Input
+            disabled={true}
+            label="Image URL:"
+            width="100%"
+            value={selectedImage?.uri ?? imageUri}
+            placeholder=""
+          />
+          <BackButton width="170px" onClick={resetImageInfo}>
+            <i className="material-icons">keyboard_backspace</i>
+            Select image URL
+          </BackButton>
+          <Spacer y={1} />
+          {imageTag ? (
+            <>
+              <Input
+                disabled={true}
+                label="Image tag:"
+                type="text"
+                width="100%"
+                value={imageTag}
+                setValue={() => {}}
+                placeholder=""
+              />
+              <BackButton
+                width="170px"
+                onClick={() => {
+                  setImageTag("");
+                }}
+              >
+                <i className="material-icons">keyboard_backspace</i>
+                Select image tag
+              </BackButton>
+            </>
+          ) : (
+            <>
+              <Text color="helper">Specify your image tag.</Text>
+              <Spacer y={0.5} />
+              <ExpandedWrapper>
+                <TagList
+                  selectedImage={selectedImage}
+                  setSelectedTag={setImageTag}
+                  loading={resp.isLoading}
+                />
+              </ExpandedWrapper>
+            </>
+          )}
+        </>
+      )}
+    </div>
+  );
 };
 
 export default ImageSettings;

+ 63 - 38
dashboard/src/main/home/app-dashboard/image-settings/TagList.tsx

@@ -1,23 +1,35 @@
 import React, { useState } from "react";
 import styled from "styled-components";
-import tag_icon from "assets/tag.png";
-import addCircle from "assets/add-circle.png";
-import { ArtifactType, ImageType } from "./types";
+
+import Loading from "components/Loading";
 import SearchBar from "components/SearchBar";
 
+import addCircle from "assets/add-circle.png";
+import tag_icon from "assets/tag.png";
+
+import { type ArtifactType, type ImageType } from "./types";
+
 type Props = {
   selectedImage?: ImageType;
   setSelectedTag: (x: string) => void;
+  loading: boolean;
 };
 
 const TagList: React.FC<Props> = ({
   selectedImage,
   setSelectedTag,
+  loading,
 }) => {
   const [searchFilter, setSearchFilter] = useState<string>("");
 
-  const renderTagList = () => {
-    if (selectedImage == null) {
+  const renderTagList = (): JSX.Element => {
+    if (loading) {
+      return (
+        <LoadingWrapper>
+          <Loading message={"Loading all images linked to your project"} />
+        </LoadingWrapper>
+      );
+    } else if (selectedImage == null) {
       if (searchFilter) {
         return (
           <TagItem
@@ -26,47 +38,62 @@ const TagList: React.FC<Props> = ({
             }}
           >
             <img src={addCircle} />
-            {`Use tag \"${searchFilter}\"`}
+            {`Use tag "${searchFilter}"`}
           </TagItem>
         );
       }
-      return <LoadingWrapper>Please specify an tag.</LoadingWrapper>;
+      return <LoadingWrapper>Please specify a tag.</LoadingWrapper>;
     }
-    
+
     if (selectedImage.artifacts.length === 0 && !searchFilter) {
-      return <LoadingWrapper>Image has no tags; please specify a different image.</LoadingWrapper>;
+      return (
+        <LoadingWrapper>
+          Image has no tags; please specify a different image.
+        </LoadingWrapper>
+      );
     }
 
     const sortedArtifacts = searchFilter
       ? selectedImage.artifacts
-        .filter(({ tag }) => tag.toLowerCase().includes(searchFilter.toLowerCase()))
-        .sort((a, b) => {
-          const aIndex = a.tag.toLowerCase().indexOf(searchFilter.toLowerCase());
-          const bIndex = b.tag.toLowerCase().indexOf(searchFilter.toLowerCase());
-          return aIndex - bIndex;
-        })
+          .filter(({ tag }) =>
+            tag.toLowerCase().includes(searchFilter.toLowerCase())
+          )
+          .sort((a, b) => {
+            const aIndex = a.tag
+              .toLowerCase()
+              .indexOf(searchFilter.toLowerCase());
+            const bIndex = b.tag
+              .toLowerCase()
+              .indexOf(searchFilter.toLowerCase());
+            return aIndex - bIndex;
+          })
       : selectedImage.artifacts.sort((a, b) => {
+          return (
+            new Date(b.updated_at ?? "").getTime() -
+            new Date(a.updated_at ?? "").getTime()
+          );
+        });
+
+    const tagCards = sortedArtifacts.map(
+      (artifact: ArtifactType, i: number) => {
         return (
-          new Date(b.updated_at ?? "").getTime() -
-          new Date(a.updated_at ?? "").getTime()
+          <TagItem
+            key={i}
+            onClick={() => {
+              setSelectedTag(artifact.tag);
+            }}
+          >
+            <img src={tag_icon} />
+            {artifact.tag}
+          </TagItem>
         );
-      })
-
-    const tagCards = sortedArtifacts.map((artifact: ArtifactType, i: number) => {
-      return (
-        <TagItem
-          key={i}
-          onClick={() => {
-            setSelectedTag(artifact.tag);
-          }}
-        >
-          <img src={tag_icon} />
-          {artifact.tag}
-        </TagItem>
-      );
-    });
+      }
+    );
 
-    if (searchFilter !== "" && !sortedArtifacts.some(({ tag }) => tag === searchFilter)) {
+    if (
+      searchFilter !== "" &&
+      !sortedArtifacts.some(({ tag }) => tag === searchFilter)
+    ) {
       tagCards.push(
         <TagItem
           onClick={() => {
@@ -74,12 +101,12 @@ const TagList: React.FC<Props> = ({
           }}
         >
           <img src={addCircle} />
-          {`Use tag \"${searchFilter}\"`}
+          {`Use tag "${searchFilter}"`}
         </TagItem>
       );
     }
 
-    return tagCards;
+    return <>{tagCards}</>;
   };
 
   return (
@@ -89,9 +116,7 @@ const TagList: React.FC<Props> = ({
         disabled={false}
         prompt={"Search tags..."}
       />
-      <ExpandedWrapper>
-        {renderTagList()}
-      </ExpandedWrapper>
+      <ExpandedWrapper>{renderTagList()}</ExpandedWrapper>
     </>
   );
 };