Просмотр исходного кода

Start implementation of autocomplete for adding tags

jnfrati 4 лет назад
Родитель
Сommit
039468e573

+ 37 - 0
api/server/handlers/project/get_tags.go

@@ -0,0 +1,37 @@
+package project
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type GetTagsHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewGetTagsHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetTagsHandler {
+	return &GetTagsHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *GetTagsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	tags, err := p.Config().Repo.Tag().ListTagsByProjectId(proj.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	}
+
+	p.WriteResult(w, r, tags)
+}

+ 27 - 0
api/server/router/project.go

@@ -946,5 +946,32 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/tags -> project.NewGetTagsHandler
+	getTagsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/tags",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getTagsHandler := project.NewGetTagsHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getTagsEndpoint,
+		Handler:  getTagsHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

Разница между файлами не показана из-за своего большого размера
+ 14115 - 1
dashboard/package-lock.json


+ 1 - 0
dashboard/package.json

@@ -6,6 +6,7 @@
     "@ironplans/react": "^0.4.0",
     "@loadable/component": "^5.15.2",
     "@material-ui/core": "^4.11.3",
+    "@material-ui/lab": "^4.0.0-alpha.61",
     "@sentry/react": "^6.13.2",
     "@sentry/tracing": "^6.13.2",
     "@visx/axis": "^1.6.1",

+ 119 - 0
dashboard/src/components/Autocomplete.tsx

