github.com/replicatedhq/ship@v0.55.0/web/init/src/components/kustomize/kustomize_overlay/KustomizeOverlay.jsx (about)

     1  import React from "react";
     2  import classNames from "classnames";
     3  import AceEditor from "react-ace";
     4  import ReactTooltip from "react-tooltip"
     5  import * as yaml from "js-yaml";
     6  import isEmpty from "lodash/isEmpty";
     7  import sortBy from "lodash/sortBy";
     8  import pick from "lodash/pick";
     9  import keyBy from "lodash/keyBy";
    10  import find from "lodash/find";
    11  import trim from "lodash/trim";
    12  import findIndex from "lodash/findIndex";
    13  import map from "lodash/map";
    14  import defaultTo from "lodash/defaultTo";
    15  
    16  import FileTree from "./FileTree";
    17  import KustomizeModal from "./KustomizeModal";
    18  import Loader from "../../shared/Loader";
    19  import { AceEditorHOC, PATCH_TOKEN } from "./AceEditorHOC";
    20  import DiffEditor from "../../shared/DiffEditor";
    21  
    22  import "../../../../node_modules/brace/mode/yaml";
    23  import "../../../../node_modules/brace/theme/chrome";
    24  
    25  export const PATCH_OVERLAY = "PATCH";
    26  export const BASE_OVERLAY = "BASE";
    27  export const RESOURCE_OVERLAY = "RESOURCE";
    28  
    29  export default class KustomizeOverlay extends React.Component {
    30    constructor() {
    31      super();
    32      this.state = {
    33        fileTree: [],
    34        fileTreeBasePath: "",
    35        selectedFile: "",
    36        fileContents: {},
    37        fileLoadErr: false,
    38        fileLoadErrMessage: "",
    39        savePatchErr: false,
    40        savePatchErrorMessage: "",
    41        applyPatchErr: false,
    42        applyPatchErrorMessage: "",
    43        viewDiff: false,
    44        markers: [],
    45        patch: "",
    46        savingFinalize: false,
    47        displayConfirmModal: false,
    48        overlayToDelete: "",
    49        addingNewResource: false,
    50        newResourceName: "",
    51        lastSavedPatch: null,
    52        displayConfirmModalMessage: "",
    53        displayConfirmModalDiscardMessage: "",
    54        displayConfirmModalSubMessage: "",
    55        modalAction: this.discardOverlay,
    56      };
    57      this.addResourceWrapper = React.createRef();
    58      this.addResourceInput = React.createRef();
    59    }
    60  
    61    toggleModal = (overlayPath, overlayType) => {
    62      const displayConfirmModalSubMessage = "It will not be applied to the kustomization.yaml file that is generated for you.";
    63      let displayConfirmModalMessage = "Are you sure you want to discard this patch?";
    64      let displayConfirmModalDiscardMessage = "Discard patch";
    65  
    66      if (overlayType === BASE_OVERLAY) {
    67        displayConfirmModalMessage = "Are you sure you want to discard this base resource?";
    68        displayConfirmModalDiscardMessage = "Discard base";
    69      } else if (overlayType === RESOURCE_OVERLAY) {
    70        displayConfirmModalMessage = "Are you sure you want to discard this resource?";
    71        displayConfirmModalDiscardMessage = "Discard resource";
    72      }
    73  
    74      this.setState({
    75        displayConfirmModal: !this.state.displayConfirmModal,
    76        overlayToDelete: this.state.displayConfirmModal ? "" : overlayPath,
    77        displayConfirmModalMessage,
    78        displayConfirmModalDiscardMessage,
    79        displayConfirmModalSubMessage,
    80        modalAction: () => (this.discardOverlay(overlayType)),
    81      });
    82    }
    83  
    84    toggleModalForExcludedBase = (basePath) => {
    85      this.setState({
    86        displayConfirmModal: !this.state.displayConfirmModal,
    87        displayConfirmModalMessage: "Are you sure you want to include this base resource?",
    88        displayConfirmModalDiscardMessage: "Include base",
    89        displayConfirmModalSubMessage: "It will be included in the kustomization.yaml file that is generated for you.",
    90        modalAction: () => (this.includeBase(basePath)),
    91      });
    92    }
    93  
    94    includeBase = async(basePath) => {
    95      await this.props.includeBase(basePath);
    96      this.setState({ displayConfirmModal: false });
    97    }
    98  
    99    componentDidUpdate(lastProps, lastState) {
   100      const { currentStep } = this.props;
   101      this.rebuildTooltip();
   102      if (this.props.currentStep !== lastProps.currentStep && !isEmpty(this.props.currentStep)) {
   103        this.setFileTree(currentStep);
   104      }
   105      if (this.props.fileContents !== lastProps.fileContents && !isEmpty(this.props.fileContents)) {
   106        this.setState({ fileContents: keyBy(this.props.fileContents, "key") });
   107      }
   108      if (
   109        (this.state.viewDiff !== lastState.viewDiff) ||
   110        (this.state.patch !== lastState.patch) ||
   111        (this.state.selectedFile !== lastState.selectedFile)
   112      ) {
   113        this.aceEditorOverlay.editor.resize();
   114      }
   115      if (this.props.patch !== lastProps.patch) {
   116        this.setState({
   117          lastSavedPatch: this.state.lastSavedPatch !== null ? this.state.lastSavedPatch : this.props.patch,
   118          patch: this.props.patch
   119        });
   120      }
   121    }
   122  
   123    componentDidMount() {
   124      const { currentStep } = this.props;
   125      if (currentStep && !isEmpty(currentStep)) {
   126        this.setFileTree(currentStep);
   127      }
   128      if (this.props.fileContents && !isEmpty(this.props.fileContents)) {
   129        this.setState({ fileContents: keyBy(this.props.fileContents, "key") });
   130      }
   131    }
   132  
   133    handleApplyPatch = async () => {
   134      const { selectedFile, fileTreeBasePath } = this.state;
   135      const contents = this.aceEditorOverlay.editor.getValue();
   136  
   137      const applyPayload = {
   138        resource: `${fileTreeBasePath}${selectedFile}`,
   139        patch: contents,
   140      };
   141      await this.props.applyPatch(applyPayload)
   142        .catch((err) => {
   143          this.setState({
   144            applyPatchErr: true,
   145            applyPatchErrorMessage: err.message
   146          });
   147  
   148          setTimeout(() => {
   149            this.setState({
   150              applyPatchErr: false,
   151              applyPatchErrorMessage: ""
   152            });
   153          }, 3000);
   154        });
   155    }
   156  
   157    toggleDiff = async () => {
   158      const { patch, modified } = this.props;
   159      const hasPatchButNoModified = patch.length > 0 && modified.length === 0;
   160      if (hasPatchButNoModified) {
   161        await this.handleApplyPatch().catch();
   162      }
   163  
   164      this.setState({ viewDiff: !this.state.viewDiff });
   165    }
   166  
   167    createOverlay = () => {
   168      const { selectedFile } = this.state;
   169      let file = find(this.props.fileContents, ["key", selectedFile]);
   170      if (!file) return;
   171      const files = yaml.safeLoadAll(file.baseContent);
   172      let overlayFields = map(files, (file) => {
   173        return pick(file, "apiVersion", "kind", "metadata.name")
   174      });
   175      if (files.length === 1) {
   176        overlayFields = overlayFields[0];
   177      }
   178      const overlay = yaml.safeDump(overlayFields);
   179      this.setState({ patch: `--- \n${overlay}` });
   180    }
   181  
   182    setSelectedFile = async (path) => {
   183      const { lastSavedPatch, patch } = this.state;
   184  
   185      let canChangeFile = !lastSavedPatch || patch === lastSavedPatch || confirm("You have unsaved changes in the patch. If you proceed, you will lose any of the changes you've made.");
   186      if (canChangeFile) {
   187        this.setState({ selectedFile: path, lastSavedPatch: null });
   188        await this.props.getFileContent(path).then(() => {
   189          // set state with new file content
   190          this.setState({
   191            fileContents: keyBy(this.props.fileContents, "key"),
   192          });
   193        });
   194      }
   195    }
   196  
   197    handleFinalize = async () => {
   198      const {
   199        finalizeKustomizeOverlay,
   200        finalizeStep,
   201        history,
   202        isNavcycle,
   203        actions,
   204        startPoll,
   205        routeId,
   206        pollCallback
   207      } = this.props;
   208  
   209      if (isNavcycle) {
   210        await finalizeStep({ action: actions[0] });
   211        startPoll(routeId, pollCallback);
   212      } else {
   213        await finalizeKustomizeOverlay()
   214          .then(() => {
   215            this.setState({ savingFinalize: false });
   216            history.push("/");
   217          }).catch();
   218      }
   219    }
   220  
   221    discardOverlay = async (overlayType) => {
   222      const { overlayToDelete } = this.state;
   223      await this.deleteOverlay(overlayToDelete, overlayType);
   224      this.setState({
   225        patch: "",
   226        displayConfirmModal: false,
   227        lastSavedPatch: null
   228      });
   229    }
   230  
   231    deleteOverlay = async (path, overlayType) => {
   232      const { fileTree, selectedFile } = this.state;
   233      const isResource = overlayType === RESOURCE_OVERLAY;
   234      const isBase = overlayType === BASE_OVERLAY;
   235      const overlays = find(fileTree, { name: "overlays" });
   236      const overlayExists = overlays && findIndex(overlays.children, { path }) > -1;
   237  
   238      if (isResource) {
   239        await this.props.deleteOverlay(path, "resource");
   240        return;
   241      }
   242  
   243      if (isBase) {
   244        if (selectedFile === path) {
   245          this.setState({ selectedFile: "" });
   246        }
   247        await this.props.deleteOverlay(path, "base");
   248        return;
   249      }
   250  
   251      if (overlayExists) {
   252        await this.props.deleteOverlay(path, "patch");
   253        return;
   254      }
   255    }
   256  
   257    handleKustomizeSave = async (finalize) => {
   258      const { selectedFile, fileContents } = this.state;
   259      const { isResource } = fileContents[selectedFile];
   260      const contents = this.aceEditorOverlay.editor.getValue();
   261      this.setState({ patch: contents });
   262  
   263      const payload = {
   264        path: selectedFile,
   265        contents,
   266        isResource
   267      };
   268  
   269      if (!isResource) await this.handleApplyPatch();
   270      await this.props.saveKustomizeOverlay(payload)
   271        .then(() => {
   272          this.setState({ lastSavedPatch: null });
   273        })
   274        .catch((err) => {
   275          this.setState({
   276            savePatchErr: true,
   277            savePatchErrorMessage: err.message
   278          });
   279  
   280          setTimeout(() => {
   281            this.setState({
   282              savePatchErr: false,
   283              savePatchErrorMessage: ""
   284            });
   285          }, 3000);
   286        });
   287      await this.props.getCurrentStep();
   288      if (finalize) {
   289        this.setState({ savingFinalize: true, addingNewResource: false });
   290        this.handleFinalize();
   291      }
   292    }
   293  
   294    handleCreateResource = async () => {
   295      const { newResourceName } = this.state;
   296      const contents = "\n"; // Cannot be empty
   297      this.setState({ patch: contents });
   298  
   299      const payload = {
   300        path: `/${newResourceName}`,
   301        contents,
   302        isResource: true
   303      };
   304  
   305      await this.props.saveKustomizeOverlay(payload)
   306        .then(() => {
   307          this.setSelectedFile(`/${newResourceName}`);
   308          this.setState({ addingNewResource: false, newResourceName: "" })
   309        })
   310        .catch((err) => {
   311          this.setState({
   312            savePatchErr: true,
   313            savePatchErrorMessage: err.message
   314          });
   315  
   316          setTimeout(() => {
   317            this.setState({
   318              savePatchErr: false,
   319              savePatchErrorMessage: ""
   320            });
   321          }, 3000);
   322        });
   323      await this.props.getCurrentStep();
   324    }
   325  
   326    handleGeneratePatch = async (path) => {
   327      const current = this.aceEditorOverlay.editor.getValue();
   328      const { selectedFile, fileTreeBasePath } = this.state;
   329      this.setState({ lastSavedPatch: null })
   330      const payload = {
   331        original: selectedFile,
   332        current,
   333        path,
   334        resource: `${fileTreeBasePath}${selectedFile}`,
   335      };
   336      await this.props.generatePatch(payload);
   337  
   338      const position = this.aceEditorOverlay.editor.find(PATCH_TOKEN); // Find text for position
   339      if(position) {
   340        this.aceEditorOverlay.editor.focus();
   341        this.aceEditorOverlay.editor.gotoLine(position.start.row + 1, position.start.column);
   342        this.aceEditorOverlay.editor.find(PATCH_TOKEN); // Have to find text again to auto focus text
   343      }
   344    }
   345  
   346    rebuildTooltip = () => {
   347      // We need to rebuild these because...well I dunno why but if you don't the tooltips will not be visible after toggling the overlay editor.
   348      ReactTooltip.rebuild();
   349      ReactTooltip.hide();
   350    }
   351  
   352    setFileTree = ({ kustomize }) => {
   353      if (!kustomize.tree) return;
   354      const sortedTree = sortBy(kustomize.tree.children, (dir) => {
   355        dir.children ? dir.children.length : 0
   356      });
   357  
   358      this.setState({
   359        fileTree: sortedTree,
   360        fileTreeBasePath: kustomize.basePath
   361      });
   362    }
   363  
   364    setAceEditor = (editor) => {
   365      this.aceEditorOverlay = editor;
   366    }
   367  
   368    updateModifiedPatch = (patch, isResource) => {
   369      // We already circumvent React's lifecycle state system for updates
   370      // Set the current patch state to the changed value to avoid
   371      // React re-rendering the ACE Editor
   372      if (!isResource) {
   373        this.state.patch = patch; // eslint-disable-line
   374      }
   375    };
   376  
   377    handleAddResourceClick = async () => {
   378      // Ref input won't focus until state has been set
   379      await this.setState({ addingNewResource: true });
   380      this.addResourceInput.current.focus();
   381      window.addEventListener("click", this.handleClickOutsideResourceInput);
   382    }
   383  
   384    handleClickOutsideResourceInput = (e) => {
   385      const { addingNewResource } = this.state;
   386      if (addingNewResource && !this.addResourceWrapper.current.contains(e.target)) {
   387        this.setState({ addingNewResource: false, newResourceName: "" });
   388        window.removeEventListener("click", this.handleClickOutsideResourceInput);
   389      }
   390    }
   391  
   392    handleCreateNewResource = (e) => {
   393      if (e.charCode === 13) {
   394        this.handleCreateResource()
   395      }
   396    }
   397  
   398    render() {
   399      const { dataLoading, modified, firstRoute, goBack } = this.props;
   400      const {
   401        fileTree,
   402        selectedFile,
   403        fileLoadErr,
   404        fileLoadErrMessage,
   405        patch,
   406        savingFinalize,
   407        fileContents,
   408        addingNewResource,
   409        newResourceName,
   410        modalAction,
   411        applyPatchErr,
   412        applyPatchErrorMessage,
   413        savePatchErr,
   414        savePatchErrorMessage
   415      } = this.state;
   416      const fileToView = defaultTo(find(fileContents, ["key", selectedFile]), {});
   417      const showOverlay = patch.length;
   418      const showBase = !fileToView.isResource;
   419  
   420      return (
   421        <div className="flex flex1">
   422          <div className="u-minHeight--full u-minWidth--full flex-column flex1 u-position--relative">
   423            <div className="flex flex1 u-minHeight--full u-height--full">
   424              <div className="flex-column flex1 Sidebar-wrapper u-overflow--hidden">
   425                <div className="flex-column flex1">
   426                  <div className="flex1 u-overflow--auto u-background--biscay">
   427                    <div className="flex1 dirtree-wrapper u-overflow--hidden flex-column">
   428                      {fileTree.map((tree, i) => {
   429                          const id = `sub-dir-${tree.name}-${tree.children.length}-${tree.path}-${i}`;
   430                          return (
   431                            <div
   432                              key={id}
   433                              className={classNames("u-overflow--auto FileTree-wrapper u-position--relative dirtree", {
   434                                "flex-auto has-border": i > 0,
   435                                "flex-0-auto": i <= 0
   436                              })}>
   437                                <input
   438                                  type="checkbox"
   439                                  name={id}
   440                                  id={id}
   441                                  defaultChecked={true}
   442                                />
   443                                <label htmlFor={id}>
   444                                  {tree.name === "/" ? "base" : tree.name}
   445                                </label>
   446                                <FileTree
   447                                  files={tree.children}
   448                                  basePath={tree.name}
   449                                  handleFileSelect={(path) => this.setSelectedFile(path)}
   450                                  handleDeleteOverlay={this.toggleModal}
   451                                  handleClickExcludedBase={this.toggleModalForExcludedBase}
   452                                  selectedFile={this.state.selectedFile}
   453                                  isOverlayTree={tree.name === "overlays"}
   454                                  isResourceTree={tree.name === "resources"}
   455                                  isBaseTree={tree.name === "/"}
   456                                />
   457                            </div>
   458                          );
   459                        })}
   460                      <div className="add-new-resource u-position--relative" ref={this.addResourceWrapper}>
   461                        <input
   462                          type="text"
   463                          className={`Input add-resource-name-input u-position--absolute ${!addingNewResource ? "u-visibility--hidden" : ""}`}
   464                          name="new-resource"
   465                          placeholder="filename.yaml"
   466                          onChange={(e) => { this.setState({ newResourceName: e.target.value }) }}
   467                          onKeyPress={(e) => { this.handleCreateNewResource(e) }}
   468                          value={newResourceName}
   469                          ref={this.addResourceInput}
   470                        />
   471                        <p
   472                          className={`add-resource-link u-position--absolute u-marginTop--small u-marginLeft--normal u-cursor--pointer u-fontSize--small u-color--silverSand u-fontWeight--bold ${addingNewResource ? "u-visibility--hidden" : ""}`}
   473                          onClick={this.handleAddResourceClick}
   474                        >+ Add Resource
   475                        </p>
   476                      </div>
   477                    </div>
   478                  </div>
   479                </div>
   480              </div>
   481              <div className="flex-column flex1 u-height--auto u-overflow--hidden LayoutContent-wrapper u-position--relative">
   482                <div className="flex flex1 u-position--relative">
   483  
   484                  <div className={`flex-column flex1 base-editor-wrapper ${showOverlay && "u-paddingRight--15"} ${showBase ? "visible" : ""}`}>
   485                    <div className="flex1 flex-column u-position--relative">
   486                      {fileLoadErr ?
   487                        <div className="flex-column flex1 alignItems--center justifyContent--center">
   488                          <p className="u-color--chestnut u-fontSize--normal u-fontWeight--medium">Oops, we ran into a probelm getting that file, <span className="u-fontWeight--bold">{fileLoadErrMessage}</span></p>
   489                        </div>
   490                        : dataLoading.fileContentLoading ?
   491                          <div className="flex-column flex1 alignItems--center justifyContent--center">
   492                            <Loader size="50" color="#337AB7" />
   493                          </div>
   494                          :
   495                          <div className="flex1 flex-column">
   496                            <div className="u-paddingLeft--20 u-paddingRight--20 u-paddingTop--20">
   497                              <p className="u-marginBottom--normal u-fontSize--large u-color--tuna u-fontWeight--bold">Base YAML</p>
   498                              <p className="u-fontSize--small u-lineHeight--more u-fontWeight--medium u-color--doveGray">Select a file to be used as the base YAML. You can then click the edit icon on the top right to create a patch for that file.</p>
   499                            </div>
   500                            {selectedFile !== "" ?
   501                              <div className="flex1 file-contents-wrapper AceEditor--wrapper">
   502                                {!showOverlay &&
   503                                  <div data-tip="create-overlay-tooltip" data-for="create-overlay-tooltip" className="overlay-toggle u-cursor--pointer" onClick={this.createOverlay}>
   504                                    <span className="icon clickable u-overlayCreateIcon"></span>
   505                                  </div>
   506                                }
   507                                <ReactTooltip id="create-overlay-tooltip" effect="solid" className="replicated-tooltip">Create patch</ReactTooltip>
   508                                <AceEditorHOC
   509                                  handleGeneratePatch={this.handleGeneratePatch}
   510                                  handleApplyPatch={this.handleApplyPatch}
   511                                  fileToView={fileToView}
   512                                  diffOpen={this.state.viewDiff}
   513                                  overlayOpen={showOverlay}
   514                                />
   515                              </div>
   516                              :
   517                              <div className="flex1 flex-column empty-file-wrapper alignItems--center justifyContent--center">
   518                                <p className="u-fontSize--small u-fontWeight--medium u-color--dustyGray">No file selected.</p>
   519                              </div>
   520                            }
   521                          </div>
   522                      }
   523                    </div>
   524                  </div>
   525  
   526                  <div className={`flex-column flex1 overlays-editor-wrapper ${showOverlay ? "visible" : ""}`}>
   527                    <div className="u-paddingLeft--20 u-paddingRight--20 u-paddingTop--20">
   528                      <p className="u-marginBottom--normal u-fontSize--large u-color--tuna u-fontWeight--bold">{showBase ? "Patch" : "Resource"}</p>
   529                      <p className="u-fontSize--small u-lineHeight--more u-fontWeight--medium u-color--doveGray">This file will be applied as a patch to the base manifest. Edit the values that you want patched. The current file you're editing will be automatically saved when you open a new file.</p>
   530                    </div>
   531                    <div className="flex1 flex-column file-contents-wrapper u-position--relative">
   532                      <div className="flex1 AceEditor--wrapper">
   533                        {showOverlay && showBase ? <span data-tip="close-overlay-tooltip" data-for="close-overlay-tooltip" className="icon clickable u-closeOverlayIcon" onClick={() => this.toggleModal(this.state.selectedFile, PATCH_OVERLAY)}></span> : null}
   534                        <ReactTooltip id="close-overlay-tooltip" effect="solid" className="replicated-tooltip">Discard patch</ReactTooltip>
   535                        <AceEditor
   536                          ref={this.setAceEditor}
   537                          mode="yaml"
   538                          theme="chrome"
   539                          className="flex1 flex acePatchEditor"
   540                          value={trim(patch)}
   541                          height="100%"
   542                          width="100%"
   543                          editorProps={{
   544                            $blockScrolling: Infinity,
   545                            useSoftTabs: true,
   546                            tabSize: 2,
   547                          }}
   548                          debounceChangePeriod={1000}
   549                          setOptions={{
   550                            scrollPastEnd: false
   551                          }}
   552                          onChange={(patch) => this.updateModifiedPatch(patch, fileToView.isResource)}
   553                        />
   554                      </div>
   555                    </div>
   556                  </div>
   557                </div>
   558  
   559                {showOverlay && showBase ?
   560                  <div className={`${this.state.viewDiff ? "flex1" : "flex-auto"} flex-column`}>
   561                    <div className="diff-viewer-wrapper flex-column flex1">
   562                      <span className="diff-toggle" onClick={this.toggleDiff}>{this.state.viewDiff ? "Hide diff" : "Show diff"}</span>
   563                      {this.state.viewDiff &&
   564                        <DiffEditor
   565                          diffTitle="Diff YAML"
   566                          diffSubCopy="Here you can see the diff of the base YAML, and the finalized version with the overlay applied."
   567                          original={fileToView.baseContent}
   568                          updated={this.props.modified}
   569                        />
   570                      }
   571                    </div>
   572                  </div>
   573                  : null}
   574  
   575                <div className="flex-auto flex layout-footer-actions less-padding">
   576                  <div className="flex flex1">
   577                    {firstRoute ? null :
   578                      <div className="flex-auto u-marginRight--normal">
   579                        <button className="btn secondary" onClick={() => goBack()}>Back</button>
   580                      </div>
   581                    }
   582                    <div className="flex-column flex-verticalCenter">
   583                      <p className="u-margin--none u-marginRight--30 u-fontSize--small u-color--dustyGray u-fontWeight--normal">Contributed by <a target="_blank" rel="noopener noreferrer" href="https://replicated.com" className="u-fontWeight--medium u-color--astral u-textDecoration--underlineOnHover">Replicated</a></p>
   584                    </div>
   585                  </div>
   586                  <div className="flex1 flex alignItems--center justifyContent--flexEnd">
   587                    {selectedFile === "" ?
   588                      <button type="button" onClick={this.props.skipKustomize} className="btn primary">Continue</button>
   589                      :
   590                      <div className="flex">
   591                        {applyPatchErr && <span className="flex flex1 u-fontSize--small u-fontWeight--medium u-color--chestnut u-marginRight--20 alignItems--center">{ applyPatchErrorMessage }</span>}
   592                        {savePatchErr && <span className="flex flex1 u-fontSize--small u-fontWeight--medium u-color--chestnut u-marginRight--20 alignItems--center">{ savePatchErrorMessage }</span>}
   593                        <button type="button" disabled={dataLoading.saveKustomizeLoading || patch === "" || savingFinalize} onClick={() => this.handleKustomizeSave(false)} className="btn primary save-btn u-marginRight--normal">{dataLoading.saveKustomizeLoading && !savingFinalize ? "Saving patch" : "Save patch"}</button>
   594                        {patch === "" ?
   595                          <button type="button" onClick={this.props.skipKustomize} className="btn primary">Continue</button>
   596                          :
   597                          <button type="button" disabled={dataLoading.saveKustomizeLoading || savingFinalize} onClick={() => this.handleKustomizeSave(true)} className="btn secondary finalize-btn">{savingFinalize ? "Finalizing overlay" : "Save & continue"}</button>
   598                        }
   599                      </div>
   600                    }
   601                  </div>
   602                </div>
   603  
   604              </div>
   605            </div>
   606          </div>
   607          <KustomizeModal
   608            isOpen={this.state.displayConfirmModal}
   609            onRequestClose={this.toggleModal}
   610            discardOverlay={modalAction}
   611            message={this.state.displayConfirmModalMessage}
   612            subMessage={this.state.displayConfirmModalSubMessage}
   613            discardMessage={this.state.displayConfirmModalDiscardMessage}
   614          />
   615        </div>
   616      );
   617    }
   618  }