| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698 |
- 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 loading from "assets/loading.gif";
- import { 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 TabRegion from "components/TabRegion";
- import JobList from "./jobs/JobList";
- import SettingsSection from "./SettingsSection";
- import FormWrapper from "components/values-form/FormWrapper";
- import { PlaceHolder } from "brace";
- type PropsType = {
- namespace: string;
- currentChart: ChartType;
- currentCluster: ClusterType;
- closeChart: () => void;
- setSidebar: (x: boolean) => void;
- };
- type StateType = {
- currentChart: ChartType;
- imageIsPlaceholder: boolean;
- loading: boolean;
- jobs: any[];
- tabOptions: any[];
- tabContents: any;
- currentTab: string | null;
- websockets: Record<string, any>;
- showDeleteOverlay: boolean;
- deleting: boolean;
- saveValuesStatus: string | null;
- formData: any;
- valuesToOverride: any;
- };
- export default class ExpandedJobChart extends Component<PropsType, StateType> {
- state = {
- currentChart: this.props.currentChart,
- imageIsPlaceholder: false,
- 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,
- formData: {} as any,
- valuesToOverride: {} as any,
- };
- // Retrieve full chart data (includes form and values)
- getChartData = (chart: ChartType) => {
- let { currentProject } = this.context;
- let { currentCluster, currentChart } = 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) => {
- let image = res.data?.config?.image?.repository;
- if (image === "porterdev/hello-porter-job") {
- this.setState(
- {
- currentChart: res.data,
- loading: false,
- imageIsPlaceholder: true,
- },
- () => {
- this.updateTabs();
- }
- );
- } else {
- this.setState({ currentChart: res.data, loading: false }, () => {
- this.updateTabs();
- });
- }
- })
- .catch(console.log);
- };
- refreshChart = () => this.getChartData(this.state.currentChart);
- mergeNewJob = (newJob: any) => {
- console.log("newJob", newJob);
- console.log("image?", newJob.values?.image?.repository);
- 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
- ) {
- self[i] = newJob;
- exists = true;
- }
- });
- if (!exists) {
- 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"];
- let releaseLabel =
- event.Object?.metadata?.labels["meta.helm.sh/release-name"];
- if (
- chartLabel &&
- releaseLabel &&
- chartLabel == chartVersion &&
- releaseLabel == chart.name
- ) {
- 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.state.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.state.currentChart.config as Object),
- ...values,
- });
- }
- api
- .upgradeChartValues(
- "<token>",
- {
- namespace: this.state.currentChart.namespace,
- storage: StorageType.Secret,
- values: conf,
- },
- {
- id: currentProject.id,
- name: this.state.currentChart.name,
- cluster_id: currentCluster.id,
- }
- )
- .then((res) => {
- this.setState({ saveValuesStatus: "successful" });
- this.refreshChart();
- })
- .catch((err) => {
- console.log(err);
- this.setState({ saveValuesStatus: "error" });
- setCurrentError(JSON.stringify(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,
- release_name: chart.name,
- }
- )
- .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();
- });
- this.setState({ jobs });
- };
- renderTabContents = (currentTab: string) => {
- switch (currentTab) {
- case "jobs":
- if (this.state.imageIsPlaceholder) {
- return (
- <Placeholder>
- <TextWrap>
- <Header>
- <Spinner src={loading} /> This job is currently being deployed
- </Header>
- Navigate to the "Actions" tab of your GitHub repo to view live
- build logs.
- </TextWrap>
- </Placeholder>
- );
- }
- 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.state.currentChart}
- refreshChart={this.refreshChart}
- setShowDeleteOverlay={(x: boolean) =>
- this.setState({ showDeleteOverlay: x })
- }
- />
- );
- default:
- }
- };
- updateTabs() {
- let formData = this.state.currentChart.form;
- if (formData) {
- this.setState(
- {
- formData,
- },
- () =>
- this.setState({
- // TODO: handle passing in override values at same time as formData
- valuesToOverride: {
- showCronToggle: { value: false },
- },
- })
- );
- }
- 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.state;
- 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}`;
- };
- componentDidMount() {
- let { currentChart } = this.state;
- window.analytics.track("Opened Chart", {
- chart: currentChart.name,
- });
- this.getChartData(currentChart);
- this.getJobs(currentChart);
- this.setupJobWebsocket(currentChart);
- }
- handleUninstallChart = () => {
- let { currentProject, currentCluster } = this.context;
- let { currentChart } = this.state;
- 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.closeChart();
- })
- .catch(console.log);
- };
- renderDeleteOverlay = () => {
- if (this.state.deleting) {
- return (
- <DeleteOverlay>
- <Loading />
- </DeleteOverlay>
- );
- }
- };
- render() {
- let { closeChart } = this.props;
- let { currentChart } = this.state;
- let chart = currentChart;
- return (
- <>
- <CloseOverlay onClick={closeChart} />
- <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>
- Run {this.state.jobs.length} times <Dot>•</Dot>Last run
- {" " + this.readableDate(chart.info.last_deployed)}
- </LastDeployed>
- </InfoWrapper>
- <TagWrapper>
- Namespace <NamespaceTag>{chart.namespace}</NamespaceTag>
- </TagWrapper>
- </TitleSection>
- <CloseButton onClick={closeChart}>
- <CloseButtonImg src={close} />
- </CloseButton>
- </HeaderWrapper>
- <BodyWrapper>
- <FormWrapper
- isReadOnly={this.state.imageIsPlaceholder}
- valuesToOverride={this.state.valuesToOverride}
- clearValuesToOverride={() =>
- this.setState({ valuesToOverride: {} })
- }
- formData={this.state.formData}
- tabOptions={this.state.tabOptions}
- isInModal={true}
- renderTabContents={this.renderTabContents}
- tabOptionsOnly={true}
- onSubmit={this.handleSaveValues}
- saveValuesStatus={this.state.saveValuesStatus}
- />
- </BodyWrapper>
- </StyledExpandedChart>
- </>
- );
- }
- }
- ExpandedJobChart.contextType = Context;
- const TextWrap = styled.div``;
- const Header = styled.div`
- font-weight: 500;
- color: #aaaabb;
- font-size: 16px;
- margin-bottom: 15px;
- `;
- const Placeholder = styled.div`
- height: 100%;
- padding: 30px;
- padding-bottom: 70px;
- font-size: 13px;
- color: #ffffff44;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- `;
- const Spinner = styled.img`
- width: 15px;
- height: 15px;
- margin-right: 12px;
- margin-bottom: -2px;
- `;
- const BodyWrapper = styled.div`
- width: 100%;
- height: 100%;
- overflow: hidden;
- `;
- 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 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: 24px 0px 17px 0px;
- height: 20px;
- `;
- 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);
- 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;
- overflow: hidden;
- flex-direction: column;
- @keyframes floatIn {
- from {
- opacity: 0;
- transform: translateY(30px);
- }
- to {
- opacity: 1;
- transform: translateY(0px);
- }
- }
- `;
|