Răsfoiți Sursa

Implemented select input

jnfrati 3 ani în urmă
părinte
comite
c74b0a5e45

+ 229 - 0
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/Select.tsx

@@ -0,0 +1,229 @@
+import Loading from "components/Loading";
+import React, { useRef, useState } from "react";
+import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
+import styled from "styled-components";
+
+export type SelectProps<T> = {
+  value: T;
+  options: T[];
+  accessor: (option: T) => string | React.ReactNode;
+  onChange: (value: T) => void;
+  isOptionEqualToValue?: (option: T, value: T) => boolean;
+  label: string;
+  isLoading?: boolean;
+  dropdown?: {
+    maxH?: string;
+    width?: string;
+    label?: string;
+    option?: {
+      height?: string;
+    };
+  };
+  placeholder: string;
+  className?: string;
+  readOnly?: boolean;
+};
+
+const Select = <T extends unknown>({
+  value,
+  options,
+  accessor,
+  onChange,
+  isOptionEqualToValue,
+  label,
+  isLoading,
+  placeholder,
+  dropdown,
+  className,
+  readOnly,
+}: SelectProps<T>) => {
+  const wrapperRef = useRef();
+  const [expanded, setExpanded] = useState(false);
+
+  useOutsideAlerter(wrapperRef, () => {
+    setExpanded(false);
+  });
+
+  const handleOptionClick = (value: T) => {
+    setExpanded(false);
+    onChange(value);
+  };
+
+  const getLabel = () => {
+    if (label) {
+      return <SelectStyles.Label> {label} </SelectStyles.Label>;
+    }
+    return null;
+  };
+
+  if (isLoading) {
+    return (
+      <>
+        {getLabel()}
+        <SelectStyles.Wrapper>
+          <SelectStyles.Selector
+            className={className}
+            expanded={false}
+            readOnly={readOnly}
+          >
+            <div>
+              <Loading />
+            </div>
+          </SelectStyles.Selector>
+        </SelectStyles.Wrapper>
+      </>
+    );
+  }
+
+  return (
+    <>
+      {getLabel()}
+      <SelectStyles.Wrapper ref={wrapperRef}>
+        <SelectStyles.Selector
+          className={className}
+          onClick={() => setExpanded(!expanded)}
+          expanded={expanded}
+          readOnly={readOnly}
+        >
+          <div>
+            <span>{value ? accessor(value) : placeholder}</span>
+          </div>
+          {readOnly ? null : <i className="material-icons">arrow_drop_down</i>}
+        </SelectStyles.Selector>
+        {expanded && !readOnly ? (
+          <SelectStyles.Dropdown.Wrapper
+            width={dropdown?.width}
+            maxH={dropdown?.maxH}
+          >
+            <SelectStyles.Dropdown.Label>
+              {dropdown?.label}
+            </SelectStyles.Dropdown.Label>
+            {options.map((option, i) => (
+              <SelectStyles.Dropdown.Option
+                key={i}
+                onClick={() => !readOnly && handleOptionClick(option)}
+                lastItem={i === options.length - 1}
+                selected={
+                  isOptionEqualToValue
+                    ? isOptionEqualToValue(option, value)
+                    : option === value
+                }
+                height={dropdown?.option?.height}
+              >
+                {accessor(option)}
+              </SelectStyles.Dropdown.Option>
+            ))}
+          </SelectStyles.Dropdown.Wrapper>
+        ) : null}
+      </SelectStyles.Wrapper>
+    </>
+  );
+};
+
+export default Select;
+
+export const SelectStyles = {
+  Wrapper: styled.div`
+    position: relative;
+  `,
+  Label: styled.div`
+    color: #ffffff;
+    margin-bottom: 10px;
+    font-size: 13px;
+  `,
+
+  Selector: styled.div<{ expanded: boolean; readOnly: boolean }>`
+    height: 35px;
+    border: 1px solid #ffffff55;
+    font-size: 13px;
+    padding: 5px 10px;
+    padding-left: 15px;
+    border-radius: 3px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    cursor: ${(props) => (props.readOnly ? "normal" : "pointer")};
+    background: ${(props) => {
+      if (props.readOnly) {
+        return "#ffffff55";
+      }
+
+      if (props.expanded) {
+        return "#ffffff33";
+      }
+      return "#ffffff11";
+    }};
+
+    :hover {
+      background: ${(props) => {
+        if (props.readOnly) {
+          return "#ffffff55";
+        }
+
+        if (props.expanded) {
+          return "#ffffff33";
+        }
+        return "#ffffff22";
+      }};
+    }
+
+    > i {
+      font-size: 20px;
+      transform: ${(props) => (props.expanded ? "rotate(180deg)" : "")};
+    }
+
+    > div {
+      display: flex;
+      align-items: center;
+      width: 85%;
+
+      > span {
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        z-index: 0;
+      }
+    }
+  `,
+
+  Dropdown: {
+    Wrapper: styled.div<{ width: string; maxH?: string }>`
+      background: #26282f;
+      width: ${(props) => props.width || "100%"};
+      max-height: ${(props) => props.maxH || "300px"};
+      border-radius: 3px;
+      z-index: 999;
+      overflow-y: auto;
+      margin-bottom: 20px;
+      box-shadow: 0 8px 20px 0px #00000088;
+      position: absolute;
+    `,
+    Option: styled.div<{
+      selected: boolean;
+      lastItem: boolean;
+      height?: string;
+    }>`
+      width: 100%;
+      border-top: 1px solid #00000000;
+      border-bottom: 1px solid
+        ${(props) => (props.lastItem ? "#ffffff00" : "#ffffff15")};
+      height: ${(props) => props.height || "37px"};
+      font-size: 13px;
+      align-items: center;
+      display: flex;
+      align-items: center;
+      padding-left: 15px;
+      cursor: pointer;
+      padding-right: 10px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      background: ${(props) => (props.selected ? "#ffffff11" : "")};
+
+      :hover {
+        background: #ffffff22;
+      }
+    `,
+    Label: styled.div``,
+  },
+};

