فهرست منبع

Merge pull request #1840 from porter-dev/nico/implement-job-description

[Improvement] Job description and next run info for cronjobs
abelanger5 4 سال پیش
والد
کامیت
a59d9c3b7c

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 14068
dashboard/package-lock.json


+ 1 - 0
dashboard/package.json

@@ -25,6 +25,7 @@
     "clipboard": "^2.0.8",
     "cohere-js": "^1.0.19",
     "core-js": "^3.16.1",
+    "cron-parser": "^4.3.0",
     "cron-validator": "^1.3.1",
     "cronstrue": "^2.2.0",
     "d3-array": "^2.11.0",

+ 4 - 0
dashboard/src/components/porter-form/PorterForm.tsx

@@ -10,6 +10,7 @@ import {
   Section,
   SelectField,
   ServiceIPListField,
+  TextAreaField,
 } from "./types";
 import TabRegion, { TabOption } from "../TabRegion";
 import Heading from "../form-components/Heading";
@@ -26,6 +27,7 @@ import ServiceIPList from "./field-components/ServiceIPList";
 import ResourceList from "./field-components/ResourceList";
 import VeleroForm from "./field-components/VeleroForm";
 import CronInput from "./field-components/CronInput";
+import TextAreaInput from "./field-components/TextAreaInput";
 
 interface Props {
   leftTabOptions?: TabOption[];
@@ -88,6 +90,8 @@ const PorterForm: React.FC<Props> = (props) => {
         return <VeleroForm />;
       case "cron":
         return <CronInput {...(bundledProps as CronField)} />;
+      case "text-area":
+        return <TextAreaInput {...(bundledProps as TextAreaField)} />;
     }
     return <p>Not Implemented: {(field as any).type}</p>;
   };

+ 128 - 0
dashboard/src/components/porter-form/field-components/TextAreaInput.tsx

@@ -0,0 +1,128 @@
+import { Tooltip } from "@material-ui/core";
+import React from "react";
+import styled from "styled-components";
+import useFormField from "../hooks/useFormField";
+import { StringInputFieldState, TextAreaField } from "../types";
+import { hasSetValue } from "../utils";
+
+const TextAreaInput: React.FC<TextAreaField> = (props) => {
+  const {
+    id,
+    variable,
+    label,
+    info,
+    placeholder,
+    required,
+    settings,
+    isReadOnly,
+    value,
+  } = props;
+
+  const { state, variables, setVars } = useFormField<StringInputFieldState>(
+    id,
+    {
+      initVars: {
+        [variable]: hasSetValue(props) ? value[0] : undefined,
+      },
+    }
+  );
+
+  if (!state) {
+    return null;
+  }
+
+  return (
+    <div>
+      {label || info ? (
+        <Label>
+          {label}
+          {info && (
+            <Tooltip
+              title={
+                <div
+                  style={{
+                    fontFamily: "Work Sans, sans-serif",
+                    fontSize: "12px",
+                    fontWeight: "normal",
+                    padding: "5px 6px",
+                  }}
+                >
+                  {info}
+                </div>
+              }
+              placement="top"
+            >
+              <StyledInfoTooltip>
+                <i className="material-icons">help_outline</i>
+              </StyledInfoTooltip>
+            </Tooltip>
+          )}
+          {required && <Required>{" *"}</Required>}
+        </Label>
+      ) : null}
+      <TextArea
+        maxLength={settings?.options?.maxCount}
+        minLength={settings?.options?.minCount}
+        disabled={isReadOnly}
+        value={variables[variable]}
+        placeholder={placeholder}
+        onChange={(e) => {
+          e?.persist();
+          setVars((prev) => {
+            return {
+              ...prev,
+              [variable]: e?.target?.value,
+            };
+          });
+        }}
+      ></TextArea>
+    </div>
+  );
+};
+
+export default TextAreaInput;
+
+const TextArea = styled.textarea`
+  width: 100%;
+  max-width: 100%;
+  min-height: 150px;
+  height: auto;
+  max-height: 300px;
+  background: #ffffff11;
+  color: #ffffff;
+  border-radius: 5px;
+  padding: 10px;
+  cursor: ${(props: { disabled: boolean }) =>
+    props.disabled ? "not-allowed" : ""};
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+`;
+
+const StyledInfoTooltip = styled.div`
+  display: inline-block;
+  position: relative;
+  margin-right: 2px;
+  > i {
+    display: flex;
+    align-items: center;
+    font-size: 16px;
+    margin-left: 5px;
+    color: #858faaaa;
+    cursor: pointer;
+    :hover {
+      color: #aaaabb;
+    }
+  }
+`;
+
+const Required = styled.div`
+  margin-left: 8px;
+  color: #fc4976;
+`;

+ 16 - 1
dashboard/src/components/porter-form/types.ts

