Kaynağa Gözat

Implement native tag selector instead of mui autocomplete

jnfrati 4 yıl önce
ebeveyn
işleme
b61a107e43

+ 215 - 0
dashboard/src/components/SearchSelector.tsx

@@ -0,0 +1,215 @@
+import _ from "lodash";
+import React, { useMemo, useState } from "react";
+import styled from "styled-components";
+
+type Props = {
+  options: any[];
+  onSelect: (option: any) => void;
+  label?: string;
+  dropdownLabel?: string;
+  getOptionLabel?: (option: any) => string;
+  filterBy?: ((option: any) => string) | string;
+  noOptionsText?: string;
+  dropdownMaxHeight?: string;
+  className?: string;
+  renderOptionIcon?: (option: any) => React.ReactNode;
+};
+
+const SearchSelector = ({
+  options,
+  onSelect,
+  label,
+  dropdownLabel,
+  getOptionLabel,
+  filterBy,
+  noOptionsText,
+  dropdownMaxHeight,
+  className,
+  renderOptionIcon,
+}: Props) => {
+  const [isExpanded, setIsExpanded] = useState(false);
+  const [filter, setFilter] = useState("");
+
+  const handleOptionClick = (e: any, option: any) => {
+    setIsExpanded(false);
+    onSelect(option);
+  };
+
+  const getLabel = (option: any) => {
+    if (typeof getOptionLabel === "function") {
+      return getOptionLabel(option);
+    }
+
+    return React.isValidElement(option) ? option : "";
+  };
+
+  const filteredOptions = useMemo(() => {
+    if (typeof filterBy === "function") {
+      return options.filter((option) => filterBy(option).includes(filter));
+    }
+
+    if (typeof filterBy === "string") {
+      return options.filter((option) =>
+        _.get(option, filterBy).includes(filter)
+      );
+    }
+
+    return options.filter((option) => option.includes(filter));
+  }, [filter, options]);
+
+  return (
+    <>
+      {label?.length ? <Label>{label}</Label> : null}
+      <InputWrapper
+        onBlur={() => {
+          setIsExpanded(false);
+        }}
+        className={className}
+      >
+        <Input
+          value={filter}
+          onClick={(e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            setIsExpanded(true);
+          }}
+          onChange={(e) => setFilter(e.target.value)}
+        />
+        {isExpanded ? (
+          <DropdownWrapper>
+            <Dropdown dropdownMaxHeight={dropdownMaxHeight}>
+              {!filteredOptions.length ? (
+                <DropdownLabel>
+                  {noOptionsText || "No options available for this filter"}
+                </DropdownLabel>
+              ) : (
+                <>
+                  {dropdownLabel && (
+                    <DropdownLabel>{dropdownLabel}</DropdownLabel>
+                  )}
+                  {filteredOptions.map((option, i) => (
+                    <Option
+                      key={i}
+                      onMouseDown={(e) => {
+                        e.stopPropagation();
+                        e.preventDefault();
+                      }}
+                      onClick={(e) => handleOptionClick(e, option)}
+                    >
+                      {typeof renderOptionIcon === "function"
+                        ? renderOptionIcon(option)
+                        : null}
+                      {getLabel(option)}
+                    </Option>
+                  ))}
+                </>
+              )}
+            </Dropdown>
+          </DropdownWrapper>
+        ) : null}
+      </InputWrapper>
+    </>
+  );
+};
+
+export default SearchSelector;
+
+const InputWrapper = styled.div`
+  display: flex;
+  margin-bottom: -1px;
+  align-items: center;
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  background: #ffffff11;
+  position: relative;
+  width: 100%;
+`;
+
+const Input = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  color: #ffffff;
+  padding: 5px 10px;
+  min-height: 35px;
+  max-height: 45px;
+  width: 100%;
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+`;
+
+const DropdownWrapper = styled.div`
+  position: absolute;
+  width: 100%;
+  right: 0;
+  z-index: 9999;
+  top: calc(100% + 5px);
+`;
+
+const Dropdown = styled.div`
+  background: #26282f;
+
+  max-height: ${(props: { dropdownMaxHeight: string }) =>
+    props.dropdownMaxHeight || "300px"};
+  border-radius: 3px;
+  z-index: 999;
+  overflow-y: auto;
+  margin-bottom: 20px;
+  box-shadow: 0 8px 20px 0px #00000088;
+`;
+
+const DropdownLabel = styled.div`
+  font-size: 13px;
+  color: #ffffff44;
+  font-weight: 500;
+  margin: 10px 13px;
+`;
+
+const Option = styled.div`
+  width: 100%;
+  border-top: 1px solid #00000000;
+  border-bottom: 1px solid #ffffff15;
+  min-height: 35px;
+  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;
+
+  :last-child {
+    border-bottom: 1px solid #ffffff00;
+  }
+
+  :hover {
+    background: #ffffff22;
+  }
+`;
+
+const Icon = styled.div`
+  height: 20px;
+  width: 30px;
+  margin-left: -5px;
+  margin-right: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: visible;
+
+  > img {
+    height: 18px;
+    width: auto;
+  }
+`;

+ 2 - 5
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -21,7 +21,7 @@ import TagSelector from "./TagSelector";
 
 type PropsType = {
   currentChart: ChartType;
-  refreshChart: () => void;
+  refreshChart: () => Promise<void>;
   setShowDeleteOverlay: (x: boolean) => void;
   saveButtonText?: string | null;
 };
@@ -218,10 +218,7 @@ const SettingsSection: React.FC<PropsType> = ({
     return (
       <>
         <Heading>Application tags</Heading>
-        <TagSelector
-          release={currentChart}
-          onSave={(val) => console.log(val)}
-        />
+        <TagSelector release={currentChart} onSave={(val) => refreshChart()} />
         {!isDeployedFromGithub(currentChart) ? (
           <>
             <Heading>Source Settings</Heading>

+ 38 - 51
dashboard/src/main/home/cluster-dashboard/expanded-chart/TagSelector.tsx

@@ -1,5 +1,4 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
-import { Autocomplete as MaterialAutocomplete } from "@material-ui/lab";
 import styled from "styled-components";
 import { Tooltip } from "@material-ui/core";
 import Modal from "main/home/modals/Modal";
@@ -11,9 +10,10 @@ import { Context } from "shared/Context";
 import { ChartType } from "shared/types";
 import Helper from "components/form-components/Helper";
 import { differenceBy } from "lodash";
+import SearchSelector from "components/SearchSelector";
 
 type Props = {
-  onSave: (values: any[]) => void;
+  onSave: ((values: any[]) => void) | ((values: any[]) => Promise<void>);
   release: ChartType;
 };
 
@@ -29,7 +29,8 @@ const TagSelector = ({ onSave, release }: Props) => {
   const onDelete = (index: number) => {
     setValues((prev) => {
       const newValues = [...prev];
-      newValues.splice(index, 1);
+      const removedTag = newValues.splice(index, 1);
+      setAvailableTags((prevAt) => [...prevAt, ...removedTag]);
       return newValues;
     });
   };
@@ -40,7 +41,7 @@ const TagSelector = ({ onSave, release }: Props) => {
     try {
       await api.updateReleaseTags(
         "<token>",
-        { tags: [...(release.tags || []), name] },
+        { tags: [...values.map((tag) => tag.name)] },
         {
           project_id: currentProject.id,
           cluster_id: currentCluster.id,
@@ -48,7 +49,7 @@ const TagSelector = ({ onSave, release }: Props) => {
           release_name: release.name,
         }
       );
-      onSave(values);
+      await onSave(values);
       setButtonStatus("successful");
     } catch (error) {
       console.log(error);
@@ -76,20 +77,26 @@ const TagSelector = ({ onSave, release }: Props) => {
           release.tags?.includes(tag.name)
         );
         const tmpAvailableTags = differenceBy(data, releaseTags, "name");
-        debugger;
+
         setValues(releaseTags);
         setAvailableTags(tmpAvailableTags);
       });
   }, [currentProject]);
 
   const hasUnsavedChanges = useMemo(() => {
-    const difference = differenceBy(
+    const hasAddedSomething = !!differenceBy(
       values,
       release.tags?.map((tagName: string) => ({ name: tagName })) || [],
       "name"
-    );
+    ).length;
+
+    const hasDeletedSomething = !!differenceBy(
+      release.tags?.map((tagName: string) => ({ name: tagName })) || [],
+      values,
+      "name"
+    ).length;
 
-    return !!difference.length;
+    return hasAddedSomething || hasDeletedSomething;
   }, [values, release]);
 
   return (
@@ -107,30 +114,22 @@ const TagSelector = ({ onSave, release }: Props) => {
           release={release}
         />
       ) : null}
+
       <Flex>
-        <MaterialAutocomplete
-          fullWidth
-          filterSelectedOptions
-          options={availableTags.filter(
-            (option) => !values.find((v) => v.name === option.name)
-          )}
-          onChange={(_, value) => {
+        <SearchSelector
+          options={availableTags}
+          dropdownLabel="Select a tag"
+          filterBy="name"
+          onSelect={(value) => {
+            console.log(value);
+            setAvailableTags((prev) =>
+              prev.filter((prevVal) => prevVal.name !== value.name)
+            );
             setValues((prev) => [...prev, value]);
           }}
           getOptionLabel={(option) => option.name}
-          getOptionSelected={(option, value) => option.name === value}
-          renderInput={(params) => {
-            console.log(params);
-            return (
-              <>
-                <InputWrapper ref={params.InputProps.ref}>
-                  <Input {...params.inputProps} />
-                </InputWrapper>
-                {params.InputProps.startAdornment}
-              </>
-            );
-          }}
-        ></MaterialAutocomplete>
+          renderOptionIcon={(option) => <TagColorBox color={option.color} />}
+        ></SearchSelector>
         <Tooltip title="Create a new tag">
           <AddButton
             className="material-icons-outlined"
@@ -164,7 +163,7 @@ const TagSelector = ({ onSave, release }: Props) => {
           text="Save changes"
           onClick={() => handleSave()}
           status={buttonStatus}
-          disabled={!hasUnsavedChanges}
+          disabled={!hasUnsavedChanges || buttonStatus === "loading"}
         ></SaveButton>
       </Flex>
     </>
@@ -321,7 +320,7 @@ const Tag = styled.div<{ color: string }>`
   > .material-icons {
     font-size: 20px;
     margin-left: 5px;
-    filter: invert(1);
+    mix-blend-mode: difference;
     :hover {
       cursor: pointer;
     }
@@ -336,26 +335,6 @@ const TagText = styled.span`
   text-overflow: ellipsis;
 `;
 
-const InputWrapper = styled.div`
-  display: flex;
-  margin-bottom: -1px;
-  align-items: center;
-  border: 1px solid #ffffff55;
-  border-radius: 3px;
-  background: #ffffff11;
-`;
-
-const Input = styled.input`
-  outline: none;
-  border: none;
-  font-size: 13px;
-  background: none;
-  color: #ffffff;
-  padding: 5px 10px;
-  min-height: 35px;
-  max-height: 45px;
-`;
-
 const Label = styled.div`
   color: #ffffff;
   margin-bottom: 10px;
@@ -364,3 +343,11 @@ const Label = styled.div`
   font-size: 13px;
   font-family: "Work Sans", sans-serif;
 `;
+
+const TagColorBox = styled.div`
+  width: 20px;
+  height: 20px;
+  margin-right: 5px;
+  border-radius: 5px;
+  background-color: ${(props: { color: string }) => props.color};
+`;