@@ -0,0 +1,119 @@
+import React, { useEffect, useState } from "react";
+import { Autocomplete as MaterialAutocomplete } from "@material-ui/lab";
+import styled from "styled-components";
+
+type Props = {
+  options: any[];
+  defaultValue: any[];
+  onChange: (values: any[]) => void;
+};
+
+const Autocomplete = ({ options, defaultValue, onChange }: Props) => {
+  const [values, setValues] = useState(() => defaultValue || []);
+
+  useEffect(() => {
+    onChange(values);
+  }, [values]);
+
+  return (
+    <MaterialAutocomplete
+      multiple
+      filterSelectedOptions
+      options={options}
+      onChange={(_, value, type, details) => {
+        if (type === "create-option") {
+          value.splice(value.length - 1, 1);
+          setValues([...value, { name: details.option }]);
+          return;
+        }
+        setValues(value);
+      }}
+      value={values}
+      getOptionLabel={(option) => option.name}
+      renderTags={(values, getChipProps) => {
+        return values.map((val, index) => {
+          // @ts-ignore
+          const { onDelete, ...chipProps } = getChipProps({ index });
+
+          return (
+            <Tag {...chipProps} color={val.color}>
+              <TagText>{val.name}</TagText>
+              <i
+                aria-role={"button"}
+                className="material-icons"
+                onClick={onDelete}
+              >
+                delete
+              </i>
+            </Tag>
+          );
+        });
+      }}
+      renderInput={(params) => {
+        console.log(params);
+        return (
+          <>
+            <InputWrapper ref={params.InputProps.ref}>
+              <Input {...params.inputProps} />
+            </InputWrapper>
+            {params.InputProps.startAdornment}
+          </>
+        );
+      }}
+    ></MaterialAutocomplete>
+  );
+};
+
+export default Autocomplete;
+
+const Tag = styled.div<{ color: string }>`
+  color: ${(props) => props.color || "inherit"};
+  user-select: none;
+  border: 1px solid black;
+  border-radius: 15px;
+  padding: 5px 10px;
+  text-align: center;
+  display: flex;
+  align-items: center;
+  font-size: 14px;
+  background-color: ${(props) => props.color || "inherit"};
+  margin-left: 10px;
+  margin-top: 5px;
+  margin-bottom: 5px;
+
+  > .material-icons {
+    font-size: 20px;
+    margin-left: 5px;
+    filter: invert(1);
+    :hover {
+      cursor: pointer;
+    }
+  }
+`;
+
+const TagText = styled.span`
+  filter: invert(1);
+`;
+
+const InputWrapper = styled.div`
+  display: flex;
+  margin-bottom: -1px;
+  align-items: center;
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  background: #ffffff11;
+
+  ${Tag} {
+  }
+`;
+
+const Input = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  color: #ffffff;
+  padding: 5px 10px;
+  min-height: 35px;
+  max-height: 45px;
+`;

+ 28 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/SettingsSection.tsx

@@ -16,6 +16,9 @@ import useAuth from "shared/auth/useAuth";
 import Loading from "components/Loading";
 import NotificationSettingsSection from "./NotificationSettingsSection";
 import { Link } from "react-router-dom";
+import Autocomplete from "components/Autocomplete";
+// import { Autocomplete, AutocompleteGetTagProps } from "@material-ui/lab";
+// import TextField from "@material-ui/core/TextField";
 
 type PropsType = {
   currentChart: ChartType;
@@ -53,6 +56,10 @@ const SettingsSection: React.FC<PropsType> = ({
   const { currentCluster, currentProject, setCurrentError } = useContext(
     Context
   );
+
+  const [fullTagList, setFullTagList] = useState([]);
+  const [selectedTags, setSelectedTags] = useState([]);
+
   const [isAuthorized] = useAuth();
 
   useEffect(() => {
@@ -84,9 +91,19 @@ const SettingsSection: React.FC<PropsType> = ({
       .catch(console.log)
       .finally(() => setLoadingWebhookToken(false));
 
-    return () => (isSubscribed = false);
+    return () => {
+      isSubscribed = false;
+    };
   }, [currentChart, currentCluster, currentProject]);
 
+  useEffect(() => {
+    api
+      .getTagsByProjectId("<token>", {}, { project_id: currentProject.id })
+      .then(({ data }) => {
+        setFullTagList(data);
+      });
+  }, [currentProject]);
+
   const handleSubmit = async () => {
     setSaveValuesStatus("loading");
 
@@ -213,6 +230,16 @@ const SettingsSection: React.FC<PropsType> = ({
     return (
       <>
         <>
+          <Heading>Application tags</Heading>
+          <Autocomplete
+            defaultValue={
+              currentChart.tags?.map((tagName: string) => ({
+                name: tagName,
+              })) || []
+            }
+            onChange={(value) => setSelectedTags(value)}
+            options={fullTagList}
+          />
           <Heading>Source Settings</Heading>
           <Helper>Specify an image tag to use.</Helper>
           <ImageSelector

+ 7 - 5
dashboard/src/shared/api.tsx

@@ -370,11 +370,7 @@ const deletePRDeployment = baseApi<
     deployment_id: number;
   }
 >("DELETE", (pathParams) => {
-  const {
-    cluster_id,
-    project_id,
-    deployment_id,
-  } = pathParams;
+  const { cluster_id, project_id, deployment_id } = pathParams;
   return `/api/projects/${project_id}/clusters/${cluster_id}/deployments/${deployment_id}`;
 });
 
@@ -1757,6 +1753,11 @@ const triggerPreviewEnvWorkflow = baseApi<
     `/api/projects/${project_id}/clusters/${cluster_id}/deployments/${deployment_id}/trigger_workflow`
 );
 
+const getTagsByProjectId = baseApi<{}, { project_id: number }>(
+  "GET",
+  ({ project_id }) => `/api/projects/${project_id}/tags`
+);
+
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
   checkAuth,
@@ -1921,4 +1922,5 @@ export default {
   updateBuildConfig,
   reRunGHWorkflow,
   triggerPreviewEnvWorkflow,
+  getTagsByProjectId,
 };

+ 1 - 0
dashboard/src/shared/types.tsx

@@ -49,6 +49,7 @@ export interface ChartType {
   version: number;
   namespace: string;
   latest_version: string;
+  tags: any;
 }
 
 export interface ChartTypeWithExtendedConfig extends ChartType {

+ 1 - 3
internal/repository/gorm/tag.go

@@ -106,9 +106,7 @@ func (repo *TagRepository) ReadTagByNameAndProjectId(tagName string, projectId u
 func (repo *TagRepository) ListTagsByProjectId(projectId uint) ([]*models.Tag, error) {
 	tags := make([]*models.Tag, 0)
 
-	err := repo.db.Model(&models.Tag{}).Where("project_id = ?", projectId).Preload("Releases", func(tx *gorm.DB) *gorm.DB {
-		return tx.Select("Name")
-	}).Find(&tags).Error
+	err := repo.db.Model(&models.Tag{}).Where("project_id = ?", projectId).Preload("Releases").Find(&tags).Error
 
 	if err != nil {
 		return nil, err

Некоторые файлы не были показаны из-за большого количества измененных файлов