+ 66 - 48
dashboard/src/main/home/cluster-dashboard/stacks/ExpandedStack/components/SourceEditorDocker.tsx

@@ -4,13 +4,17 @@ import Selector from "components/Selector";
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import api from "shared/api";
 import { Context } from "shared/Context";
+import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
 import { proxy, useSnapshot } from "valtio";
 import { SourceConfig } from "../../types";
+import Select from "./Select";
 
 const SourceEditorDocker = ({
   sourceConfig,
   onChange,
+  readOnly = false,
 }: {
+  readOnly: boolean;
   sourceConfig: SourceConfig;
   onChange: (sourceConfig: SourceConfig) => void;
 }) => {
@@ -48,9 +52,15 @@ const SourceEditorDocker = ({
         currentImageUrl={sourceConfig.image_repo_uri}
         value={registry}
         onChange={setRegistry}
+        readOnly={readOnly}
       />
       {registry && (
-        <_ImageSelector registry={registry} value={image} onChange={setImage} />
+        <_ImageSelector
+          registry={registry}
+          value={image}
+          onChange={setImage}
+          readOnly={readOnly}
+        />
       )}
       {registry && imageName && (
         <_TagSelector
@@ -58,6 +68,7 @@ const SourceEditorDocker = ({
           imageName={imageName}
           value={tag}
           onChange={setTag}
+          readOnly={readOnly}
         />
       )}
     </>
@@ -78,14 +89,17 @@ const _DockerRepositorySelector = ({
   currentImageUrl,
   value,
   onChange,
+  readOnly,
 }: {
   currentImageUrl: string;
   value: DockerRegistry;
   onChange: (newRegistry: DockerRegistry) => void;
+  readOnly: boolean;
 }) => {
   const { currentProject } = useContext(Context);
 
   const [registries, setRegistries] = useState<DockerRegistry[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
 
   useEffect(() => {
     api
@@ -104,32 +118,28 @@ const _DockerRepositorySelector = ({
           );
           onChange(currentRegistry);
         }
+        setIsLoading(false);
       });
   }, []);
 
-  const registryOptions = registries.map((registry) => {
-    return {
-      value: registry.url,
-      label: registry.name,
-    };
-  });
-
-  const handleChange = (registryUrl: string) => {
-    const registry = registries.find(
-      (registry) => registry.url === registryUrl
-    );
-
-    onChange(registry);
+  const handleChange = (newRegistry: DockerRegistry) => {
+    onChange(newRegistry);
   };
 
   return (
-    <SelectRow
-      value={value?.url}
-      options={registryOptions}
-      setActiveValue={handleChange}
-      width="100%"
-      label="Docker Registry"
-    />
+    <>
+      <Select
+        value={value}
+        options={registries}
+        onChange={handleChange}
+        accessor={(val) => val.name}
+        label="Docker Registry"
+        placeholder="Select a registry"
+        isOptionEqualToValue={(a, b) => a.url === b.url}
+        readOnly={readOnly}
+        isLoading={isLoading}
+      />
+    </>
   );
 };
 
@@ -143,16 +153,20 @@ const _ImageSelector = ({
   registry,
   value,
   onChange,
+  readOnly,
 }: {
   registry: DockerRegistry;
   value: string;
   onChange: (newValue: string) => void;
+  readOnly: boolean;
 }) => {
   const { currentProject } = useContext(Context);
 
   const [images, setImages] = useState<ImageRepo[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
 
   useEffect(() => {
+    setIsLoading(true);
     api
       .getImageRepos<ImageRepo[]>(
         "<token>",
@@ -168,27 +182,33 @@ const _ImageSelector = ({
         if (!value) {
           onChange(data[0].uri);
         }
+        setIsLoading(false);
       });
   }, []);
 
-  const imageOptions = images.map((image) => {
-    return {
-      value: image.uri,
-      label: image.name,
-    };
-  });
-
   const handleChange = (image: string) => {
     onChange(image);
   };
 
+  const displayName = (imageUrl: string) => {
+    const image = images.find((i) => i.uri === imageUrl);
+    if (!image) {
+      return imageUrl;
+    }
+    return image.name;
+  };
+
   return (
-    <SelectRow
-      label="Image"
+    <Select
       value={value}
-      options={imageOptions}
-      setActiveValue={handleChange}
-      width="100%"
+      options={images.map((image) => image.uri)}
+      accessor={displayName}
+      label="Image"
+      placeholder="Select an image"
+      onChange={handleChange}
+      isOptionEqualToValue={(a, b) => a === b}
+      readOnly={readOnly}
+      isLoading={isLoading}
     />
   );
 };
@@ -206,16 +226,20 @@ const _TagSelector = ({
   imageName,
   value,
   onChange,
+  readOnly,
 }: {
   registry: DockerRegistry;
   imageName: string;
   value: string;
   onChange: (newTag: string) => void;
+  readOnly: boolean;
 }) => {
   const { currentProject } = useContext(Context);
   const [imageTags, setImageTags] = useState<string[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
 
   useEffect(() => {
+    setIsLoading(true);
     api
       .getImageTags<DockerImageTag[]>(
         "<token>",
@@ -234,30 +258,24 @@ const _TagSelector = ({
         });
         setImageTags(sortedTags.map((tag) => tag.tag));
 
-        if (!value) {
-          onChange(sortedTags[0].tag);
-        }
+        setIsLoading(false);
       });
   }, [registry, imageName]);
 
-  const tagOptions = imageTags.map((tag) => {
-    return {
-      value: tag,
-      label: tag,
-    };
-  });
-
   const handleChange = (tag: string) => {
     onChange(tag);
   };
 
   return (
-    <SelectRow
-      label="Tag"
+    <Select
       value={value}
-      options={tagOptions}
-      setActiveValue={handleChange}
-      width="100%"
+      options={imageTags}
+      accessor={(tag) => tag}
+      label="Tag"
+      placeholder="Select a tag"
+      onChange={handleChange}
+      readOnly={readOnly}
+      isLoading={isLoading}
     />
   );
 };