| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- import React, { Component } from "react";
- import styled from "styled-components";
- import _ from "lodash";
- import { Section, FormElement } from "shared/types";
- import { Context } from "shared/Context";
- import TabRegion from "components/TabRegion";
- import ValuesForm from "components/values-form/ValuesForm";
- import SaveButton from "../SaveButton";
- type PropsType = {
- formData: any;
- onSubmit?: (formValues: any) => void;
- saveValuesStatus?: string | null;
- saveButtonText?: string | null;
- // Handle additional non-form tabs
- // TODO: find cleaner way to share submitValues w/ rerun jobs button
- renderTabContents?: (currentTab: string, submitValues?: any) => any;
- tabOptions?: any[];
- tabOptionsOnly?: boolean;
- // Allow external control of state
- valuesToOverride?: any;
- clearValuesToOverride?: () => void;
- // External values made available to all child components
- externalValues?: any;
- // Display and debugger settings
- isInModal?: boolean;
- isReadOnly?: boolean;
- showStateDebugger?: boolean;
- // TabRegion props to pass through
- color?: string;
- addendum?: any;
- };
- type StateType = {
- metaState: any;
- requiredFields: string[];
- currentTab: string;
- tabOptions: { value: string; label: string }[];
- };
- /**
- * Renders from raw JSON form data and manages form state.
- *
- * To control values using external state prop in "valuesToOverride" (refer to
- * FormDebugger or LaunchTemplate for example usage).
- *
- * TODO: Handle passing in valuesToOverride at same time as formData
- */
- export default class FormWrapper extends Component<PropsType, StateType> {
- state = {
- metaState: {} as any,
- requiredFields: [] as string[],
- currentTab: "",
- tabOptions: [] as { value: string; label: string }[],
- };
- updateTabs = (resetState?: boolean, callback?: any) => {
- if (resetState) {
- let tabOptions = [] as { value: string; label: string }[];
- let tabs = this.props.formData?.tabs;
- let requiredFields = [] as string[];
- let metaState: any = {
- "currentCluster.service.is_gcp": {
- value: this.context.currentCluster.service == "gke",
- },
- "currentCluster.service.is_aws": {
- value: this.context.currentCluster.service == "eks",
- },
- "currentCluster.service.is_do": {
- value: this.context.currentCluster.service == "doks",
- },
- };
- tabs?.forEach((tab: any, i: number) => {
- // Exclude value if omitFromLaunch is set
- let omit =
- tab.settings?.omitFromLaunch && this.props.externalValues?.isLaunch;
- if (tab?.name && tab.label && !omit) {
- // If a tab is valid, extract state
- tab.sections?.forEach((section: Section, i: number) => {
- section?.contents?.forEach((item: FormElement, i: number) => {
- if (item === null || item === undefined) {
- return;
- }
- if (
- item.type === "variable" &&
- item.variable &&
- item.settings?.default
- ) {
- metaState[item.variable] = { value: item.settings.default };
- return;
- }
- // If no name is assigned use values.yaml variable as identifier
- let key = item.name || item.variable;
- let def =
- item.settings &&
- item.settings.unit &&
- !item.settings.omitUnitFromValue
- ? `${item.settings.default}${item.settings.unit}`
- : item.settings?.default;
- def = (item.value && item.value[0]) || def;
- if (item.type === "checkbox") {
- def = item.value && item.value[0];
- }
- // Handle add to list of required fields
- if (item.required && key) {
- requiredFields.push(key);
- }
- let value: any = def;
- switch (item.type) {
- case "checkbox":
- value = def || false;
- break;
- case "string-input":
- value = def || "";
- break;
- case "string-input-password":
- value = def || item.settings.default;
- case "array-input":
- value = def || [];
- break;
- case "env-key-value-array":
- value = def || {};
- break;
- case "key-value-array":
- value = def || {};
- break;
- case "number-input":
- value = def?.toString() ? def : "";
- break;
- case "select":
- value = def || item.settings.options[0].value;
- break;
- case "provider-select":
- let providerMap: any = {
- gke: "gcp",
- eks: "aws",
- doks: "do",
- };
- def = providerMap[this.context.currentCluster.service];
- value = def || "aws";
- break;
- case "base-64":
- value = def || "";
- case "base-64-password":
- value = def || "";
- default:
- }
- if (value !== null && value !== undefined) {
- metaState[key] = { value };
- }
- });
- });
- if (!this.props.tabOptionsOnly) {
- tabOptions.push({ value: tab.name, label: tab.label });
- }
- }
- });
- if (this.props.tabOptions?.length > 0) {
- let prependTabs = [] as { value: string; label: string }[];
- let appendTabs = [] as { value: string; label: string }[];
- this.props.tabOptions.forEach(
- (tab: { value: string; label: string }) => {
- if (tab.value === "status" || tab.value === "metrics") {
- prependTabs.push(tab);
- } else {
- appendTabs.push(tab);
- }
- }
- );
- tabOptions = prependTabs.concat(tabOptions.concat(appendTabs));
- }
- if (tabOptions.length > 0) {
- this.setState(
- {
- tabOptions: tabOptions,
- currentTab:
- this.state.currentTab === ""
- ? tabOptions[0].value
- : this.state.currentTab,
- metaState,
- requiredFields: requiredFields,
- },
- callback
- );
- } else {
- this.setState({ tabOptions }, callback);
- }
- } else {
- // TODO: refactor by consolidating w/ above
- // Handle change only to external tabs (e.g. DevOps mode toggle)
- let tabOptions = [] as { value: string; label: string }[];
- let tabs = this.props.formData?.tabs;
- tabs?.forEach((tab: any, i: number) => {
- if (tab?.name && tab.label) {
- tabOptions.push({ value: tab.name, label: tab.label });
- }
- });
- if (this.props.tabOptions?.length > 0) {
- let prependTabs = [] as { value: string; label: string }[];
- let appendTabs = [] as { value: string; label: string }[];
- this.props.tabOptions.forEach(
- (tab: { value: string; label: string }) => {
- if (tab.value === "status" || tab.value === "metrics") {
- prependTabs.push(tab);
- } else {
- appendTabs.push(tab);
- }
- }
- );
- tabOptions = prependTabs.concat(tabOptions.concat(appendTabs));
- }
- this.setState({ tabOptions }, callback);
- }
- };
- componentDidMount() {
- this.updateTabs(true, () => {
- this.setState(
- {
- metaState: {
- ...this.state.metaState,
- ...this.props.valuesToOverride,
- },
- },
- () => {
- this.props.clearValuesToOverride &&
- this.props.clearValuesToOverride();
- }
- );
- });
- }
- componentDidUpdate(prevProps: any) {
- // Override metaState values set from outside FormWrapper
- if (
- this.props.valuesToOverride &&
- !_.isEmpty(this.props.valuesToOverride) &&
- !_.isEqual(prevProps.valuesToOverride, this.props.valuesToOverride)
- ) {
- this.setState(
- {
- metaState: {
- ...this.state.metaState,
- ...this.props.valuesToOverride,
- },
- },
- () => {
- // Seems redundant with below but need to ensure no leaked state updates
- if (
- !_.isEqual(prevProps.tabOptions, this.props.tabOptions) ||
- !_.isEqual(prevProps.formData, this.props.formData)
- ) {
- let formHasChanged = !_.isEqual(
- prevProps.formData,
- this.props.formData
- );
- this.updateTabs(formHasChanged);
- }
- this.props.clearValuesToOverride &&
- this.props.clearValuesToOverride();
- }
- );
- } else if (
- !_.isEqual(prevProps.tabOptions, this.props.tabOptions) ||
- !_.isEqual(prevProps.formData, this.props.formData)
- ) {
- if (
- prevProps.tabOptions?.length === 0 &&
- !_.isEqual(prevProps.tabOptions, this.props.tabOptions)
- ) {
- this.setState({ currentTab: "status" });
- }
- let formHasChanged = !_.isEqual(prevProps.formData, this.props.formData);
- this.updateTabs(formHasChanged);
- }
- }
- isSet = (value: any) => {
- if (
- value === null ||
- value === undefined ||
- value === "" ||
- value === false
- ) {
- return false;
- }
- return true;
- };
- isDisabled = () => {
- if (this.props.saveValuesStatus == "loading") {
- return true;
- }
- let requiredMissing = false;
- this.state.requiredFields?.forEach((requiredKey: string, i: number) => {
- if (!this.isSet(this.state.metaState[requiredKey]?.value)) {
- requiredMissing = true;
- }
- });
- return requiredMissing;
- };
- renderTabContents = () => {
- let tabs = this.props.formData?.tabs;
- if (tabs) {
- let matchedTab = null as any;
- tabs.forEach((tab: any, i: number) => {
- if (tab?.name === this.state.currentTab) {
- matchedTab = tab;
- }
- });
- if (matchedTab) {
- return (
- <ValuesForm
- externalValues={this.props.externalValues}
- disabled={this.props.isReadOnly}
- metaState={this.state.metaState}
- setMetaState={(key: string, value: any) => {
- let metaState: any = this.state.metaState;
- metaState[key] = { value };
- this.setState({ metaState });
- }}
- sections={matchedTab.sections}
- />
- );
- }
- }
- // If no form tabs match, check against external tabs
- if (this.props.renderTabContents) {
- // TODO: find a cleaner way to share submissionValues w/ rerun button
- let submissionValues: any = {};
- Object.keys(this.state.metaState)?.forEach((key: string, i: number) => {
- submissionValues[key] = this.state.metaState[key]?.value;
- });
- return this.props.renderTabContents(
- this.state.currentTab,
- submissionValues
- );
- }
- return <div>No matched tabs found.</div>;
- };
- renderStateDebugger = () => {
- if (this.props.showStateDebugger) {
- return (
- <>
- <StateDisplay>
- <Header>FormWrapper State</Header>
- <ScrollWrapper>
- {JSON.stringify(this.state.metaState, undefined, 2)}
- </ScrollWrapper>
- </StateDisplay>
- </>
- );
- }
- };
- handleSubmit = () => {
- // Extract metaState values
- let submissionValues: any = {};
- Object.keys(this.state.metaState)?.forEach((key: string, i: number) => {
- submissionValues[key] = this.state.metaState[key]?.value;
- });
- this.props.onSubmit && this.props.onSubmit(submissionValues);
- };
- showSaveButton = (): boolean => {
- if (this.props.isReadOnly || this.state.tabOptions?.length === 0) {
- return false;
- }
- let tabs = this.props.formData?.tabs;
- if (tabs) {
- let matchedTab = null as any;
- tabs.forEach((tab: any, i: number) => {
- if (tab?.name === this.state.currentTab) {
- matchedTab = tab;
- }
- });
- if (matchedTab) {
- return true;
- }
- }
- // Check if current tab is among non-form tab options
- let nonFormTabValues = this.props.tabOptions?.map((tab: any, i: number) => {
- return tab.value;
- });
- if (nonFormTabValues && nonFormTabValues.includes(this.state.currentTab)) {
- return false;
- }
- return true;
- };
- renderContents = (showSave: boolean) => {
- return (
- <>
- <TabRegion
- options={this.state.tabOptions}
- currentTab={this.state.currentTab}
- setCurrentTab={(x: string) => this.setState({ currentTab: x })}
- addendum={this.props.addendum}
- color={this.props.color}
- >
- {this.renderTabContents()}
- </TabRegion>
- {showSave && (
- <SaveButton
- disabled={this.isDisabled()}
- text={this.props.saveButtonText || "Deploy"}
- onClick={this.handleSubmit}
- status={
- this.isDisabled() && this.props.saveValuesStatus != "loading"
- ? "Missing required fields"
- : this.props.saveValuesStatus
- }
- makeFlush={!this.props.isInModal}
- />
- )}
- {this.renderStateDebugger()}
- </>
- );
- };
- render() {
- let showSave = this.showSaveButton();
- return (
- <>
- {this.props.isInModal || !showSave ? (
- <StyledValuesWrapper showSave={showSave}>
- {this.renderContents(showSave)}
- </StyledValuesWrapper>
- ) : (
- <PaddedWrapper>
- <StyledValuesWrapper showSave={showSave}>
- {this.renderContents(showSave)}
- </StyledValuesWrapper>
- </PaddedWrapper>
- )}
- </>
- );
- }
- }
- FormWrapper.contextType = Context;
- const Spacer = styled.div`
- width: 100%;
- height: 200px;
- background: red;
- position: relative;
- `;
- const TabWrapper = styled.div`
- min-height: 100px;
- display: flex;
- align-items: center;
- justify-content: center;
- `;
- const ScrollWrapper = styled.div`
- padding: 20px;
- overflow-y: auto;
- max-height: 300px;
- padding-top: 15px;
- `;
- const Header = styled.div`
- width: 100%;
- height: 40px;
- color: #ffffff;
- font-weight: 500;
- padding-left: 17px;
- background: #00000022;
- display: flex;
- align-items: center;
- `;
- const StateDisplay = styled.pre`
- width: 100%;
- font-size: 13px;
- display:
- overflow: hidden;
- border-radius: 5px;
- position: relative;
- line-height: 1.5em;
- color: #aaaabb;
- background: #ffffff11;
- `;
- const StyledValuesWrapper = styled.div<{ showSave: boolean }>`
- width: 100%;
- padding: 0;
- height: ${(props) => (props.showSave ? "calc(100% - 55px)" : "100%")};
- `;
- const PaddedWrapper = styled.div`
- padding-bottom: 65px;
- position: relative;
- `;
|