@@ -128,6 +128,20 @@ export interface CronField extends GenericInputField {
   };
 }
 
+export interface TextAreaField extends GenericInputField {
+  type: "text-area";
+  label: string;
+  placeholder: string;
+  info: string;
+  settings: {
+    default?: string;
+    options?: {
+      maxCount?: number;
+      minCount?: number;
+    };
+  };
+}
+
 export type FormField =
   | HeadingField
   | SubtitleField
@@ -140,7 +154,8 @@ export type FormField =
   | ResourceListField
   | VeleroBackupField
   | VariableField
-  | CronField;
+  | CronField
+  | TextAreaField;
 
 export interface ShowIfAnd {
   and: ShowIf[];

+ 2 - 0
dashboard/src/main/home/cluster-dashboard/ClusterDashboard.tsx

@@ -177,6 +177,7 @@ class ClusterDashboard extends Component<PropsType, StateType> {
             <SortSelector
               setSortType={(sortType) => this.setState({ sortType })}
               sortType={this.state.sortType}
+              currentView={currentView}
             />
           </SortFilterWrapper>
         </ControlRow>
@@ -247,6 +248,7 @@ class ClusterDashboard extends Component<PropsType, StateType> {
             <SortSelector
               setSortType={(sortType) => this.setState({ sortType })}
               sortType={this.state.sortType}
+              currentView={currentView}
             />
           </SortFilterWrapper>
         </ControlRow>

+ 20 - 1
dashboard/src/main/home/cluster-dashboard/SortSelector.tsx

@@ -8,6 +8,7 @@ import Selector from "components/Selector";
 type PropsType = {
   setSortType: (x: string) => void;
   sortType: string;
+  currentView: string;
 };
 
 type StateType = {
@@ -21,9 +22,27 @@ export default class SortSelector extends Component<PropsType, StateType> {
       { label: "Newest", value: "Newest" },
       { label: "Oldest", value: "Oldest" },
       { label: "Alphabetical", value: "Alphabetical" },
+      { label: "Next Run", value: "Next Run" },
     ] as { label: string; value: string }[],
   };
 
+  getSortOptions() {
+    if (this.props.currentView === "jobs") {
+      return [
+        { label: "Newest", value: "Newest" },
+        { label: "Oldest", value: "Oldest" },
+        { label: "Alphabetical", value: "Alphabetical" },
+        { label: "Next Run", value: "Next Run" },
+      ];
+    }
+
+    return [
+      { label: "Newest", value: "Newest" },
+      { label: "Oldest", value: "Oldest" },
+      { label: "Alphabetical", value: "Alphabetical" },
+    ];
+  }
+
   render() {
     return (
       <StyledSortSelector>
@@ -33,7 +52,7 @@ export default class SortSelector extends Component<PropsType, StateType> {
         <Selector
           activeValue={this.props.sortType}
           setActiveValue={(sortType) => this.props.setSortType(sortType)}
-          options={this.state.sortOptions}
+          options={this.getSortOptions()}
           dropdownLabel="Sort By"
           width="150px"
           dropdownWidth="230px"

+ 82 - 0
dashboard/src/main/home/cluster-dashboard/chart/Chart.tsx

@@ -13,6 +13,14 @@ import StatusIndicator from "components/StatusIndicator";
 import { pushFiltered } from "shared/routing";
 import api from "shared/api";
 import { readableDate } from "shared/string_utils";
+import { Tooltip, Zoom } from "@material-ui/core";
+import CronParser from "cron-parser";
+
+import {
+  createTheme,
+  MuiThemeProvider,
+  withStyles,
+} from "@material-ui/core/styles";
 
 type Props = {
   chart: ChartType;
@@ -22,6 +30,17 @@ type Props = {
   closeChartRedirectUrl?: string;
 };
 
+const theme = createTheme({
+  overrides: {
+    MuiTooltip: {
+      tooltip: {
+        backgroundColor: "#3E3F44",
+        border: "1px solid #ffffff33",
+      },
+    },
+  },
+});
+
 const Chart: React.FunctionComponent<Props> = ({
   chart,
   controllers,
@@ -31,6 +50,7 @@ const Chart: React.FunctionComponent<Props> = ({
 }) => {
   const [expand, setExpand] = useState<boolean>(false);
   const [chartControllers, setChartControllers] = useState<any>([]);
+  const [showDescription, setShowDescription] = useState(false);
   const context = useContext(Context);
   const location = useLocation();
   const history = useHistory();
@@ -83,6 +103,21 @@ const Chart: React.FunctionComponent<Props> = ({
     return tmpControllers;
   }, [chartControllers, controllers]);
 
+  let interval = null;
+  if (chart?.config?.schedule?.enabled) {
+    interval = CronParser.parseExpression(chart?.config?.schedule.value, {
+      currentDate: new Date(),
+    });
+  }
+
+  // @ts-ignore
+  const rtf = new Intl.DateTimeFormat("en", {
+    localeMatcher: "best fit", // other values: "lookup"
+    // @ts-ignore
+    dateStyle: "full",
+    timeStyle: "long",
+  });
+
   return (
     <StyledChart
       onMouseEnter={() => setExpand(true)}
@@ -101,6 +136,33 @@ const Chart: React.FunctionComponent<Props> = ({
       <Title>
         <IconWrapper>{renderIcon()}</IconWrapper>
         {chart.name}
+        {chart?.config?.description && (
+          <>
+            <Dot style={{ marginLeft: "9px", color: "#ffffff88" }}>•</Dot>
+            <MuiThemeProvider theme={theme}>
+              <Tooltip
+                TransitionComponent={Zoom}
+                placement={"bottom-start"}
+                title={
+                  <div
+                    style={{
+                      fontFamily: "Work Sans, sans-serif",
+                      fontSize: "12px",
+                      fontWeight: "normal",
+                      padding: "5px 6px",
+                      color: "#ffffffdd",
+                      lineHeight: "16px",
+                    }}
+                  >
+                    {chart.config.description as string}
+                  </div>
+                }
+              >
+                <Description>{chart.config.description}</Description>
+              </Tooltip>
+            </MuiThemeProvider>
+          </>
+        )}
       </Title>
 
       <BottomWrapper>
@@ -129,6 +191,14 @@ const Chart: React.FunctionComponent<Props> = ({
                 </JobStatus>
               </>
             )}
+            {chart.config?.schedule?.enabled ? (
+              <>
+                <Dot style={{ marginLeft: "10px" }}>•</Dot>
+                <JobStatus>
+                  Next run {rtf.format(interval?.next().toDate() || new Date())}
+                </JobStatus>
+              </>
+            ) : null}
           </LastDeployed>
         </InfoWrapper>
 
@@ -147,6 +217,17 @@ const Chart: React.FunctionComponent<Props> = ({
 
 export default Chart;
 
+const Description = styled.div`
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  max-width: 80%;
+  color: #ffffff88;
+  position: relative;
+  font-size: 13px;
+  padding-top: 1px;
+`;
+
 const BottomWrapper = styled.div`
   display: flex;
   justify-content: space-between;
@@ -244,6 +325,7 @@ const IconWrapper = styled.div`
 `;
 
 const Title = styled.div`
+  display: flex;
   position: relative;
   text-decoration: none;
   padding: 12px 35px 12px 45px;

+ 36 - 0
dashboard/src/main/home/cluster-dashboard/chart/ChartList.tsx

@@ -16,6 +16,7 @@ import { PorterUrl } from "shared/routing";
 import Chart from "./Chart";
 import Loading from "components/Loading";
 import { useWebsockets } from "shared/hooks/useWebsockets";
+import CronParser from "cron-parser";
 
 type Props = {
   currentCluster: ClusterType;
@@ -360,6 +361,41 @@ const ChartList: React.FunctionComponent<Props> = ({
       );
     } else if (sortType == "Alphabetical") {
       result.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
+    } else if (sortType == "Next Run" && currentView === "jobs") {
+      const cronJobs = result.filter(
+        (chart) => chart?.config?.schedule?.enabled
+      );
+      const nonCronJobs = result.filter(
+        (chart) => !chart?.config?.schedule?.enabled
+      );
+      cronJobs.sort((a: any, b: any) => {
+        let firstInterval = null;
+        if (a?.config?.schedule?.enabled) {
+          firstInterval = CronParser.parseExpression(
+            a?.config?.schedule.value,
+            {
+              currentDate: new Date(),
+            }
+          );
+        }
+
+        let secondInterval = null;
+        if (b?.config?.schedule?.enabled) {
+          secondInterval = CronParser.parseExpression(
+            b?.config?.schedule.value,
+            {
+              currentDate: new Date(),
+            }
+          );
+        }
+
+        return Date.parse(firstInterval.next().toISOString()) >
+          Date.parse(secondInterval.next().toISOString())
+          ? 1
+          : -1;
+      });
+
+      return [...cronJobs, ...nonCronJobs];
     }
 
     return result;

+ 90 - 15
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -25,6 +25,8 @@ import { useChart } from "shared/hooks/useChart";
 import Modal from "main/home/modals/Modal";
 import ConnectToJobInstructionsModal from "./jobs/ConnectToJobInstructionsModal";
 import CommandLineIcon from "assets/command-line-icon";
+import CronParser from "cron-parser";
+import CronPrettifier from "cronstrue";
 
 const readableDate = (s: string) => {
   let ts = new Date(s);
@@ -131,10 +133,28 @@ export const ExpandedJobChartFC: React.FC<{
       );
     }
 
+    let interval = null;
+    if (chart?.config?.schedule.enabled) {
+      interval = CronParser.parseExpression(chart?.config?.schedule.value, {
+        currentDate: new Date(),
+      });
+    }
+    // @ts-ignore
+    const rtf = new Intl.DateTimeFormat("en", {
+      localeMatcher: "best fit", // other values: "lookup"
+      // @ts-ignore
+      dateStyle: "full",
+      timeStyle: "long",
+    });
+
     if (currentTab === "jobs") {
       return (
         <TabWrapper>
-          <ButtonWrapper>
+          <ButtonWrapper
+            style={{
+              marginBottom: chart?.config?.schedule?.enabled ? "0px" : "35px",
+            }}
+          >
             <SaveButton
               onClick={() => {
                 runJob();
@@ -158,20 +178,41 @@ export const ExpandedJobChartFC: React.FC<{
             </CLIModalIconWrapper>
           </ButtonWrapper>
 
+          {chart?.config?.schedule?.enabled ? (
+            <RunsDescription>
+              <i className="material-icons">access_time</i>
+              Runs{" "}
+              {CronPrettifier.toString(
+                chart?.config?.schedule.value
+              ).toLowerCase()}
+              <Dot
+                style={{
+                  color: "#ffffff88",
+                }}
+              >
+                •
+              </Dot>{" "}
+              Next run on
+              {" " + rtf.format(interval.next().toDate())}
+            </RunsDescription>
+          ) : null}
+
           {jobsStatus === "loading" ? (
             <Loading></Loading>
           ) : (
-            <JobList
-              jobs={jobs}
-              setJobs={() => {}}
-              expandJob={(job: any) => {
-                setSelectedJob(job);
-              }}
-              isDeployedFromGithub={!!chart?.git_action_config?.git_repo}
-              repositoryUrl={chart?.git_action_config?.git_repo}
-              currentChartVersion={Number(chart.version)}
-              latestChartVersion={Number(chart.latest_version)}
-            />
+            <>
+              <JobList
+                jobs={jobs}
+                setJobs={() => {}}
+                expandJob={(job: any) => {
+                  setSelectedJob(job);
+                }}
+                isDeployedFromGithub={!!chart?.git_action_config?.git_repo}
+                repositoryUrl={chart?.git_action_config?.git_repo}
+                currentChartVersion={Number(chart.version)}
+                latestChartVersion={Number(chart.latest_version)}
+              />
+            </>
           )}
         </TabWrapper>
       );
@@ -345,6 +386,9 @@ const ExpandedJobHeader: React.FC<{
         Namespace <NamespaceTag>{chart.namespace}</NamespaceTag>
       </TagWrapper>
     </TitleSection>
+    {chart?.config?.description ? (
+      <Description>{chart?.config?.description}</Description>
+    ) : null}
 
     <InfoWrapper>
       <LastDeployed>
@@ -377,6 +421,37 @@ const ExpandedJobHeader: React.FC<{
   </HeaderWrapper>
 );
 
+const RunsDescription = styled.div`
+  color: #ffffff;
+  font-size: 13px;
+  margin-top: 20px;
+  margin-bottom: 20px;
+  display: flex;
+  align-items: center;
+  padding: 14px 20px;
+  background: #2b2e36;
+  border: 1px solid #ffffff22;
+  color: #ffffffdd;
+  border-radius: 4px;
+  user-select: text;
+
+  > i {
+    font-size: 16px;
+    color: #ffffffdd;
+    margin-right: 10px;
+  }
+`;
+
+const Description = styled.div`
+  user-select: text;
+  font-size: 13px;
+  margin-left: 0;
+  display: flex;
+  align-items: center;
+  color: #ffffffdd;
+  line-height: 150%;
+`;
+
 const CLIModalIconWrapper = styled.div`
   height: 35px;
   font-size: 13px;
@@ -425,7 +500,7 @@ const LineBreak = styled.div`
 
 const ButtonWrapper = styled.div`
   display: flex;
-  margin: 5px 0 35px;
+  margin: 5px 0 0 0;
   justify-content: space-between;
 `;
 const BackButton = styled.div`
@@ -507,9 +582,9 @@ const Dot = styled.div`
 
 const InfoWrapper = styled.div`
   display: flex;
-  align-items: center;
+  flex-direction: column;
+  justify-content: center;
   margin: 24px 0px 17px 0px;
-  height: 20px;
 `;
 
 const LastDeployed = styled.div`

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

@@ -109,6 +109,10 @@ export interface ChartTypeWithExtendedConfig extends ChartType {
     showStartCommand: boolean;
     statefulset: { enabled: boolean };
     terminationGracePeriodSeconds: number;
+    schedule: {
+      enabled: boolean;
+      value: string;
+    };
   };
 }
 

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است