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

Show release runs (#3008)

* job runs table

* temp job runs table for release

---------

Co-authored-by: Justin Rhee <jusrhee@Justins-MacBook-Air.local>
jusrhee 3 лет назад
Родитель
Сommit
d921205f82

+ 1 - 0
dashboard/src/components/OldPlaceholder.tsx

@@ -31,4 +31,5 @@ const StyledPlaceholder = styled.div<{
   color: #ffffff44;
   border-radius: 5px;
   background: ${props => props.theme.fg};
+  border: 1px solid ${props => props.theme.border};
 `;

+ 100 - 0
dashboard/src/main/home/app-dashboard/expanded-app/ActivityFeed.tsx

@@ -0,0 +1,100 @@
+import React, { useEffect, useState, useContext } from "react";
+import styled from "styled-components";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+
+import Text from "components/porter/Text";
+import Container from "components/porter/Container";
+import VerticalSteps from "components/porter/VerticalSteps";
+
+type Props = {
+  chart: any;
+};
+
+const dummyEvents = [
+  {
+    type: "build",
+  },
+  {
+    type: "deploy",
+  }
+]
+
+const ActivityFeed: React.FC<Props> = ({
+  chart,
+}) => {
+  const { currentProject, currentCluster } = useContext(Context);
+
+  useEffect(() => {
+    // Do something
+  }, []);
+
+  const renderEvent = (event: any) => {
+    return (
+      <EventCard>
+        some event
+      </EventCard>
+    )
+  };
+
+  return (
+    <StyledActivityFeed>
+      {dummyEvents.map((event, i) => {
+        return (
+          <EventWrapper 
+            isLast={i === dummyEvents.length - 1} 
+            key={i}
+          >
+            {(i !== dummyEvents.length - 1) && <Line />}
+            <Dot />
+            {renderEvent(event)}
+          </EventWrapper>
+        );
+      })}
+    </StyledActivityFeed>
+  );
+};
+
+export default ActivityFeed;
+
+const EventCard = styled.div`
+  width: 100%;
+  padding: 20px;
+  border-radius: 5px;
+  background: ${({ theme }) => theme.fg};
+  border: 1px solid ${({ theme }) => theme.border};
+`;
+
+const Line = styled.div`
+  width: 1px;
+  height: calc(100% + 35px);
+  background: #414141;
+  position: absolute;
+  left: 3px;
+  top: 8px;
+  opacity: 1;
+`;
+
+const Dot = styled.div`
+  width: 7px;
+  height: 7px;
+  background: #fff;
+  border-radius: 50%;
+  position: absolute;
+  left: 0;
+  top: 7px;
+  opacity: 1;
+`;
+
+const EventWrapper = styled.div<{
+  isLast: boolean;
+}>`
+  padding-left: 30px;
+  position: relative;
+  margin-bottom: ${props => props.isLast ? "" : "25px"};
+`;
+
+const StyledActivityFeed = styled.div`
+  width: 100%;
+`;

+ 1 - 1
dashboard/src/main/home/app-dashboard/expanded-app/EnvVariablesTab.tsx

@@ -10,7 +10,7 @@ interface EnvVariablesTabProps {
   envVars: any;
   setEnvVars: (x: any) => void;
   status: React.ReactNode;
-  updatePorterApp: () => void;
+  updatePorterApp: any;
   clearStatus: () => void;
 }
 

+ 17 - 1
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -42,6 +42,8 @@ import { EnvVariablesTab } from "./EnvVariablesTab";
 import GHABanner from "./GHABanner";
 import LogSection from "./LogSection";
 import EventsTab from "./EventsTab";
+import ActivityFeed from "./ActivityFeed";
+import JobRuns from "./JobRuns";
 
 type Props = RouteComponentProps & {};
 
@@ -550,6 +552,8 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         );
       case "events":
         return <EventsTab currentChart={appData.chart} />;
+      case "activity":
+        return <ActivityFeed chart={appData.chart} />;
       case "logs":
         return <LogSection currentChart={appData.chart} />;
       case "environment-variables":
@@ -562,8 +566,16 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             clearStatus={() => setButtonStatus("")}
           />
         );
+      case "pre-deploy":
+        return (
+          <JobRuns
+            lastRunStatus="all"
+            namespace={appData.chart?.namespace}
+            sortType="Newest"
+          />
+        );
       default:
-        return <div>dream on</div>;
+        return <div>Tab not found</div>;
     }
   };
 
@@ -714,7 +726,9 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                     ? hasBuiltImage
                       ? [
                         { label: "Overview", value: "overview" },
+                        { label: "Events", value: "events" },
                         { label: "Logs", value: "logs" },
+                        { label: "Pre-deploy", value: "pre-deploy" },
                         {
                           label: "Environment variables",
                           value: "environment-variables",
@@ -724,6 +738,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                       ]
                       : [
                         { label: "Overview", value: "overview" },
+                        { label: "Pre-deploy", value: "pre-deploy" },
                         {
                           label: "Environment variables",
                           value: "environment-variables",
@@ -735,6 +750,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                       { label: "Overview", value: "overview" },
                       { label: "Events", value: "events" },
                       { label: "Logs", value: "logs" },
+                      { label: "Pre-deploy", value: "pre-deploy" },
                       {
                         label: "Environment variables",
                         value: "environment-variables",

+ 497 - 0
dashboard/src/main/home/app-dashboard/expanded-app/JobRuns.tsx

@@ -0,0 +1,497 @@
+import DynamicLink from "components/DynamicLink";
+import Loading from "components/Loading";
+import Table from "components/OldTable";
+import Placeholder from "components/Placeholder";
+import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
+import { CellProps, Column, Row } from "react-table";
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { NewWebsocketOptions, useWebsockets } from "shared/hooks/useWebsockets";
+import { useRouting } from "shared/routing";
+import { relativeDate, timeFrom } from "shared/string_utils";
+import styled from "styled-components";
+
+type Props = {
+  lastRunStatus: "failed" | "succeeded" | "active" | "all";
+  namespace: string;
+  sortType: "Newest" | "Oldest" | "Alphabetical";
+};
+
+const runnedFor = (start: string | number, end?: string | number) => {
+  const duration = timeFrom(start, end);
+
+  const unit =
+    duration.time === 1
+      ? duration.unitOfTime.substring(0, duration.unitOfTime.length - 1)
+      : duration.unitOfTime;
+
+  return `${duration.time} ${unit}`;
+};
+
+const JobRuns: React.FC<Props> = ({
+  lastRunStatus,
+  namespace,
+  sortType,
+}) => {
+  const { currentCluster, currentProject } = useContext(Context);
+  const [jobRuns, setJobRuns] = useState<JobRun[]>(null);
+  const [hasError, setHasError] = useState(false);
+  const tmpJobRuns = useRef([]);
+  const lastStreamStatus = useRef("");
+  const { openWebsocket, newWebsocket, closeAllWebsockets } = useWebsockets();
+
+  const getJobRuns = () => {
+    closeAllWebsockets();
+    tmpJobRuns.current = [];
+    lastStreamStatus.current = "";
+    setJobRuns(null);
+    setHasError(false);
+    const websocketId = `job-runs-for-all-charts-ws`;
+    const endpoint = `/api/projects/${currentProject.id}/clusters/${currentCluster.id}/namespaces/${namespace}/jobs/stream`;
+
+    const config: NewWebsocketOptions = {
+      onopen: console.log,
+      onmessage: (message) => {
+        const data = JSON.parse(message.data);
+
+        if (data.streamStatus === "finished") {
+          setHasError(false);
+          setJobRuns(tmpJobRuns.current);
+          lastStreamStatus.current = data.streamStatus;
+          return;
+        }
+
+        if (data.streamStatus === "errored") {
+          setHasError(true);
+          tmpJobRuns.current = [];
+          setJobRuns([]);
+          return;
+        }
+
+        tmpJobRuns.current = [...tmpJobRuns.current, data];
+      },
+      onclose: (event) => {
+        // console.log(event);
+        closeAllWebsockets();
+      },
+      onerror: (error) => {
+        setHasError(true);
+        console.log(error);
+        closeAllWebsockets();
+      },
+    };
+    newWebsocket(websocketId, endpoint, config);
+    openWebsocket(websocketId);
+  };
+
+  useEffect(() => {
+    if (!namespace) {
+      return;
+    }
+
+    getJobRuns();
+  }, [currentCluster, currentProject, namespace]);
+
+  useEffect(() => {
+    return () => {
+      closeAllWebsockets();
+    };
+  }, []);
+
+  const columns = useMemo<Column<JobRun>[]>(
+    () => [
+      {
+        Header: "Started",
+        accessor: (originalRow) => relativeDate(originalRow.status.startTime),
+      },
+      {
+        Header: "Run for",
+        accessor: (originalRow) => {
+          if (originalRow.status?.completionTime) {
+            return originalRow.status?.completionTime;
+          } else if (
+            Array.isArray(originalRow.status?.conditions) &&
+            originalRow.status?.conditions[0]?.lastTransitionTime
+          ) {
+            return originalRow.status?.conditions[0]?.lastTransitionTime;
+          } else {
+            return "Still running...";
+          }
+        },
+        Cell: ({ row }) => {
+          if (row.original.status?.completionTime) {
+            return runnedFor(
+              row.original.status?.startTime,
+              row.original.status?.completionTime
+            );
+          } else if (
+            Array.isArray(row.original.status?.conditions) &&
+            row.original.status?.conditions[0]?.lastTransitionTime
+          ) {
+            return runnedFor(
+              row.original.status?.startTime,
+              row.original.status?.conditions[0]?.lastTransitionTime
+            );
+          } else {
+            return "Still running...";
+          }
+        },
+        styles: {
+          padding: "10px",
+        },
+      },
+      {
+        Header: "Status",
+        id: "status",
+        Cell: ({ row }: CellProps<JobRun>) => {
+          if (row.original.status?.succeeded >= 1) {
+            return <Status color="#38a88a">Succeeded</Status>;
+          }
+
+          if (row.original.status?.failed >= 1) {
+            return <Status color="#cc3d42">Failed</Status>;
+          }
+
+          return <Status color="#ffffff11">Running</Status>;
+        },
+      },
+      {
+        Header: "Commit tag",
+        id: "commit_or_image_tag",
+        accessor: (originalRow) => {
+          const container = originalRow.spec?.template?.spec?.containers[0];
+          return container?.image?.split(":")[1] || "N/A";
+        },
+        Cell: ({ row }: any) => {
+          const container = row.original.spec?.template?.spec?.containers[0];
+
+          const tag = container?.image?.split(":")[1];
+          return tag;
+        },
+      },
+      {
+        id: "expand",
+        Cell: ({ row }: CellProps<JobRun>) => {
+          /**
+           * project_id: currentProject.id,
+          chart_revision: 0,
+          job: row.original?.metadata?.name,
+           */
+          const urlParams = new URLSearchParams();
+          urlParams.append("project_id", String(currentProject.id));
+          urlParams.append("chart_revision", String(0));
+          urlParams.append("job", row.original.metadata.name);
+
+          return (
+            <RedirectButton
+              to={{
+                pathname: `/jobs/${currentCluster.name}/${row.original?.metadata?.namespace}/${row.original?.metadata?.labels["meta.helm.sh/release-name"]}`,
+                search: `app=${row.original?.metadata?.namespace.split("porter-stack-")[1]}&` + urlParams.toString(),
+              }}
+            >
+              <i className="material-icons">open_in_new</i>
+            </RedirectButton>
+          );
+        },
+        maxWidth: 40,
+      },
+    ],
+    []
+  );
+
+  const data = useMemo(() => {
+    if (jobRuns === null) {
+      return [];
+    }
+    let tmp = [...tmpJobRuns.current];
+    const filter = new JobRunsFilter(tmp);
+    switch (lastRunStatus) {
+      case "active":
+        tmp = filter.filterByActive();
+        break;
+      case "failed":
+        tmp = filter.filterByFailed();
+        break;
+      case "succeeded":
+        tmp = filter.filterBySucceded();
+        break;
+      default:
+        tmp = filter.dontFilter();
+        break;
+    }
+
+    const sorter = new JobRunsSorter(tmp);
+    switch (sortType) {
+      case "Alphabetical":
+        tmp = sorter.sortByAlphabetical();
+        break;
+      case "Newest":
+        tmp = sorter.sortByNewest();
+        break;
+      case "Oldest":
+        tmp = sorter.sortByOldest();
+        break;
+      default:
+        break;
+    }
+
+    return tmp;
+  }, [jobRuns, lastRunStatus, sortType]);
+
+  if (hasError && lastStreamStatus.current !== "finished") {
+    return (
+      <ErrorWrapper>
+        Couldn't retrieve jobs, please try again.{" "}
+        <RetryButton onClick={() => getJobRuns()}>Retry</RetryButton>
+      </ErrorWrapper>
+    );
+  }
+
+  if (jobRuns === null) {
+    return <Loading />;
+  }
+
+  if (!jobRuns?.length) {
+    return <Placeholder>No pre-deploy job runs were found.</Placeholder>;
+  }
+
+  return (
+    <Table
+      columns={columns}
+      disableGlobalFilter
+      data={data}
+      isLoading={jobRuns === null}
+      enablePagination
+    />
+  );
+};
+
+export default JobRuns;
+
+const RetryButton = styled.button`
+  margin-left: 10px;
+  border: none;
+  background: #5460c6;
+  color: white;
+  padding: 5px 10px;
+  border-radius: 25px;
+  min-height: 35px;
+  min-width: 65px;
+  cursor: pointer;
+`;
+
+const ErrorWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 300px;
+  width: 100%;
+  color: #ffffff88;
+`;
+
+const Status = styled.div<{ color: string }>`
+  padding: 5px 10px;
+  background: ${(props) => props.color};
+  font-size: 13px;
+  border-radius: 3px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: min-content;
+  height: 25px;
+  min-width: 90px;
+`;
+
+const CommandString = styled.div`
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 160px;
+  color: #ffffff55;
+  margin-right: 27px;
+  font-family: monospace;
+`;
+
+const RedirectButton = styled(DynamicLink)`
+  user-select: none;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  > i {
+    border-radius: 20px;
+    font-size: 18px;
+    padding: 5px;
+    margin: 0 5px;
+    color: #ffffff44;
+    :hover {
+      background: #ffffff11;
+    }
+  }
+`;
+
+type JobRun = {
+  metadata: {
+    name: string;
+    namespace: string;
+    selfLink: string;
+    uid: string;
+    resourceVersion: string;
+    creationTimestamp: string;
+    labels: {
+      [key: string]: string;
+      "app.kubernetes.io/instance": string;
+      "app.kubernetes.io/managed-by": string;
+      "app.kubernetes.io/version": string;
+      "helm.sh/chart": string;
+      "helm.sh/revision": string;
+      "meta.helm.sh/release-name": string;
+    };
+    ownerReferences: {
+      apiVersion: string;
+      kind: string;
+      name: string;
+      uid: string;
+      controller: boolean;
+      blockOwnerDeletion: boolean;
+    }[];
+    managedFields: unknown[];
+  };
+  spec: {
+    [key: string]: unknown;
+    parallelism: number;
+    completions: number;
+    backOffLimit?: number;
+    selector: {
+      [key: string]: unknown;
+      matchLabels: {
+        [key: string]: unknown;
+        "controller-uid": string;
+      };
+    };
+    template: {
+      [key: string]: unknown;
+      metadata: {
+        creationTimestamp: string | null;
+        labels: {
+          [key: string]: unknown;
+          "controller-uid": string;
+          "job-name": string;
+        };
+      };
+      spec: {
+        containers: {
+          name: string;
+          image: string;
+          command: string[];
+          env?: {
+            [key: string]: unknown;
+            name: string;
+            value?: string;
+            valueFrom?: {
+              secretKeyRef?: { name: string; key: string };
+              configMapKeyRef?: { name: string; key: string };
+            };
+          }[];
+          resources: {
+            [key: string]: unknown;
+            limits: { [key: string]: unknown; memory: string };
+            requests: { [key: string]: unknown; cpu: string; memory: string };
+          };
+          terminationMessagePath: string;
+          terminationMessagePolicy: string;
+          imagePullPolicy: string;
+        }[];
+
+        restartPolicy: string;
+        terminationGracePeriodSeconds: number;
+        dnsPolicy: string;
+        shareProcessNamespace: boolean;
+        securityContext: unknown;
+        schedulerName: string;
+        tolerations: {
+          [key: string]: unknown;
+          key: string;
+          operator: string;
+          value: string;
+          effect: string;
+        }[];
+      };
+    };
+  };
+  status: {
+    [key: string]: unknown;
+    conditions: {
+      [key: string]: unknown;
+      type: string;
+      status: string;
+      lastProbeTime: string;
+      lastTransitionTime: string;
+    }[];
+    startTime: string;
+    completionTime: string | undefined | null;
+    succeeded?: number;
+    failed?: number;
+    active?: number;
+  };
+};
+
+class JobRunsFilter {
+  jobRuns: JobRun[];
+
+  constructor(newJobRuns: JobRun[]) {
+    this.jobRuns = newJobRuns;
+  }
+
+  filterByFailed() {
+    return this.jobRuns.filter((jobRun) => jobRun?.status?.failed);
+  }
+
+  filterByActive() {
+    return this.jobRuns.filter((jobRun) => jobRun?.status?.active);
+  }
+
+  filterBySucceded() {
+    return this.jobRuns.filter(
+      (jobRun) =>
+        jobRun?.status?.succeeded &&
+        !jobRun?.status?.active &&
+        !jobRun?.status?.failed
+    );
+  }
+
+  dontFilter() {
+    return this.jobRuns;
+  }
+}
+
+class JobRunsSorter {
+  jobRuns: JobRun[];
+
+  constructor(newJobRuns: JobRun[]) {
+    this.jobRuns = newJobRuns;
+  }
+
+  sortByNewest() {
+    return this.jobRuns.sort((a, b) => {
+      return Date.parse(a?.metadata?.creationTimestamp) >
+        Date.parse(b?.metadata?.creationTimestamp)
+        ? -1
+        : 1;
+    });
+  }
+
+  sortByOldest() {
+    return this.jobRuns.sort((a, b) => {
+      return Date.parse(a?.metadata?.creationTimestamp) >
+        Date.parse(b?.metadata?.creationTimestamp)
+        ? 1
+        : -1;
+    });
+  }
+
+  sortByAlphabetical() {
+    return this.jobRuns.sort((a, b) =>
+      a?.metadata?.name > b?.metadata?.name ? 1 : -1
+    );
+  }
+}

+ 18 - 6
dashboard/src/main/home/app-dashboard/expanded-app/StatusFooter.tsx

@@ -219,13 +219,21 @@ const StatusFooter: React.FC<Props> = ({
         <Container row>
           {percentage === "0.00%" ? (
             <StatusDot />
+          ) : total === 0 ? (
+            <StatusDot color="#ffffff33" />
           ) : (
             <StatusCircle percentage={percentage} />
           )}
           <Text color="helper">
-            Running {available}/{total} instances{" "}
-            {stale == 1 ? `(${stale} old instance)` : ""}
-            {stale > 1 ? `(${stale} old instances)` : ""}
+            {total > 0 ? (
+              <>
+                Running {available}/{total} instances{" "}
+                {stale == 1 ? `(${stale} old instance)` : ""}
+                {stale > 1 ? `(${stale} old instances)` : ""}
+              </>
+            ) : (
+              "Loading . . ."
+            )}
           </Text>
           {/*
           <Spacer inline x={1} />
@@ -248,13 +256,13 @@ const StatusFooter: React.FC<Props> = ({
 
 export default StatusFooter;
 
-const StatusDot = styled.div`
+const StatusDot = styled.div<{ color?: string }>`
   min-width: 7px;
   max-width: 7px;
   height: 7px;
   border-radius: 50%;
   margin-right: 10px;
-  background: #38a88a;
+  background: ${props => props.color || "#38a88a"};
 `;
 
 const Mi = styled.i`
@@ -269,7 +277,10 @@ const I = styled.i`
   margin-right: 5px;
 `;
 
-const StatusCircle = styled.div<{ percentage?: any }>`
+const StatusCircle = styled.div<{ 
+  percentage?: any;
+  dashed?: boolean;
+}>`
   width: 16px;
   height: 16px;
   border-radius: 50%;
@@ -279,6 +290,7 @@ const StatusCircle = styled.div<{ percentage?: any }>`
     #ffffff33 ${(props) => props.percentage},
     #ffffffaa 0% ${(props) => props.percentage}
   );
+  border: ${(props) => (props.dashed ? "1px dashed #ffffff55" : "none")};
 `;
 
 const StyledStatusFooter = styled.div`

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

@@ -10,7 +10,7 @@ import {
 } from "shared/types";
 import api from "shared/api";
 import { pushFiltered } from "shared/routing";
-import { ExpandedJobChartFC } from "./ExpandedJobChart";
+import ExpandedJobChart from "./ExpandedJobChart";
 import ExpandedChart from "./ExpandedChart";
 import Loading from "components/Loading";
 import PageNotFound from "components/PageNotFound";
@@ -102,7 +102,7 @@ class ExpandedChartWrapper extends Component<PropsType, StateType> {
       );
     } else if (currentChart && baseRoute === "jobs") {
       return (
-        <ExpandedJobChartFC
+        <ExpandedJobChart
           namespace={namespace}
           currentChart={currentChart}
           currentCluster={this.context.currentCluster}

+ 19 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -1,6 +1,7 @@
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import styled from "styled-components";
 import yaml from "js-yaml";
+import { RouteComponentProps, withRouter } from "react-router";
 
 import leftArrow from "assets/left-arrow.svg";
 import { cloneDeep, set } from "lodash";
@@ -29,6 +30,8 @@ import CronPrettifier from "cronstrue";
 import BuildSettingsTab from "./build-settings/BuildSettingsTab";
 import { useStackEnvGroups } from "./useStackEnvGroups";
 import api from "shared/api";
+import { getQueryParam, pushFiltered } from "shared/routing";
+import { useLocation } from "react-router";
 
 const readableDate = (s: string) => {
   let ts = new Date(s);
@@ -40,13 +43,15 @@ const readableDate = (s: string) => {
   return `${time} on ${date}`;
 };
 
-export const ExpandedJobChartFC: React.FC<{
+type PropsType = RouteComponentProps & {
   namespace: string;
   currentChart: ChartType;
   currentCluster: ClusterType;
   closeChart: () => void;
   setSidebar: (x: boolean) => void;
-}> = ({ currentChart: oldChart, closeChart, currentCluster }) => {
+}
+
+const ExpandedJobChart: React.FC<PropsType> = ({ currentChart: oldChart, closeChart, currentCluster, ...props }) => {
   const { currentProject, setCurrentOverlay } = useContext(Context);
   const [isAuthorized] = useAuth();
   const {
@@ -60,6 +65,8 @@ export const ExpandedJobChartFC: React.FC<{
     loadChartWithSpecificRevision,
   } = useChart(oldChart, closeChart);
 
+  const location = useLocation();
+
   const {
     jobs,
     hasPorterImageTemplate,
@@ -331,7 +338,14 @@ export const ExpandedJobChartFC: React.FC<{
       <ExpandedJobRun
         currentChart={chart}
         jobRun={selectedJob}
-        onClose={() => setSelectedJob(null)}
+        onClose={() => {
+          const app = getQueryParam({ location }, "app");
+          if (app) {
+            window.location.href = `/apps/${app}`;
+          } else {
+            setSelectedJob(null);
+          }
+        }}
       />
     );
   }
@@ -408,6 +422,8 @@ export const ExpandedJobChartFC: React.FC<{
   );
 };
 
+export default withRouter(ExpandedJobChart);
+
 const ExpandedJobHeader: React.FC<{
   chart: ChartType;
   jobs: any[];

+ 2 - 2
dashboard/src/main/home/project-settings/InviteList.tsx

@@ -512,8 +512,8 @@ const Placeholder = styled.div`
   font-size: 13px;
   color: #aaaabb;
   border-radius: 5px;
-  background: #26292e;
-  border: 1px solid #494b4f;
+  background: ${({ theme }) => theme.fg}};
+  border: 1px solid ${({ theme }) => theme.border};
 `;
 
 const ButtonWrapper = styled.div`

+ 2 - 1
dashboard/src/main/home/project-settings/api-tokens/TokenList.tsx

@@ -149,10 +149,11 @@ export default TokenList;
 const TokenWrapper = styled.div`
   color: #ffffff55;
   background: #ffffff01;
-  border: 1px solid #aaaabbaa;
+  border: 1px solid ${({ theme }) => theme.border};
   font-size: 13px;
   border-radius: 5px;
   cursor: pointer;
+  background: ${({ theme }) => theme.fg};
   margin: 8px 0;
   :hover {
     border: 1px solid #aaaabb;