|
|
@@ -1,10 +1,11 @@
|
|
|
import React, { Component } from "react";
|
|
|
import styled from "styled-components";
|
|
|
import randomWords from "random-words";
|
|
|
-import posthog from "posthog-js";
|
|
|
import _ from "lodash";
|
|
|
import { Context } from "shared/Context";
|
|
|
import api from "shared/api";
|
|
|
+import close from "assets/close.png";
|
|
|
+import { RouteComponentProps, withRouter } from "react-router";
|
|
|
|
|
|
import {
|
|
|
ActionConfigType,
|
|
|
@@ -20,10 +21,12 @@ import SaveButton from "components/SaveButton";
|
|
|
import ActionConfEditor from "components/repo-selector/ActionConfEditor";
|
|
|
import ValuesWrapper from "components/values-form/ValuesWrapper";
|
|
|
import ValuesForm from "components/values-form/ValuesForm";
|
|
|
+import RadioSelector from "components/RadioSelector";
|
|
|
import { isAlphanumeric } from "shared/common";
|
|
|
|
|
|
-type PropsType = {
|
|
|
+type PropsType = RouteComponentProps & {
|
|
|
currentTemplate: any;
|
|
|
+ currentTab: string;
|
|
|
hideLaunch: () => void;
|
|
|
values: any;
|
|
|
form: any;
|
|
|
@@ -47,26 +50,29 @@ type StateType = {
|
|
|
namespaceOptions: { label: string; value: string }[];
|
|
|
actionConfig: ActionConfigType;
|
|
|
branch: string;
|
|
|
- pathIsSet: boolean;
|
|
|
+ repoType: string;
|
|
|
+ dockerfilePath: string | null;
|
|
|
+ folderPath: string | null;
|
|
|
+ selectedRegistry: any | null;
|
|
|
+ env: any;
|
|
|
};
|
|
|
|
|
|
const defaultActionConfig: ActionConfigType = {
|
|
|
git_repo: "",
|
|
|
image_repo_uri: "",
|
|
|
git_repo_id: 0,
|
|
|
- dockerfile_path: "",
|
|
|
};
|
|
|
|
|
|
-export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
+class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
state = {
|
|
|
currentView: "repo",
|
|
|
clusterOptions: [] as { label: string; value: string }[],
|
|
|
clusterMap: {} as { [clusterId: string]: ClusterType },
|
|
|
- saveValuesStatus: "No container image specified" as string | null,
|
|
|
+ saveValuesStatus: "" as string | null,
|
|
|
selectedCluster: this.context.currentCluster.name,
|
|
|
selectedNamespace: "default",
|
|
|
selectedImageUrl: "" as string | null,
|
|
|
- sourceType: "registry",
|
|
|
+ sourceType: "",
|
|
|
templateName: "",
|
|
|
selectedTag: "" as string | null,
|
|
|
tabOptions: [] as ChoiceType[],
|
|
|
@@ -75,21 +81,34 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
namespaceOptions: [] as { label: string; value: string }[],
|
|
|
actionConfig: { ...defaultActionConfig },
|
|
|
branch: "",
|
|
|
- pathIsSet: false,
|
|
|
+ repoType: "",
|
|
|
+ dockerfilePath: null as string | null,
|
|
|
+ folderPath: null as string | null,
|
|
|
+ selectedRegistry: null as any | null,
|
|
|
+ env: {},
|
|
|
};
|
|
|
|
|
|
createGHAction = (chartName: string, chartNamespace: string) => {
|
|
|
let { currentProject, currentCluster } = this.context;
|
|
|
let { actionConfig } = this.state;
|
|
|
+ let imageRepoUri = `${this.state.selectedRegistry.url}/${chartName}-${chartNamespace}`;
|
|
|
+
|
|
|
+ // DockerHub registry integration is per repo
|
|
|
+ if (this.state.selectedRegistry.service === "dockerhub") {
|
|
|
+ imageRepoUri = this.state.selectedRegistry.url;
|
|
|
+ }
|
|
|
|
|
|
api
|
|
|
.createGHAction(
|
|
|
"<token>",
|
|
|
{
|
|
|
git_repo: actionConfig.git_repo,
|
|
|
- image_repo_uri: actionConfig.image_repo_uri,
|
|
|
- dockerfile_path: actionConfig.dockerfile_path,
|
|
|
+ registry_id: this.state.selectedRegistry.id,
|
|
|
+ dockerfile_path: this.state.dockerfilePath,
|
|
|
+ folder_path: this.state.folderPath,
|
|
|
+ image_repo_uri: imageRepoUri,
|
|
|
git_repo_id: actionConfig.git_repo_id,
|
|
|
+ env: this.state.env,
|
|
|
},
|
|
|
{
|
|
|
project_id: currentProject.id,
|
|
|
@@ -131,23 +150,23 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
}
|
|
|
)
|
|
|
.then((_) => {
|
|
|
- if (this.state.sourceType === "repo") {
|
|
|
- this.createGHAction(name, this.state.selectedNamespace);
|
|
|
- }
|
|
|
// this.props.setCurrentView('cluster-dashboard');
|
|
|
this.setState({ saveValuesStatus: "successful" }, () => {
|
|
|
// redirect to dashboard
|
|
|
- });
|
|
|
- posthog.capture("Deployed template", {
|
|
|
- name: this.props.currentTemplate.name,
|
|
|
- namespace: this.state.selectedNamespace,
|
|
|
- values: values,
|
|
|
+ setTimeout(() => {
|
|
|
+ this.props.history.push("cluster-dashboard");
|
|
|
+ }, 500);
|
|
|
+ window.analytics.track("Deployed Add-on", {
|
|
|
+ name: this.props.currentTemplate.name,
|
|
|
+ namespace: this.state.selectedNamespace,
|
|
|
+ values: values,
|
|
|
+ });
|
|
|
});
|
|
|
})
|
|
|
.catch((err) => {
|
|
|
this.setState({ saveValuesStatus: "error" });
|
|
|
setCurrentError(err.response.data.errors[0]);
|
|
|
- posthog.capture("Failed to deploy template", {
|
|
|
+ window.analytics.track("Failed to Deploy Add-on", {
|
|
|
name: this.props.currentTemplate.name,
|
|
|
namespace: this.state.selectedNamespace,
|
|
|
values: values,
|
|
|
@@ -156,14 +175,14 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
- onSubmit = (rawValues: any) => {
|
|
|
+ onSubmit = async (rawValues: any) => {
|
|
|
let { currentCluster, currentProject } = this.context;
|
|
|
let name =
|
|
|
this.state.templateName || randomWords({ exactly: 3, join: "-" });
|
|
|
this.setState({ saveValuesStatus: "loading" });
|
|
|
|
|
|
// Convert dotted keys to nested objects
|
|
|
- let values = {};
|
|
|
+ let values : any = {};
|
|
|
for (let key in rawValues) {
|
|
|
_.set(values, key, rawValues[key]);
|
|
|
}
|
|
|
@@ -206,16 +225,35 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
}
|
|
|
|
|
|
_.set(values, "ingress.provider", provider);
|
|
|
+ var url : string
|
|
|
+
|
|
|
+ // check if template is docker and create external domain if necessary
|
|
|
+ if (this.props.currentTemplate.name == "web") {
|
|
|
+ if (values?.ingress?.enabled && values?.ingress?.hosts?.length == 0) {
|
|
|
+ url = await new Promise((resolve, reject) => {
|
|
|
+ api.createSubdomain(
|
|
|
+ "<token>",
|
|
|
+ {
|
|
|
+ release_name: this.props.currentTemplate.name.toLowerCase().trim(),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: currentProject.id,
|
|
|
+ cluster_id: currentCluster.id,
|
|
|
+ }
|
|
|
+ ).then((res) => {
|
|
|
+ resolve(res.data?.external_url)
|
|
|
+ })
|
|
|
+ .catch((err) => {
|
|
|
+ this.setState({ saveValuesStatus: "error" });
|
|
|
+ });
|
|
|
+ })
|
|
|
+
|
|
|
+ values.ingress.hosts = [url]
|
|
|
+ values.ingress.custom_domain = true
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- console.log(`
|
|
|
- ${this.props.currentTemplate.name}\n
|
|
|
- ${this.state.selectedImageUrl}\n
|
|
|
- ${values}\n
|
|
|
- ${this.state.selectedNamespace}\n
|
|
|
- ${name}\n
|
|
|
- ${currentProject.id}\n
|
|
|
- ${currentCluster.id}\n}
|
|
|
- `);
|
|
|
+ console.log("VALUES ARE", values)
|
|
|
|
|
|
api
|
|
|
.deployTemplate(
|
|
|
@@ -233,57 +271,121 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
cluster_id: currentCluster.id,
|
|
|
name: this.props.currentTemplate.name.toLowerCase().trim(),
|
|
|
version: "latest",
|
|
|
+ repo_url: process.env.APPLICATION_CHART_REPO_URL,
|
|
|
}
|
|
|
)
|
|
|
.then((_) => {
|
|
|
+ console.log("Deployed template.");
|
|
|
if (this.state.sourceType === "repo") {
|
|
|
+ console.log("Creating GHA");
|
|
|
this.createGHAction(name, this.state.selectedNamespace);
|
|
|
}
|
|
|
// this.props.setCurrentView('cluster-dashboard');
|
|
|
this.setState({ saveValuesStatus: "successful" }, () => {
|
|
|
// redirect to dashboard with namespace
|
|
|
+ setTimeout(() => {
|
|
|
+ this.props.history.push("cluster-dashboard");
|
|
|
+ }, 1000);
|
|
|
});
|
|
|
+ /*
|
|
|
try {
|
|
|
- posthog.capture("Deployed template", {
|
|
|
+ window.analytics.track("Deployed Application", {
|
|
|
name: this.props.currentTemplate.name,
|
|
|
namespace: this.state.selectedNamespace,
|
|
|
+ sourceType: this.state.sourceType,
|
|
|
values: values,
|
|
|
});
|
|
|
} catch (error) {
|
|
|
console.log(error);
|
|
|
}
|
|
|
+ */
|
|
|
})
|
|
|
.catch((err) => {
|
|
|
this.setState({ saveValuesStatus: "error" });
|
|
|
-
|
|
|
+ /*
|
|
|
try {
|
|
|
- posthog.capture("Failed to deploy template", {
|
|
|
+ window.analytics.track("Failed to Deploy Application", {
|
|
|
name: this.props.currentTemplate.name,
|
|
|
namespace: this.state.selectedNamespace,
|
|
|
+ sourceType: this.state.sourceType,
|
|
|
values: values,
|
|
|
error: err,
|
|
|
});
|
|
|
} catch (error) {
|
|
|
console.log(error);
|
|
|
}
|
|
|
+ */
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+ submitIsDisabled = () => {
|
|
|
+ let {
|
|
|
+ templateName,
|
|
|
+ sourceType,
|
|
|
+ selectedImageUrl,
|
|
|
+ dockerfilePath,
|
|
|
+ folderPath,
|
|
|
+ } = this.state;
|
|
|
+
|
|
|
+ // Allow if name is invalid
|
|
|
+ if (templateName.length > 0 && !isAlphanumeric(templateName)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.props.form?.hasSource) {
|
|
|
+ // Allow if source type is registry and image URL is specified
|
|
|
+ if (sourceType === "registry" && selectedImageUrl) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Allow if source type is repo and dockerfile or folder path is set
|
|
|
+ if (sourceType === "repo" && (dockerfilePath || folderPath)) {
|
|
|
+ return !this.state.selectedRegistry;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } else {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ getStatus = () => {
|
|
|
+ let {
|
|
|
+ selectedRegistry,
|
|
|
+ sourceType,
|
|
|
+ dockerfilePath,
|
|
|
+ folderPath,
|
|
|
+ } = this.state;
|
|
|
+
|
|
|
+ if (this.submitIsDisabled()) {
|
|
|
+ if (
|
|
|
+ sourceType === "repo" &&
|
|
|
+ (dockerfilePath || folderPath) &&
|
|
|
+ !selectedRegistry
|
|
|
+ ) {
|
|
|
+ return "A connected container registry is required";
|
|
|
+ }
|
|
|
+ let { templateName } = this.state;
|
|
|
+ if (templateName.length > 0 && !isAlphanumeric(templateName)) {
|
|
|
+ return "Template name contains illegal characters";
|
|
|
+ }
|
|
|
+ return "No application source specified";
|
|
|
+ } else {
|
|
|
+ return this.state.saveValuesStatus;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
renderTabContents = () => {
|
|
|
return (
|
|
|
<ValuesWrapper
|
|
|
formTabs={this.props.form?.tabs}
|
|
|
onSubmit={
|
|
|
- this.props.currentTemplate.name === "docker"
|
|
|
+ this.props.currentTab === "docker"
|
|
|
? this.onSubmit
|
|
|
: this.onSubmitAddon
|
|
|
}
|
|
|
- saveValuesStatus={this.state.saveValuesStatus}
|
|
|
- disabled={
|
|
|
- (this.state.templateName.length > 0 &&
|
|
|
- !isAlphanumeric(this.state.templateName)) ||
|
|
|
- (this.props.form?.hasSource ? !this.state.selectedImageUrl : false)
|
|
|
- }
|
|
|
+ saveValuesStatus={this.getStatus()}
|
|
|
+ disabled={this.submitIsDisabled()}
|
|
|
>
|
|
|
{(metaState: any, setMetaState: any) => {
|
|
|
return this.props.form?.tabs.map((tab: any, i: number) => {
|
|
|
@@ -292,6 +394,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
return (
|
|
|
<ValuesForm
|
|
|
metaState={metaState}
|
|
|
+ handleEnvChange={(x: any) => this.setState({ env: x })}
|
|
|
setMetaState={setMetaState}
|
|
|
key={tab.name}
|
|
|
sections={tab.sections}
|
|
|
@@ -366,11 +469,6 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
};
|
|
|
|
|
|
setSelectedImageUrl = (x: string) => {
|
|
|
- if (x === "") {
|
|
|
- this.setState({ saveValuesStatus: "No container image specified" });
|
|
|
- } else {
|
|
|
- this.setState({ saveValuesStatus: "" });
|
|
|
- }
|
|
|
this.setState({ selectedImageUrl: x });
|
|
|
};
|
|
|
|
|
|
@@ -390,6 +488,7 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
if (this.state.tabOptions.length > 0) {
|
|
|
return (
|
|
|
<>
|
|
|
+ <Heading>Additional Settings</Heading>
|
|
|
<Subtitle>
|
|
|
Configure additional settings for this template. (Optional)
|
|
|
</Subtitle>
|
|
|
@@ -428,18 +527,45 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
|
|
|
// Display if current template uses source (image or repo)
|
|
|
renderSourceSelectorContent = () => {
|
|
|
- if (this.state.sourceType === "registry") {
|
|
|
+ if (this.state.sourceType === "") {
|
|
|
return (
|
|
|
- <>
|
|
|
+ <BlockList>
|
|
|
+ <Block
|
|
|
+ onClick={() => {
|
|
|
+ this.setState({ sourceType: "repo" });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <BlockIcon src="https://3.bp.blogspot.com/-xhNpNJJyQhk/XIe4GY78RQI/AAAAAAAAItc/ouueFUj2Hqo5dntmnKqEaBJR4KQ4Q2K3ACK4BGAYYCw/s1600/logo%2Bgit%2Bicon.png" />
|
|
|
+ <BlockTitle>Git Repository</BlockTitle>
|
|
|
+ <BlockDescription>
|
|
|
+ Deploy using source from a Git repo.
|
|
|
+ </BlockDescription>
|
|
|
+ </Block>
|
|
|
+ <Block
|
|
|
+ onClick={() => {
|
|
|
+ this.setState({ sourceType: "registry" });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <BlockIcon src="https://cdn4.iconfinder.com/data/icons/logos-and-brands/512/97_Docker_logo_logos-512.png" />
|
|
|
+ <BlockTitle>Docker Registry</BlockTitle>
|
|
|
+ <BlockDescription>
|
|
|
+ Deploy a container from an image registry.
|
|
|
+ </BlockDescription>
|
|
|
+ </Block>
|
|
|
+ </BlockList>
|
|
|
+ );
|
|
|
+ } else if (this.state.sourceType === "registry") {
|
|
|
+ return (
|
|
|
+ <StyledSourceBox>
|
|
|
+ <CloseButton onClick={() => this.setState({ sourceType: "" })}>
|
|
|
+ <CloseButtonImg src={close} />
|
|
|
+ </CloseButton>
|
|
|
<Subtitle>
|
|
|
- Select the container image you would like to connect to this
|
|
|
- template
|
|
|
- {/* <Highlight onClick={() => this.setState({ sourceType: "repo" })}>
|
|
|
- link a git repository
|
|
|
- </Highlight> */}
|
|
|
- .<Required>*</Required>
|
|
|
+ Specify the container image you would like to connect to this
|
|
|
+ template.
|
|
|
+ <Required>*</Required>
|
|
|
</Subtitle>
|
|
|
- <DarkMatter />
|
|
|
+ <DarkMatter antiHeight="-4px" />
|
|
|
<ImageSelector
|
|
|
selectedTag={this.state.selectedTag}
|
|
|
selectedImageUrl={this.state.selectedImageUrl}
|
|
|
@@ -448,19 +574,48 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
forceExpanded={true}
|
|
|
/>
|
|
|
<br />
|
|
|
- </>
|
|
|
+ </StyledSourceBox>
|
|
|
+ );
|
|
|
+ } else if (this.state.repoType === "" && false) {
|
|
|
+ return (
|
|
|
+ <StyledSourceBox>
|
|
|
+ <CloseButton onClick={() => this.setState({ sourceType: "" })}>
|
|
|
+ <CloseButtonImg src={close} />
|
|
|
+ </CloseButton>
|
|
|
+ <Subtitle>
|
|
|
+ Are you using an existing Dockerfile from your repo?
|
|
|
+ <Required>*</Required>
|
|
|
+ </Subtitle>
|
|
|
+ <RadioSelector
|
|
|
+ options={[
|
|
|
+ {
|
|
|
+ value: "dockerfile",
|
|
|
+ label: "Yes, I am using an existing Dockerfile",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ value: "buildpack",
|
|
|
+ label: "No, I am not using an existing Dockerfile",
|
|
|
+ },
|
|
|
+ ]}
|
|
|
+ selected={this.state.repoType}
|
|
|
+ setSelected={(x: string) => this.setState({ repoType: x })}
|
|
|
+ />
|
|
|
+ </StyledSourceBox>
|
|
|
);
|
|
|
} else {
|
|
|
return (
|
|
|
- <>
|
|
|
+ <StyledSourceBox>
|
|
|
+ <CloseButton onClick={() => this.setState({ sourceType: "" })}>
|
|
|
+ <CloseButtonImg src={close} />
|
|
|
+ </CloseButton>
|
|
|
<Subtitle>
|
|
|
- Select a repo to connect to, then a Dockerfile to build from.
|
|
|
+ Provide a repo folder to use as source.
|
|
|
<Required>*</Required>
|
|
|
</Subtitle>
|
|
|
+ <DarkMatter antiHeight="-4px" />
|
|
|
<ActionConfEditor
|
|
|
actionConfig={this.state.actionConfig}
|
|
|
branch={this.state.branch}
|
|
|
- pathIsSet={this.state.pathIsSet}
|
|
|
setActionConfig={(actionConfig: ActionConfigType) =>
|
|
|
this.setState({ actionConfig }, () => {
|
|
|
this.setSelectedImageUrl(
|
|
|
@@ -469,40 +624,41 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
})
|
|
|
}
|
|
|
setBranch={(branch: string) => this.setState({ branch })}
|
|
|
- setPath={(pathIsSet: boolean) => this.setState({ pathIsSet })}
|
|
|
+ setDockerfilePath={(x: string) =>
|
|
|
+ this.setState({ dockerfilePath: x })
|
|
|
+ }
|
|
|
+ dockerfilePath={this.state.dockerfilePath}
|
|
|
+ folderPath={this.state.folderPath}
|
|
|
+ setFolderPath={(x: string) => this.setState({ folderPath: x })}
|
|
|
reset={() => {
|
|
|
this.setState({
|
|
|
actionConfig: { ...defaultActionConfig },
|
|
|
branch: "",
|
|
|
- pathIsSet: false,
|
|
|
+ dockerfilePath: null,
|
|
|
+ folderPath: null,
|
|
|
});
|
|
|
}}
|
|
|
+ setSelectedRegistry={(x: any) => {
|
|
|
+ this.setState({ selectedRegistry: x });
|
|
|
+ }}
|
|
|
+ selectedRegistry={this.state.selectedRegistry}
|
|
|
/>
|
|
|
<br />
|
|
|
- </>
|
|
|
+ </StyledSourceBox>
|
|
|
);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
renderSourceSelector = () => {
|
|
|
- if (!this.props.form?.hasSource) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
return (
|
|
|
<>
|
|
|
- <TabRegion
|
|
|
- options={[
|
|
|
- { label: "Registry", value: "registry" },
|
|
|
- { label: "Github", value: "repo" },
|
|
|
- ]}
|
|
|
- currentTab={this.state.sourceType}
|
|
|
- setCurrentTab={(x) => this.setState({ sourceType: x })}
|
|
|
- >
|
|
|
- <StyledSourceBox>
|
|
|
- {this.renderSourceSelectorContent()}
|
|
|
- </StyledSourceBox>
|
|
|
- </TabRegion>
|
|
|
+ <Heading>Deployment Method</Heading>
|
|
|
+ <Subtitle>
|
|
|
+ Choose the deployment method you would like to use for this
|
|
|
+ application.
|
|
|
+ <Required>*</Required>
|
|
|
+ </Subtitle>
|
|
|
+ {this.renderSourceSelectorContent()}
|
|
|
</>
|
|
|
);
|
|
|
};
|
|
|
@@ -513,19 +669,15 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
|
|
|
return (
|
|
|
<StyledLaunchTemplate>
|
|
|
- {name !== "docker" && (
|
|
|
- <HeaderSection>
|
|
|
- <i className="material-icons" onClick={this.props.hideLaunch}>
|
|
|
- keyboard_backspace
|
|
|
- </i>
|
|
|
- {icon
|
|
|
- ? this.renderIcon(icon)
|
|
|
- : this.renderIcon(currentTemplate.icon)}
|
|
|
- <Title>{name}</Title>
|
|
|
- </HeaderSection>
|
|
|
- )}
|
|
|
+ <HeaderSection>
|
|
|
+ <i className="material-icons" onClick={this.props.hideLaunch}>
|
|
|
+ keyboard_backspace
|
|
|
+ </i>
|
|
|
+ {icon ? this.renderIcon(icon) : this.renderIcon(currentTemplate.icon)}
|
|
|
+ <Title>{name}</Title>
|
|
|
+ </HeaderSection>
|
|
|
<DarkMatter antiHeight="-13px" />
|
|
|
- <Heading isAtTop={name !== "docker"}>Name</Heading>
|
|
|
+ <Heading isAtTop={true}>Name</Heading>
|
|
|
<Subtitle>
|
|
|
Randomly generated if left blank.
|
|
|
<Warning
|
|
|
@@ -545,6 +697,9 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
placeholder="ex: doctor-scientist"
|
|
|
width="100%"
|
|
|
/>
|
|
|
+
|
|
|
+ {this.props.form?.hasSource && this.renderSourceSelector()}
|
|
|
+
|
|
|
<Heading>Destination</Heading>
|
|
|
<Subtitle>
|
|
|
Specify the cluster and namespace you would like to deploy your
|
|
|
@@ -582,7 +737,6 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
closeOverlay={true}
|
|
|
/>
|
|
|
</ClusterSection>
|
|
|
- {this.renderSourceSelector()}
|
|
|
{this.renderSettingsRegion()}
|
|
|
</StyledLaunchTemplate>
|
|
|
);
|
|
|
@@ -590,6 +744,111 @@ export default class LaunchTemplate extends Component<PropsType, StateType> {
|
|
|
}
|
|
|
|
|
|
LaunchTemplate.contextType = Context;
|
|
|
+export default withRouter(LaunchTemplate);
|
|
|
+
|
|
|
+const Bold = styled.div`
|
|
|
+ font-weight: bold;
|
|
|
+ color: white;
|
|
|
+ margin-right: 5px;
|
|
|
+`;
|
|
|
+
|
|
|
+const CloseButton = styled.div`
|
|
|
+ position: absolute;
|
|
|
+ display: block;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ padding: 13px 0 12px 0;
|
|
|
+ z-index: 1;
|
|
|
+ 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 BlockIcon = styled.img<{ bw?: boolean }>`
|
|
|
+ height: 38px;
|
|
|
+ padding: 2px;
|
|
|
+ margin-top: 30px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ filter: ${(props) => (props.bw ? "grayscale(1)" : "")};
|
|
|
+`;
|
|
|
+
|
|
|
+const BlockDescription = styled.div`
|
|
|
+ margin-bottom: 12px;
|
|
|
+ color: #ffffff66;
|
|
|
+ text-align: center;
|
|
|
+ font-weight: default;
|
|
|
+ font-size: 13px;
|
|
|
+ padding: 0px 25px;
|
|
|
+ height: 2.4em;
|
|
|
+ font-size: 12px;
|
|
|
+ display: -webkit-box;
|
|
|
+ overflow: hidden;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+`;
|
|
|
+
|
|
|
+const BlockTitle = styled.div`
|
|
|
+ margin-bottom: 12px;
|
|
|
+ width: 80%;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 14px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+`;
|
|
|
+
|
|
|
+const Block = styled.div<{ disabled?: boolean }>`
|
|
|
+ align-items: center;
|
|
|
+ user-select: none;
|
|
|
+ border-radius: 5px;
|
|
|
+ display: flex;
|
|
|
+ font-size: 13px;
|
|
|
+ overflow: hidden;
|
|
|
+ font-weight: 500;
|
|
|
+ padding: 3px 0px 12px;
|
|
|
+ flex-direction: column;
|
|
|
+ align-item: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ height: 170px;
|
|
|
+ cursor: ${(props) => (props.disabled ? "" : "pointer")};
|
|
|
+ color: #ffffff;
|
|
|
+ position: relative;
|
|
|
+ background: #26282f;
|
|
|
+ box-shadow: 0 3px 5px 0px #00000022;
|
|
|
+ :hover {
|
|
|
+ background: ${(props) => (props.disabled ? "" : "#ffffff11")};
|
|
|
+ }
|
|
|
+
|
|
|
+ animation: fadeIn 0.3s 0s;
|
|
|
+ @keyframes fadeIn {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const BlockList = styled.div`
|
|
|
+ overflow: visible;
|
|
|
+ margin-top: 6px;
|
|
|
+ margin-bottom: 27px;
|
|
|
+ display: grid;
|
|
|
+ grid-column-gap: 25px;
|
|
|
+ grid-row-gap: 25px;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
+`;
|
|
|
|
|
|
const Title = styled.div`
|
|
|
font-size: 24px;
|
|
|
@@ -603,6 +862,7 @@ const Title = styled.div`
|
|
|
const HeaderSection = styled.div`
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
|
> i {
|
|
|
cursor: pointer;
|
|
|
@@ -621,7 +881,7 @@ const Heading = styled.div<{ isAtTop?: boolean }>`
|
|
|
font-weight: 500;
|
|
|
font-size: 16px;
|
|
|
margin-bottom: 5px;
|
|
|
- margin-top: ${(props) => (props.isAtTop ? "30px" : "10px")};
|
|
|
+ margin-top: ${(props) => (props.isAtTop ? "10px" : "30px")};
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
`;
|
|
|
@@ -634,6 +894,7 @@ const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>`
|
|
|
const Required = styled.div`
|
|
|
margin-left: 8px;
|
|
|
color: #fc4976;
|
|
|
+ display: inline-block;
|
|
|
`;
|
|
|
|
|
|
const Link = styled.a`
|
|
|
@@ -698,7 +959,8 @@ const NamespaceLabel = styled.div`
|
|
|
|
|
|
const Icon = styled.img`
|
|
|
width: 21px;
|
|
|
- margin-right: 10px;
|
|
|
+ margin-right: 6px;
|
|
|
+ margin-left: 10px;
|
|
|
`;
|
|
|
|
|
|
const Polymer = styled.div`
|
|
|
@@ -725,7 +987,7 @@ const ClusterSection = styled.div`
|
|
|
font-size: 14px;
|
|
|
margin-top: 2px;
|
|
|
font-weight: 500;
|
|
|
- margin-bottom: 22px;
|
|
|
+ margin-bottom: 32px;
|
|
|
|
|
|
> i {
|
|
|
font-size: 25px;
|
|
|
@@ -769,10 +1031,11 @@ const StyledSourceBox = styled.div`
|
|
|
height: 100%;
|
|
|
background: #ffffff11;
|
|
|
color: #ffffff;
|
|
|
- padding: 10px 35px 25px;
|
|
|
+ padding: 14px 35px 20px;
|
|
|
position: relative;
|
|
|
border-radius: 5px;
|
|
|
font-size: 13px;
|
|
|
+ margin-top: 6px;
|
|
|
overflow: auto;
|
|
|
margin-bottom: 25px;
|
|
|
`;
|