diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e710af2..97fcffa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. +## 1.8.0 + +### Upgrade Notes + +- SCC v1 Terraform has been deprecated. SCC v1 resources can still be managed from `/form/securityComplianceCenter` +- Reafactored Routing Tables and Routing Table Routes Form +- OptionsForm has been removed and now is generated using the DynamicForm component +- Power VS Volume Attachments are no longer created in parallel to ensure that each provisions successfully + +### Features + +- Users can now create FalconStor VTL instances from the page `/form/vtl` +- Users can now set DNS Record Data to be the Primary IPV4 address for a VSI from the DNS Record form +- Users can now create SCC V2 Resources fomr the `/form/sccV2` page +- Users can now enable IP spoofing on the primary network interface for Virtual Servers +- Users can now create additional VPC address prefixes from the VPN Gateways page +- Users can now add VPN Gateway connections from the VPN Gateways page + +### Fixes + +- Fixed an issue causing Power VS Workspace save to be incorrectly disabled when changing images in that workspace +- Fixed an issue allowing users to add more than 3 subnets to a DNS custom resolver +- Fixed an issue allowing users to create DNS zones with duplicate names +- Fixed an issue causing Access Groups and Access Group Policies to have incorrect references to their parent access groups +- Fixed an issue causing Terraform output for COS authorization credentials to always have the role `Writer` regardless of selection + ## 1.7.1 ### Upgrade Notes diff --git a/client/package-lock.json b/client/package-lock.json index 405a4ffe..9aeb9cc7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,10 +16,10 @@ "carbon-react-code-mirror": "^0.2.0", "file-saver": "^2.0.5", "graphql": "15.5.2", - "icse-react-assets": "1.2.36", + "icse-react-assets": "1.2.37", "json-to-tf": "^0.3.1", "jszip": "^3.10.1", - "lazy-z": "^1.11.14", + "lazy-z": "^1.11.16", "react": "^18.0.0", "react-dom": "^18.0.0", "react-router-dom": "^6.3.0", @@ -13836,9 +13836,9 @@ } }, "node_modules/icse-react-assets": { - "version": "1.2.36", - "resolved": "https://registry.npmjs.org/icse-react-assets/-/icse-react-assets-1.2.36.tgz", - "integrity": "sha512-tn0ogCCMkIjQGOMmkCs27RXDEo7X0jNt7g0Il451/VHHkCg2+xqT95WaUuxjHh92JiDpcZjSMsy9n5sQ1nRkDg==", + "version": "1.2.37", + "resolved": "https://registry.npmjs.org/icse-react-assets/-/icse-react-assets-1.2.37.tgz", + "integrity": "sha512-ar60VQNPKgwUW6kcw9TnByuqWxkb9vqOgtR13GEoFv9WLCA/VIFIN7NMHkLQ7pSQkSG/mgRDYNGZK2o5+GgA4Q==", "dependencies": { "@carbon/icons-react": "^11.18.0", "@carbon/react": "^1.27.0", @@ -19214,9 +19214,9 @@ } }, "node_modules/lazy-z": { - "version": "1.11.14", - "resolved": "https://registry.npmjs.org/lazy-z/-/lazy-z-1.11.14.tgz", - "integrity": "sha512-3Nwia3ulmo9+jtt1k6uF+WWFr0o81E/1SWLAL+/BdSMjPbY/GyyxeQzl1wa0TVzR2SYiXuKVkMzagD16wO9MuA==", + "version": "1.11.16", + "resolved": "https://registry.npmjs.org/lazy-z/-/lazy-z-1.11.16.tgz", + "integrity": "sha512-YQ2MSmHowkGB+Cvu2c+R1hIZcW8km7oElxBcZcSAGNCMNnAQexlinBeEn6Enc13WbyC07wucUx1OqlQK7QtG8w==", "dependencies": { "regex-but-with-words": "^1.5.0" } diff --git a/client/package.json b/client/package.json index 7676877a..06ab9c80 100644 --- a/client/package.json +++ b/client/package.json @@ -22,10 +22,10 @@ "carbon-react-code-mirror": "^0.2.0", "file-saver": "^2.0.5", "graphql": "15.5.2", - "icse-react-assets": "1.2.36", + "icse-react-assets": "1.2.37", "json-to-tf": "^0.3.1", "jszip": "^3.10.1", - "lazy-z": "^1.11.14", + "lazy-z": "^1.11.16", "react": "^18.0.0", "react-dom": "^18.0.0", "react-router-dom": "^6.3.0", diff --git a/client/src/Craig.js b/client/src/Craig.js index ec034aa9..771ee13d 100644 --- a/client/src/Craig.js +++ b/client/src/Craig.js @@ -248,7 +248,7 @@ class Craig extends React.Component { ); }, // project reject callback - () => { + (err) => { this.setState({ schematicsFailed: true, loadingDone: true }); console.error(err); } diff --git a/client/src/components/forms/DynamicForm.js b/client/src/components/forms/DynamicForm.js index 3916465d..b72c66f5 100644 --- a/client/src/components/forms/DynamicForm.js +++ b/client/src/components/forms/DynamicForm.js @@ -13,20 +13,22 @@ import { DynamicFormToggle, DynamicTextArea, DynamicMultiSelect, -} from "./dynamic-form/components"; + DynamicPublicKey, + SubFormOverrideTile, + PowerInterfaces, + PerCloudConnections, +} from "./dynamic-form"; import { eachKey, isBoolean, contains } from "lazy-z"; -import { propsMatchState } from "../../lib"; -import { - ClassicDisabledTile, - NoClassicGatewaysTile, - NoDomainsTile, -} from "./dynamic-form/tiles"; +import { disableSave, propsMatchState } from "../../lib"; import { dynamicIcseFormGroupsProps, dynamicIcseHeadingProps, dynamicToolTipWrapperProps, } from "../../lib/forms/dynamic-form-fields"; -import { Network_3 } from "@carbon/icons-react"; +import { edgeRouterEnabledZones } from "../../lib/constants"; +import { DynamicFetchSelect } from "./dynamic-form/components"; +import { Button } from "@carbon/react"; +import { Rocket } from "@carbon/icons-react"; const doNotRenderFields = [ "heading", @@ -55,7 +57,14 @@ class DynamicForm extends React.Component { !isBoolean(this.state[field]) && !contains(doNotRenderFields, field) ) - this.state[field] = group[field].default || ""; + // prevent ssh public key from causing propsMatchState to be false + // when use data is true. also prevent router_hostname from rendering as + // empty string when null + this.state[field] = isBoolean(group[field].default) + ? group[field].default + : group[field].default === null && field !== "router_hostname" + ? null + : group[field].default || ""; }); }); @@ -154,10 +163,13 @@ class DynamicForm extends React.Component { ) : group.heading ? ( ) : ( - + {Object.keys(group).map((key, keyIndex) => { let field = group[key]; - return (field.hideWhen && field.hideWhen(this.state)) || + return (field.hideWhen && + field.hideWhen(this.state, this.props)) || key === "hideWhen" ? ( "" ) : ( @@ -178,6 +190,10 @@ class DynamicForm extends React.Component { ? DynamicTextArea : field.type === "multiselect" ? DynamicMultiSelect + : field.type === "public-key" + ? DynamicPublicKey + : field.type === "fetchSelect" + ? DynamicFetchSelect : DynamicFormTextInput, { name: key, @@ -199,99 +215,94 @@ class DynamicForm extends React.Component { ) )} - { - // this is less than elegant, as we add more custom components we can - // figure out the best way to render custom components - this.props.formName === "Power Instances" && ( -
- {this.state.network.length === 0 - ? "No Network Interfaces Selected" - : this.state.network.map((nw, index) => { - return ( - - -
-

{nw.name}

-
- -
- ); - })} -
- ) - } + + {this.props.formName === "options" ? ( + + ) : ( + "" + )} {this.props.isModal === true || !this.props.form.subForms ? "" - : this.props.form.subForms.map((subForm) => ( - - ) : subForm.jsonField === "gre_tunnels" && - !this.props.craig.store.json._options.enable_classic ? ( - ClassicDisabledTile(true) - ) : subForm.jsonField === "gre_tunnels" && - this.props.craig.store.json.classic_gateways.length === - 0 ? ( - - ) : undefined - } - hideFormTitleButton={ - subForm.hideFormTitleButton - ? subForm.hideFormTitleButton(this.state, this.props) - : false - } - name={subForm.name} - subHeading - addText={subForm.createText} - arrayData={this.props.data[subForm.jsonField]} - innerForm={DynamicForm} - disableSave={this.props.disableSave} - onDelete={ - this.props.craig[this.props.form.jsonField][subForm.jsonField] - .delete - } - onSave={ - this.props.craig[this.props.form.jsonField][subForm.jsonField] - .save - } - onSubmit={ - this.props.craig[this.props.form.jsonField][subForm.jsonField] - .create - } - propsMatchState={propsMatchState} - innerFormProps={{ - formName: subForm.name, - craig: this.props.craig, - form: subForm.form, - disableSave: this.props.disableSave, - arrayParentName: this.props.data.name, - propsMatchState: propsMatchState, - }} - toggleFormFieldName={subForm.toggleFormFieldName} - hideAbout - toggleFormProps={{ - hideName: true, - submissionFieldName: subForm.jsonField, - disableSave: this.props.disableSave, - type: "formInSubForm", - }} - /> - ))} + : this.props.form.subForms.map((subForm) => + // prevent template from rendering when edge router + subForm.jsonField === "cloud_connections" && + contains(edgeRouterEnabledZones, this.state.zone) ? ( + + ) : subForm.hideWhen && + // hide when hidden + subForm.hideWhen(this.state, this.props) ? ( + "" + ) : ( + + } + hideFormTitleButton={ + subForm.hideFormTitleButton + ? subForm.hideFormTitleButton(this.state, this.props) + : false + } + name={subForm.name} + subHeading + addText={subForm.addText} + arrayData={this.props.data[subForm.jsonField]} + innerForm={DynamicForm} + disableSave={this.props.disableSave} + onDelete={ + this.props.craig[this.props.form.jsonField][ + subForm.jsonField + ].delete + } + onSave={ + this.props.craig[this.props.form.jsonField][ + subForm.jsonField + ].save + } + onSubmit={ + this.props.craig[this.props.form.jsonField][ + subForm.jsonField + ].create + } + propsMatchState={propsMatchState} + innerFormProps={{ + formName: subForm.name, + craig: this.props.craig, + form: subForm.form, + disableSave: this.props.disableSave, + arrayParentName: this.props.data.name, + propsMatchState: propsMatchState, + }} + toggleFormFieldName={subForm.toggleFormFieldName} + hideAbout + toggleFormProps={{ + hideName: true, + submissionFieldName: subForm.jsonField, + disableSave: this.props.disableSave, + type: "formInSubForm", + noDeleteButton: subForm.noDeleteButton, + // here for testing + // hide: false, + }} + /> + ) + )} ); } diff --git a/client/src/components/forms/ObservabilityForm.js b/client/src/components/forms/ObservabilityForm.js index 2110b4e0..b7af404d 100644 --- a/client/src/components/forms/ObservabilityForm.js +++ b/client/src/components/forms/ObservabilityForm.js @@ -43,6 +43,7 @@ const ObservabilityForm = (props) => { }} craig={props.craig} innerFormProps={{ + craig: props.craig, data: props.craig.store.json.logdna, resourceGroups: splat( props.craig.store.json.resource_groups, @@ -73,6 +74,7 @@ const ObservabilityForm = (props) => { }} craig={props.craig} innerFormProps={{ + craig: props.craig, data: props.craig.store.json.sysdig, resourceGroups: splat( props.craig.store.json.resource_groups, diff --git a/client/src/components/forms/dynamic-form/PowerInterfaces.js b/client/src/components/forms/dynamic-form/PowerInterfaces.js new file mode 100644 index 00000000..34346307 --- /dev/null +++ b/client/src/components/forms/dynamic-form/PowerInterfaces.js @@ -0,0 +1,45 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { IcseFormGroup } from "icse-react-assets"; +import { Network_3 } from "@carbon/icons-react"; +import { DynamicFormTextInput } from "./components"; +import { contains } from "lazy-z"; + +export const PowerInterfaces = (props) => { + return contains(["Power Instances", "VTL"], props.componentProps.formName) ? ( +
+ {props.stateData.network.map((nw, index) => { + return ( + + +
+

{nw.name}

+
+ +
+ ); + })} +
+ ) : ( + "" + ); +}; + +PowerInterfaces.propTypes = { + stateData: PropTypes.shape({}).isRequired, + componentProps: PropTypes.shape({ + craig: PropTypes.shape({}).isRequired, + }).isRequired, + handleInputChange: PropTypes.func.isRequired, +}; diff --git a/client/src/components/forms/dynamic-form/README.md b/client/src/components/forms/dynamic-form/README.md new file mode 100644 index 00000000..1006f904 --- /dev/null +++ b/client/src/components/forms/dynamic-form/README.md @@ -0,0 +1,3 @@ +# dynamic-form Components + +Components in this directory are used as part of the DynamicForm component \ No newline at end of file diff --git a/client/src/components/forms/dynamic-form/SubFormOverrideTile.js b/client/src/components/forms/dynamic-form/SubFormOverrideTile.js new file mode 100644 index 00000000..a1c60d27 --- /dev/null +++ b/client/src/components/forms/dynamic-form/SubFormOverrideTile.js @@ -0,0 +1,36 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { + ClassicDisabledTile, + CraigEmptyResourceTile, + NoClassicGatewaysTile, + NoDomainsTile, +} from "./tiles"; + +export const SubFormOverrideTile = (props) => { + let jsonField = props.subForm.jsonField; + return jsonField === "dns_records" && + props.componentProps.data.domains.length === 0 ? ( + + ) : jsonField === "gre_tunnels" && + !props.componentProps.craig.store.json._options.enable_classic ? ( + ClassicDisabledTile(true) + ) : jsonField === "gre_tunnels" && + props.componentProps.craig.store.json.classic_gateways.length === 0 ? ( + + ) : ( + // have to pass in tile here otherwise will not render + + ); +}; + +SubFormOverrideTile.propTypes = { + subForm: PropTypes.shape({ + jsonField: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }).isRequired, + componentProps: PropTypes.shape({ + craig: PropTypes.shape({}).isRequired, + data: PropTypes.shape({}).isRequired, + }).isRequired, +}; diff --git a/client/src/components/forms/dynamic-form/components.js b/client/src/components/forms/dynamic-form/components.js index 57c61151..d7355bdf 100644 --- a/client/src/components/forms/dynamic-form/components.js +++ b/client/src/components/forms/dynamic-form/components.js @@ -1,5 +1,5 @@ import React from "react"; -import { PopoverWrapper } from "icse-react-assets"; +import { IcseFormGroup, PopoverWrapper } from "icse-react-assets"; import { dynamicTextInputProps, dynamicSelectProps, @@ -15,8 +15,12 @@ import { Toggle, TextInput, Select, + Tag, } from "@carbon/react"; import PropTypes from "prop-types"; +import { dynamicPasswordInputProps } from "../../../lib/forms/dynamic-form-fields/password-input"; +import { deepEqual } from "lazy-z"; +const tagColors = ["red", "magenta", "purple", "blue", "cyan", "teal", "green"]; const DynamicFormTextInput = (props) => { return ; @@ -103,7 +107,24 @@ DynamicFormToggle.propTypes = { }; const DynamicTextArea = (props) => { - return