|
|
@@ -0,0 +1,739 @@
|
|
|
+import React, { Component } from "react";
|
|
|
+import styled from "styled-components";
|
|
|
+import yaml from "js-yaml";
|
|
|
+import close from "assets/close.png";
|
|
|
+import _ from "lodash";
|
|
|
+
|
|
|
+import {
|
|
|
+ ResourceType,
|
|
|
+ ChartType,
|
|
|
+ StorageType,
|
|
|
+ ClusterType
|
|
|
+} from "shared/types";
|
|
|
+import { Context } from "shared/Context";
|
|
|
+import api from "shared/api";
|
|
|
+
|
|
|
+import SaveButton from "components/SaveButton";
|
|
|
+import ConfirmOverlay from "components/ConfirmOverlay";
|
|
|
+import Loading from "components/Loading";
|
|
|
+import StatusIndicator from "components/StatusIndicator";
|
|
|
+import TabRegion from "components/TabRegion";
|
|
|
+import JobList from "./jobs/JobList"
|
|
|
+import ExpandableResource from "components/ExpandableResource";
|
|
|
+import SettingsSection from "./SettingsSection";
|
|
|
+import ValuesWrapper from "components/values-form/ValuesWrapper";
|
|
|
+import ValuesForm from "components/values-form/ValuesForm";
|
|
|
+
|
|
|
+
|
|
|
+type PropsType = {
|
|
|
+ namespace: string;
|
|
|
+ currentChart: ChartType;
|
|
|
+ currentCluster: ClusterType;
|
|
|
+ setCurrentChart: (x: ChartType | null) => void;
|
|
|
+ setSidebar: (x: boolean) => void;
|
|
|
+};
|
|
|
+
|
|
|
+type StateType = {
|
|
|
+ loading: boolean;
|
|
|
+ jobs: any[];
|
|
|
+ tabOptions: any[];
|
|
|
+ tabContents: any;
|
|
|
+ currentTab: string | null;
|
|
|
+ websockets: Record<string, any>;
|
|
|
+ showDeleteOverlay: boolean;
|
|
|
+ deleting: boolean;
|
|
|
+ saveValuesStatus: string | null;
|
|
|
+};
|
|
|
+
|
|
|
+export default class ExpandedJobChart extends Component<PropsType, StateType> {
|
|
|
+ state = {
|
|
|
+ loading: true,
|
|
|
+ jobs: [] as any[],
|
|
|
+ tabOptions: [] as any[],
|
|
|
+ tabContents: [] as any,
|
|
|
+ currentTab: null as string | null,
|
|
|
+ websockets: {} as Record<string, any>,
|
|
|
+ showDeleteOverlay: false,
|
|
|
+ deleting: false,
|
|
|
+ saveValuesStatus: null as string | null,
|
|
|
+ };
|
|
|
+
|
|
|
+ // Retrieve full chart data (includes form and values)
|
|
|
+ getChartData = (chart: ChartType) => {
|
|
|
+ let { currentProject } = this.context;
|
|
|
+ let { currentCluster, currentChart, setCurrentChart } = this.props;
|
|
|
+
|
|
|
+ this.setState({ loading: true });
|
|
|
+ api
|
|
|
+ .getChart(
|
|
|
+ "<token>",
|
|
|
+ {
|
|
|
+ namespace: currentChart.namespace,
|
|
|
+ cluster_id: currentCluster.id,
|
|
|
+ storage: StorageType.Secret
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: chart.name,
|
|
|
+ revision: chart.version,
|
|
|
+ id: currentProject.id
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(res => {
|
|
|
+ setCurrentChart(res.data);
|
|
|
+ this.setState({ loading: false });
|
|
|
+ })
|
|
|
+ .catch(console.log);
|
|
|
+ };
|
|
|
+
|
|
|
+ refreshChart = () => this.getChartData(this.props.currentChart);
|
|
|
+
|
|
|
+ mergeNewJob = (newJob : any) => {
|
|
|
+ let jobs = this.state.jobs
|
|
|
+ let exists = false
|
|
|
+ jobs.forEach((job : any, i : number, self : any[]) => {
|
|
|
+ if (job.metadata?.name == newJob.metadata?.name && job.metadata?.namespace == newJob.metadata?.namespace) {
|
|
|
+ console.log("GOT MATCH")
|
|
|
+ self[i] = newJob
|
|
|
+ exists = true
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!exists) {
|
|
|
+ console.log("NEED TO PUSH", newJob)
|
|
|
+ jobs.push(newJob)
|
|
|
+ }
|
|
|
+
|
|
|
+ this.sortJobsAndSave(jobs)
|
|
|
+ }
|
|
|
+
|
|
|
+ setupJobWebsocket = (chart: ChartType) => {
|
|
|
+ let chartVersion = `${chart.chart.metadata.name}-${chart.chart.metadata.version}`
|
|
|
+
|
|
|
+ let { currentCluster, currentProject } = this.context;
|
|
|
+ let protocol = process.env.NODE_ENV == "production" ? "wss" : "ws";
|
|
|
+ let ws = new WebSocket(
|
|
|
+ `${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/job/status?cluster_id=${currentCluster.id}`
|
|
|
+ );
|
|
|
+ ws.onopen = () => {
|
|
|
+ console.log("connected to websocket");
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onmessage = (evt: MessageEvent) => {
|
|
|
+ let event = JSON.parse(evt.data);
|
|
|
+ let object = event.Object;
|
|
|
+ object.metadata.kind = event.Kind;
|
|
|
+
|
|
|
+ // if event type is add or update, merge with existing jobs
|
|
|
+ if (event.event_type == "ADD" || event.event_type == "UPDATE") {
|
|
|
+ // filter job belonging to chart
|
|
|
+ let chartLabel = event.Object?.metadata?.labels["helm.sh/chart"]
|
|
|
+
|
|
|
+ if (chartLabel && chartLabel == chartVersion) {
|
|
|
+ this.mergeNewJob(event.Object)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onclose = () => {
|
|
|
+ console.log("closing websocket");
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onerror = (err: ErrorEvent) => {
|
|
|
+ console.log(err);
|
|
|
+ ws.close();
|
|
|
+ };
|
|
|
+
|
|
|
+ return ws;
|
|
|
+ };
|
|
|
+
|
|
|
+ handleSaveValues = (config? : any) => {
|
|
|
+ let { currentCluster, setCurrentError, currentProject } = this.context;
|
|
|
+ this.setState({ saveValuesStatus: "loading" });
|
|
|
+
|
|
|
+ let conf : string
|
|
|
+
|
|
|
+ if (!config) {
|
|
|
+ conf = yaml.dump({
|
|
|
+ ...this.props.currentChart.config,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // Convert dotted keys to nested objects
|
|
|
+ let values = {};
|
|
|
+
|
|
|
+ for (let key in config) {
|
|
|
+ _.set(values, key, config[key]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Weave in preexisting values and convert to yaml
|
|
|
+ conf = yaml.dump({
|
|
|
+ ...(this.props.currentChart.config as Object),
|
|
|
+ ...values
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ api
|
|
|
+ .upgradeChartValues(
|
|
|
+ "<token>",
|
|
|
+ {
|
|
|
+ namespace: this.props.currentChart.namespace,
|
|
|
+ storage: StorageType.Secret,
|
|
|
+ values: conf,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: currentProject.id,
|
|
|
+ name: this.props.currentChart.name,
|
|
|
+ cluster_id: currentCluster.id
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(res => {
|
|
|
+ this.setState({ saveValuesStatus: "successful" });
|
|
|
+ this.refreshChart();
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ console.log(err);
|
|
|
+ this.setState({ saveValuesStatus: "error" });
|
|
|
+ setCurrentError(err)
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ getJobs = async (chart: ChartType) => {
|
|
|
+ let { currentCluster, currentProject, setCurrentError } = this.context;
|
|
|
+
|
|
|
+ api.getJobs(
|
|
|
+ "<token>",
|
|
|
+ {
|
|
|
+ cluster_id: currentCluster.id,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: currentProject.id,
|
|
|
+ chart: `${chart.chart.metadata.name}-${chart.chart.metadata.version}`,
|
|
|
+ namespace: chart.namespace,
|
|
|
+ }
|
|
|
+ ).then(res => {
|
|
|
+ // sort jobs by started timestamp
|
|
|
+ this.sortJobsAndSave(res.data)
|
|
|
+ })
|
|
|
+ .catch(err => setCurrentError(err));
|
|
|
+ }
|
|
|
+
|
|
|
+ sortJobsAndSave = (jobs : any[]) => {
|
|
|
+ jobs.sort((job1, job2) => {
|
|
|
+ let date1 : Date = new Date(job1.status?.startTime)
|
|
|
+ let date2 : Date = new Date(job2.status?.startTime)
|
|
|
+
|
|
|
+ return date2.getTime() - date1.getTime()
|
|
|
+ })
|
|
|
+
|
|
|
+ console.log("JOBS ARE", jobs)
|
|
|
+
|
|
|
+ this.setState({ jobs })
|
|
|
+ }
|
|
|
+
|
|
|
+ renderTabContents = () => {
|
|
|
+ let currentTab = this.state.currentTab
|
|
|
+ let { currentChart } = this.props;
|
|
|
+
|
|
|
+ switch (currentTab) {
|
|
|
+ case "jobs":
|
|
|
+ return (
|
|
|
+ <TabWrapper>
|
|
|
+ <JobList jobs={this.state.jobs} />
|
|
|
+ <SaveButton
|
|
|
+ text="Rerun Job"
|
|
|
+ onClick={this.handleSaveValues}
|
|
|
+ status={this.state.saveValuesStatus}
|
|
|
+ makeFlush={true}
|
|
|
+ />
|
|
|
+ </TabWrapper>
|
|
|
+ );
|
|
|
+ case "settings":
|
|
|
+ return (
|
|
|
+ <SettingsSection
|
|
|
+ currentChart={this.props.currentChart}
|
|
|
+ refreshChart={this.refreshChart}
|
|
|
+ setShowDeleteOverlay={(x: boolean) =>
|
|
|
+ this.setState({ showDeleteOverlay: x })
|
|
|
+ }
|
|
|
+ />
|
|
|
+ );
|
|
|
+ default:
|
|
|
+ if (this.state.tabOptions && currentTab && currentTab.includes("@")) {
|
|
|
+ return (
|
|
|
+ <ValuesWrapper
|
|
|
+ formTabs={this.state.tabOptions}
|
|
|
+ onSubmit={this.handleSaveValues}
|
|
|
+ saveValuesStatus={this.state.saveValuesStatus}
|
|
|
+ isInModal={true}
|
|
|
+ currentTab={currentTab}
|
|
|
+ >
|
|
|
+ {(metaState: any, setMetaState: any) => {
|
|
|
+ return this.state.tabOptions.map((tab: any, i: number) => {
|
|
|
+ // If tab is current, render
|
|
|
+ if (tab.value === currentTab) {
|
|
|
+ return (
|
|
|
+ <ValuesForm
|
|
|
+ key={i}
|
|
|
+ metaState={metaState}
|
|
|
+ setMetaState={setMetaState}
|
|
|
+ sections={tab.sections}
|
|
|
+ disabled={true}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ </ValuesWrapper>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ updateTabs() {
|
|
|
+ let formData = this.props.currentChart.form;
|
|
|
+ let tabOptions = [] as any[];
|
|
|
+
|
|
|
+ // Append universal tabs
|
|
|
+ tabOptions.push({ label: "Jobs", value: "jobs" });
|
|
|
+
|
|
|
+ if (formData) {
|
|
|
+ formData.tabs.map((tab: any, i: number) => {
|
|
|
+ tabOptions.push({
|
|
|
+ value: "@" + tab.name,
|
|
|
+ label: tab.label,
|
|
|
+ sections: tab.sections,
|
|
|
+ context: tab.context
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ tabOptions.push({ label: "Settings", value: "settings" });
|
|
|
+
|
|
|
+ // Filter tabs if previewing an old revision
|
|
|
+ this.setState({ tabOptions });
|
|
|
+ }
|
|
|
+
|
|
|
+ renderIcon = () => {
|
|
|
+ let { currentChart } = this.props;
|
|
|
+
|
|
|
+ if (
|
|
|
+ currentChart.chart.metadata.icon &&
|
|
|
+ currentChart.chart.metadata.icon !== ""
|
|
|
+ ) {
|
|
|
+ return <Icon src={currentChart.chart.metadata.icon} />;
|
|
|
+ } else {
|
|
|
+ return <i className="material-icons">tonality</i>;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ readableDate = (s: string) => {
|
|
|
+ let ts = new Date(s);
|
|
|
+ let date = ts.toLocaleDateString();
|
|
|
+ let time = ts.toLocaleTimeString([], {
|
|
|
+ hour: "numeric",
|
|
|
+ minute: "2-digit"
|
|
|
+ });
|
|
|
+ return `${time} on ${date}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ getChartStatus = (chartStatus: string) => {
|
|
|
+ return "jobs"
|
|
|
+
|
|
|
+ // if (chartStatus === "deployed") {
|
|
|
+ // for (var uid in this.state.controllers) {
|
|
|
+ // let value = this.state.controllers[uid];
|
|
|
+ // let available = this.getAvailability(value.metadata.kind, value);
|
|
|
+ // let progressing = true;
|
|
|
+
|
|
|
+ // this.state.controllers[uid]?.status?.conditions?.forEach(
|
|
|
+ // (condition: any) => {
|
|
|
+ // if (
|
|
|
+ // condition.type == "Progressing" &&
|
|
|
+ // condition.status == "False" &&
|
|
|
+ // condition.reason == "ProgressDeadlineExceeded"
|
|
|
+ // ) {
|
|
|
+ // progressing = false;
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // );
|
|
|
+
|
|
|
+ // if (!available && progressing) {
|
|
|
+ // return "loading";
|
|
|
+ // } else if (!available && !progressing) {
|
|
|
+ // return "failed";
|
|
|
+ // }
|
|
|
+ // }
|
|
|
+ // return "deployed";
|
|
|
+ // }
|
|
|
+ // return chartStatus;
|
|
|
+ };
|
|
|
+
|
|
|
+ componentDidMount() {
|
|
|
+ let { currentCluster, currentProject } = this.context;
|
|
|
+ let { currentChart } = this.props;
|
|
|
+
|
|
|
+ window.analytics.track("Opened Chart", {
|
|
|
+ chart: currentChart.name
|
|
|
+ });
|
|
|
+
|
|
|
+ this.getChartData(currentChart);
|
|
|
+ this.getJobs(currentChart);
|
|
|
+ this.updateTabs();
|
|
|
+ this.setupJobWebsocket(currentChart)
|
|
|
+ }
|
|
|
+
|
|
|
+ componentDidUpdate(prevProps: PropsType) {
|
|
|
+ if (this.props.currentChart !== prevProps.currentChart) {
|
|
|
+ this.updateTabs();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+// componentWillUnmount() {
|
|
|
+// if (this.state.websockets) {
|
|
|
+// this.state.websockets.forEach((ws: WebSocket) => {
|
|
|
+// ws.close();
|
|
|
+// });
|
|
|
+// }
|
|
|
+// }
|
|
|
+
|
|
|
+ handleUninstallChart = () => {
|
|
|
+ let { currentProject, currentCluster } = this.context;
|
|
|
+ let { currentChart } = this.props;
|
|
|
+ this.setState({ deleting: true });
|
|
|
+ api
|
|
|
+ .uninstallTemplate(
|
|
|
+ "<token>",
|
|
|
+ {},
|
|
|
+ {
|
|
|
+ namespace: currentChart.namespace,
|
|
|
+ storage: StorageType.Secret,
|
|
|
+ name: currentChart.name,
|
|
|
+ id: currentProject.id,
|
|
|
+ cluster_id: currentCluster.id
|
|
|
+ }
|
|
|
+ )
|
|
|
+ .then(res => {
|
|
|
+ this.setState({ showDeleteOverlay: false });
|
|
|
+ this.props.setCurrentChart(null);
|
|
|
+ })
|
|
|
+ .catch(console.log);
|
|
|
+ };
|
|
|
+
|
|
|
+ renderDeleteOverlay = () => {
|
|
|
+ if (this.state.deleting) {
|
|
|
+ return (
|
|
|
+ <DeleteOverlay>
|
|
|
+ <Loading />
|
|
|
+ </DeleteOverlay>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ render() {
|
|
|
+ let { currentChart, setCurrentChart } = this.props;
|
|
|
+ let chart = currentChart;
|
|
|
+ let status = this.getChartStatus(chart.info.status);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <CloseOverlay onClick={() => setCurrentChart(null)} />
|
|
|
+ <StyledExpandedChart>
|
|
|
+ <ConfirmOverlay
|
|
|
+ show={this.state.showDeleteOverlay}
|
|
|
+ message={`Are you sure you want to delete ${currentChart.name}?`}
|
|
|
+ onYes={this.handleUninstallChart}
|
|
|
+ onNo={() => this.setState({ showDeleteOverlay: false })}
|
|
|
+ />
|
|
|
+ {this.renderDeleteOverlay()}
|
|
|
+
|
|
|
+ <HeaderWrapper>
|
|
|
+ <TitleSection>
|
|
|
+ <Title>
|
|
|
+ <IconWrapper>{this.renderIcon()}</IconWrapper>
|
|
|
+ {chart.name}
|
|
|
+ </Title>
|
|
|
+ <InfoWrapper>
|
|
|
+ <LastDeployed>
|
|
|
+ {this.state.jobs.length} jobs run <Dot>•</Dot>Last run
|
|
|
+ {" " + this.readableDate(chart.info.last_deployed)}
|
|
|
+ </LastDeployed>
|
|
|
+ </InfoWrapper>
|
|
|
+
|
|
|
+ <TagWrapper>
|
|
|
+ Namespace <NamespaceTag>{chart.namespace}</NamespaceTag>
|
|
|
+ </TagWrapper>
|
|
|
+ </TitleSection>
|
|
|
+
|
|
|
+ <CloseButton onClick={() => setCurrentChart(null)}>
|
|
|
+ <CloseButtonImg src={close} />
|
|
|
+ </CloseButton>
|
|
|
+ </HeaderWrapper>
|
|
|
+
|
|
|
+ <TabRegion
|
|
|
+ currentTab={this.state.currentTab}
|
|
|
+ setCurrentTab={(x: string) => this.setState({ currentTab: x })}
|
|
|
+ options={this.state.tabOptions}
|
|
|
+ color={null}
|
|
|
+ >
|
|
|
+ {this.renderTabContents()}
|
|
|
+ </TabRegion>
|
|
|
+ </StyledExpandedChart>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+ExpandedJobChart.contextType = Context;
|
|
|
+
|
|
|
+const TabWrapper = styled.div`
|
|
|
+ height: 100%;
|
|
|
+ width: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+`
|
|
|
+
|
|
|
+const DeleteOverlay = styled.div`
|
|
|
+ position: absolute;
|
|
|
+ top: 0px;
|
|
|
+ opacity: 100%;
|
|
|
+ left: 0px;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ z-index: 999;
|
|
|
+ display: flex;
|
|
|
+ padding-bottom: 30px;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-family: "Work Sans", sans-serif;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: white;
|
|
|
+ flex-direction: column;
|
|
|
+ background: rgb(0, 0, 0, 0.73);
|
|
|
+ opacity: 0;
|
|
|
+ animation: lindEnter 0.2s;
|
|
|
+ animation-fill-mode: forwards;
|
|
|
+
|
|
|
+ @keyframes lindEnter {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const Bolded = styled.div`
|
|
|
+ font-weight: 500;
|
|
|
+ color: #ffffff44;
|
|
|
+ margin-right: 6px;
|
|
|
+`;
|
|
|
+
|
|
|
+const Url = styled.a`
|
|
|
+ display: block;
|
|
|
+ margin-left: 2px;
|
|
|
+ font-size: 13px;
|
|
|
+ margin-top: 16px;
|
|
|
+ user-select: all;
|
|
|
+ margin-bottom: -5px;
|
|
|
+ user-select: text;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ > i {
|
|
|
+ font-size: 15px;
|
|
|
+ margin-right: 10px;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const TabButton = styled.div`
|
|
|
+ position: absolute;
|
|
|
+ right: 0px;
|
|
|
+ height: 30px;
|
|
|
+ background: linear-gradient(to right, #26282f00, #26282f 20%);
|
|
|
+ padding-left: 30px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 13px;
|
|
|
+ color: ${(props: { devOpsMode: boolean }) =>
|
|
|
+ props.devOpsMode ? "#aaaabb" : "#aaaabb55"};
|
|
|
+ margin-left: 35px;
|
|
|
+ border-radius: 20px;
|
|
|
+ text-shadow: 0px 0px 8px
|
|
|
+ ${(props: { devOpsMode: boolean }) =>
|
|
|
+ props.devOpsMode ? "#ffffff66" : "none"};
|
|
|
+ cursor: pointer;
|
|
|
+ :hover {
|
|
|
+ color: ${(props: { devOpsMode: boolean }) =>
|
|
|
+ props.devOpsMode ? "" : "#aaaabb99"};
|
|
|
+ }
|
|
|
+
|
|
|
+ > i {
|
|
|
+ font-size: 17px;
|
|
|
+ margin-right: 9px;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const CloseOverlay = styled.div`
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: #202227;
|
|
|
+ animation: fadeIn 0.2s 0s;
|
|
|
+ opacity: 0;
|
|
|
+ animation-fill-mode: forwards;
|
|
|
+ @keyframes fadeIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const HeaderWrapper = styled.div``;
|
|
|
+
|
|
|
+const Dot = styled.div`
|
|
|
+ margin-right: 9px;
|
|
|
+ margin-left: 9px;
|
|
|
+`;
|
|
|
+
|
|
|
+const InfoWrapper = styled.div`
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin: 16px 0px 8px 0px;
|
|
|
+`;
|
|
|
+
|
|
|
+const LastDeployed = styled.div`
|
|
|
+ font-size: 13px;
|
|
|
+ margin-left: 0;
|
|
|
+ margin-top: -1px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ color: #aaaabb66;
|
|
|
+`;
|
|
|
+
|
|
|
+const TagWrapper = styled.div`
|
|
|
+ position: absolute;
|
|
|
+ right: 0px;
|
|
|
+ bottom: 0px;
|
|
|
+ height: 20px;
|
|
|
+ font-size: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #ffffff44;
|
|
|
+ border: 1px solid #ffffff44;
|
|
|
+ border-radius: 3px;
|
|
|
+ padding-left: 5px;
|
|
|
+ background: #26282e;
|
|
|
+`;
|
|
|
+
|
|
|
+const NamespaceTag = styled.div`
|
|
|
+ height: 20px;
|
|
|
+ margin-left: 6px;
|
|
|
+ color: #aaaabb;
|
|
|
+ background: #43454a;
|
|
|
+ border-radius: 3px;
|
|
|
+ font-size: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 0px 6px;
|
|
|
+ padding-left: 7px;
|
|
|
+ border-top-left-radius: 0px;
|
|
|
+ border-bottom-left-radius: 0px;
|
|
|
+`;
|
|
|
+
|
|
|
+const Icon = styled.img`
|
|
|
+ width: 100%;
|
|
|
+`;
|
|
|
+
|
|
|
+const IconWrapper = styled.div`
|
|
|
+ color: #efefef;
|
|
|
+ font-size: 16px;
|
|
|
+ height: 20px;
|
|
|
+ width: 20px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ border-radius: 3px;
|
|
|
+ margin-right: 12px;
|
|
|
+
|
|
|
+ > i {
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const Title = styled.div`
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 500;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+`;
|
|
|
+
|
|
|
+const TitleSection = styled.div`
|
|
|
+ width: 100%;
|
|
|
+ position: relative;
|
|
|
+`;
|
|
|
+
|
|
|
+const CloseButton = styled.div`
|
|
|
+ position: absolute;
|
|
|
+ display: block;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ padding: 13px 0 12px 0;
|
|
|
+ text-align: center;
|
|
|
+ border-radius: 50%;
|
|
|
+ right: 15px;
|
|
|
+ top: 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ :hover {
|
|
|
+ background-color: #ffffff11;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const CloseButtonImg = styled.img`
|
|
|
+ width: 14px;
|
|
|
+ margin: 0 auto;
|
|
|
+`;
|
|
|
+
|
|
|
+const StyledExpandedChart = styled.div`
|
|
|
+ width: calc(100% - 50px);
|
|
|
+ height: calc(100% - 50px);
|
|
|
+ background: red;
|
|
|
+ z-index: 0;
|
|
|
+ position: absolute;
|
|
|
+ top: 25px;
|
|
|
+ left: 25px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #26272f;
|
|
|
+ box-shadow: 0 5px 12px 4px #00000033;
|
|
|
+ animation: floatIn 0.3s;
|
|
|
+ animation-timing-function: ease-out;
|
|
|
+ animation-fill-mode: forwards;
|
|
|
+ padding: 25px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ @keyframes floatIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(30px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0px);
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const ResourceList = styled.div`
|
|
|
+ margin-bottom: 15px;
|
|
|
+ margin-top: 20px;
|
|
|
+ border-radius: 5px;
|
|
|
+ overflow: hidden;
|
|
|
+`;
|