Explorar o código

switch service selector from dropdown to tile (#4256)

d-g-town %!s(int64=2) %!d(string=hai) anos
pai
achega
593b18cf52

+ 127 - 0
dashboard/src/components/porter/Tiles.tsx

@@ -0,0 +1,127 @@
+import React from "react";
+import styled from "styled-components";
+
+import Container from "./Container";
+
+type Props = {
+  tileables: Tileable[];
+  onSelect: (value: string) => void;
+  selectedValue: string;
+  widthPercentage?: number;
+  gapPixels?: number;
+};
+
+type Tileable = {
+  icon?: string;
+  label: string;
+  value: string;
+  description?: string;
+};
+
+const Tiles: React.FC<Props> = ({
+  tileables,
+  onSelect,
+  selectedValue,
+  widthPercentage,
+}) => {
+  return (
+    <TilesWrapper>
+      <Container row spaced>
+        {tileables.map((tileable: Tileable) => {
+          const { label, value, icon, description } = tileable;
+          return (
+            <>
+              <Tile
+                key={value}
+                onClick={() => {
+                  onSelect(value);
+                }}
+                selected={selectedValue === value}
+                widthPercentage={widthPercentage}
+              >
+                {icon && <Icon src={icon} />}
+                <TileTitle>{label}</TileTitle>
+                {description && (
+                  <TileDescription>{description}</TileDescription>
+                )}
+              </Tile>
+            </>
+          );
+        })}
+      </Container>
+    </TilesWrapper>
+  );
+};
+
+export default Tiles;
+
+const Icon = styled.img`
+  height: 25px;
+  margin-top: 30px;
+`;
+
+const TileDescription = styled.div`
+  margin-bottom: 26px;
+  color: #ffffff99;
+  text-align: center;
+  font-weight: default;
+  padding: 0px 25px;
+  line-height: 1.4;
+  font-size: 12px;
+  display: -webkit-box;
+  overflow: hidden;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+`;
+
+const TileTitle = styled.div`
+  width: 80%;
+  text-align: center;
+  font-size: 14px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const Tile = styled.div<{ selected: boolean; widthPercentage?: number }>`
+  align-items: center;
+  ${(props) => props.widthPercentage && `width: ${props.widthPercentage}%`};
+  user-select: none;
+  display: flex;
+  font-size: 13px;
+  padding: 3px 0px 5px;
+  flex-direction: column;
+  align-item: center;
+  justify-content: space-between;
+  height: 180px;
+  cursor: pointer;
+  color: #ffffff;
+  position: relative;
+  border-radius: 5px;
+  background: ${(props) => props.theme.clickable.bg};
+  border: 1px solid ${(props) => (props.selected ? "#d7d1d1" : "#494b4f")};
+  :hover {
+    border: 1px solid #d7d1d1;
+  }
+
+  animation: fadeIn 0.3s 0s;
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const TilesWrapper = styled.div`
+  width: 100%;
+  overflow: visible;
+  margin-top: 15px;
+  padding-bottom: 25px;
+  display: grid;
+  grid-column-gap: 30px;
+  grid-row-gap: 30px;
+  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+`;

+ 34 - 57
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx

@@ -10,10 +10,8 @@ import styled from "styled-components";
 import { z } from "zod";
 
 import Button from "components/porter/Button";
-import Container from "components/porter/Container";
 import { ControlledInput } from "components/porter/ControlledInput";
 import Modal from "components/porter/Modal";
-import Select from "components/porter/Select";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import { type ClientServiceStatus } from "lib/hooks/useAppStatus";
@@ -30,6 +28,7 @@ import job from "assets/job.png";
 import web from "assets/web.png";
 import worker from "assets/worker.png";
 
+import Tiles from "../../../../../components/porter/Tiles";
 import ServiceContainer from "./ServiceContainer";
 
 const addServiceFormValidator = z.object({
@@ -121,7 +120,6 @@ const ServiceList: React.FC<ServiceListProps> = ({
         : "deletions.predeploy",
   });
 
-  const serviceType = watch("type");
   const serviceName = watch("name");
 
   const [showAddServiceModal, setShowAddServiceModal] =
@@ -252,38 +250,44 @@ const ServiceList: React.FC<ServiceListProps> = ({
           closeModal={() => {
             setShowAddServiceModal(false);
           }}
-          width="500px"
+          width="800px"
         >
           <Text size={16}>{addNewText}</Text>
           <Spacer y={1} />
           <Text color="helper">Select a service type:</Text>
           <Spacer y={0.5} />
-          <Container row>
-            <ServiceIcon>
-              {serviceType === "web" && <img src={web} />}
-              {serviceType === "worker" && <img src={worker} />}
-              {serviceType === "job" && <img src={job} />}
-            </ServiceIcon>
-            <Controller
-              name="type"
-              control={control}
-              render={({ field: { onChange } }) => (
-                <Select
-                  value={serviceType}
-                  width="100%"
-                  setValue={(value: string) => {
-                    onChange(value);
-                  }}
-                  options={[
-                    { label: "Web", value: "web" },
-                    { label: "Worker", value: "worker" },
-                    { label: "Cron Job", value: "job" },
-                  ]}
-                />
-              )}
-            />
-          </Container>
-          <Spacer y={1} />
+          <Controller
+            name="type"
+            control={control}
+            render={({ field: { onChange, value } }) => (
+              <Tiles
+                tileables={[
+                  {
+                    icon: web,
+                    label: "Web",
+                    value: "web",
+                    description: "Exposed to internal and/or external traffic",
+                  },
+                  {
+                    icon: worker,
+                    label: "Worker",
+                    value: "worker",
+                    description: "Long running background processes",
+                  },
+                  {
+                    icon: job,
+                    label: "Job",
+                    value: "job",
+                    description: "Scheduled tasks or one-off runs",
+                  },
+                ]}
+                onSelect={onChange}
+                selectedValue={value}
+                widthPercentage={30}
+                gapPixels={20}
+              />
+            )}
+          />
           <Text color="helper">Name this service:</Text>
           <Spacer y={0.5} />
           <ControlledInput
@@ -309,33 +313,6 @@ const ServiceList: React.FC<ServiceListProps> = ({
 
 export default ServiceList;
 
-const ServiceIcon = styled.div`
-  border: 1px solid #494b4f;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  height: 35px;
-  width: 35px;
-  min-width: 35px;
-  margin-right: 10px;
-  overflow: hidden;
-  border-radius: 5px;
-  > img {
-    height: 18px;
-    animation: floatIn 0.5s 0s;
-    @keyframes floatIn {
-      from {
-        opacity: 0;
-        transform: translateY(7px);
-      }
-      to {
-        opacity: 1;
-        transform: translateY(0px);
-      }
-    }
-  }
-`;
-
 const I = styled.i`
   color: white;
   font-size: 14px;