From c9de4e2c76ed931f10cd096a58274d4b130094ca Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 29 Oct 2019 18:45:18 +0700 Subject: [PATCH 1/8] [pf-form] FormFieldset component simplified * Also fixed imports * And added to flow index --- .flowconfig | 2 +- app/javascript/src/Form/FormFieldset.jsx | 38 +---- app/javascript/src/Form/index.jsx | 4 + spec/javascripts/Form/FormFieldset.spec.jsx | 35 +---- .../__snapshots__/FormFieldset.spec.jsx.snap | 141 +----------------- 5 files changed, 17 insertions(+), 203 deletions(-) create mode 100644 app/javascript/src/Form/index.jsx diff --git a/.flowconfig b/.flowconfig index 581524fbcf..a34c61a50e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -15,4 +15,4 @@ module.system.node.resolve_dirname=node_modules module.name_mapper.extension='scss' -> 'empty/object' module.system=haste -module.name_mapper='\(Applications\|Dashboard\|LoginPage\|Navigation\|Onboarding\|Policies\|services\|Stats\|Types\|Users\|utilities\|NewService\)\(.*\)$' -> '/app/javascript/src/\1/\2' +module.name_mapper='\(Applications\|Form\|Settings\|Dashboard\|LoginPage\|Navigation\|Onboarding\|Policies\|services\|Stats\|Types\|Users\|utilities\|NewService\)\(.*\)$' -> '/app/javascript/src/\1/\2' diff --git a/app/javascript/src/Form/FormFieldset.jsx b/app/javascript/src/Form/FormFieldset.jsx index 7336129bfc..63b936ab99 100644 --- a/app/javascript/src/Form/FormFieldset.jsx +++ b/app/javascript/src/Form/FormFieldset.jsx @@ -2,34 +2,23 @@ // TODO: Replace this component when patternfly-react implements it. import * as React from 'react' +// $FlowFixMe Flow has troubles with @patternfly modules import { FormContext } from '@patternfly/react-core/dist/js/components/Form/FormContext' -// $FlowFixMe +// $FlowFixMe Flow has troubles with @patternfly modules import styles from '@patternfly/react-styles/css/components/Form/form' -// $FlowFixMe +// $FlowFixMe Flow has troubles with @patternfly modules import { css, getModifier } from '@patternfly/react-styles' type Props = { children?: React.Node, className?: string, - label?: React.Node, - isRequired?: boolean, - isValid?: boolean, - isInline?: boolean, - helperText?: React.Node, - helperTextInvalid?: React.Node, - fieldId: string + isInline?: boolean } const FormFieldset = ({ children, className = '', - label, - isRequired = false, - isValid = true, isInline = false, - helperText, - helperTextInvalid, - fieldId, ...props }: Props) => ( @@ -38,26 +27,7 @@ const FormFieldset = ({ {...props} className={css(styles.formFieldset, isInline ? getModifier(styles, 'inline', className) : className)} > - {label && ( - - )} {isHorizontal ?
{children}
: children} - {((isValid && helperText) || (!isValid && helperTextInvalid)) && ( -
- {isValid ? helperText : helperTextInvalid} -
- )} )}
diff --git a/app/javascript/src/Form/index.jsx b/app/javascript/src/Form/index.jsx new file mode 100644 index 0000000000..3d75444b79 --- /dev/null +++ b/app/javascript/src/Form/index.jsx @@ -0,0 +1,4 @@ +// @flow + +export * from 'Form/FormFieldset' +export * from 'Form/FormLegend' diff --git a/spec/javascripts/Form/FormFieldset.spec.jsx b/spec/javascripts/Form/FormFieldset.spec.jsx index 81056359d5..f6f97694ce 100644 --- a/spec/javascripts/Form/FormFieldset.spec.jsx +++ b/spec/javascripts/Form/FormFieldset.spec.jsx @@ -5,7 +5,7 @@ import { FormFieldset } from 'Form/FormFieldset' describe('FormFieldset', () => { it('should render default form fieldset variant', () => { const view = mount( - + ) @@ -14,43 +14,16 @@ describe('FormFieldset', () => { it('should render inline form fieldset variant', () => { const view = mount( - + ) expect(view).toMatchSnapshot() }) - it('should render form fieldset variant with node label and help text', () => { + it('should render form fieldset with custom class names', () => { const view = mount( - Label} helperText="this is helper text" > - - - ) - expect(view).toMatchSnapshot() - }) - - it('should render form fieldset variant with node helperText', () => { - const view = mount( - Help text!}> - - - ) - expect(view).toMatchSnapshot() - }) - - it('should render form fieldset required variant', () => { - const view = mount( - - - - ) - expect(view).toMatchSnapshot() - }) - - it('should render form fieldset invalid variant', () => { - const view = mount( - + ) diff --git a/spec/javascripts/Form/__snapshots__/FormFieldset.spec.jsx.snap b/spec/javascripts/Form/__snapshots__/FormFieldset.spec.jsx.snap index 7a91907ff1..ab7da4ae56 100644 --- a/spec/javascripts/Form/__snapshots__/FormFieldset.spec.jsx.snap +++ b/spec/javascripts/Form/__snapshots__/FormFieldset.spec.jsx.snap @@ -1,78 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FormFieldset should render default form fieldset variant 1`] = ` - -
- -
-
-`; - -exports[`FormFieldset should render form fieldset invalid variant 1`] = ` - -
- - -
- Invalid FormFieldset -
-
-
-`; - -exports[`FormFieldset should render form fieldset required variant 1`] = ` - +
- @@ -80,87 +12,22 @@ exports[`FormFieldset should render form fieldset required variant 1`] = ` `; -exports[`FormFieldset should render form fieldset variant with node helperText 1`] = ` +exports[`FormFieldset should render form fieldset with custom class names 1`] = ` - Help text! - - } - label="Label" + className="extra-class another-class" >
- -
- - Help text! - -
-
-
-`; - -exports[`FormFieldset should render form fieldset variant with node label and help text 1`] = ` - - Label - - } -> -
- - -
- this is helper text -
`; exports[`FormFieldset should render inline form fieldset variant 1`] = `
Date: Tue, 29 Oct 2019 18:46:48 +0700 Subject: [PATCH 2/8] [settings] FormSettings types --- app/javascript/src/Settings/types/index.jsx | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 app/javascript/src/Settings/types/index.jsx diff --git a/app/javascript/src/Settings/types/index.jsx b/app/javascript/src/Settings/types/index.jsx new file mode 100644 index 0000000000..7f64c02c0d --- /dev/null +++ b/app/javascript/src/Settings/types/index.jsx @@ -0,0 +1,50 @@ +// @flow + +import * as React from 'react' + +export type FieldGroupProps = { + name: string, + value: string, + label: string, + children?: React.Node, + legend?: string, + checked?: string, + hint?: string, + placeholder?: string, + defaultValue?: string, + readOnly?: boolean, + inputType?: string, + isDefaultValue?: boolean, + onChange?: (value: string, event: SyntheticEvent) => void +} + +export type FieldCatalogProps = { catalog: { [string]: string } } + +export type TypeItemProps = { + type: FieldGroupProps & FieldCatalogProps, + item: FieldGroupProps +} + +export type LegendCollectionProps = { + legend: string, + collection: FieldGroupProps[] +} + +export type SettingsProps = { + isProxyCustomUrlActive: boolean, + integrationMethod: FieldGroupProps & FieldCatalogProps, + authenticationMethod: FieldGroupProps & FieldCatalogProps, + proxyEndpoints: FieldGroupProps[], + authenticationSettings: { + appIdKeyPairSettings: FieldGroupProps[], + apiKeySettings: FieldGroupProps, + oidcSettings: { + basicSettings: TypeItemProps, + flowSettings: FieldGroupProps[], + jwtSettings: TypeItemProps + } + }, + credentialsLocation: FieldGroupProps & FieldCatalogProps, + security: FieldGroupProps[], + gatewayResponse: LegendCollectionProps[] +} From 83b346985af8ae56cafc6d562ced7186803808e5 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 29 Oct 2019 18:47:09 +0700 Subject: [PATCH 3/8] [settings] FormSettings defaults * Used as a template for testing * Also used for default values while developing --- app/javascript/src/Settings/defaults.jsx | 263 +++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 app/javascript/src/Settings/defaults.jsx diff --git a/app/javascript/src/Settings/defaults.jsx b/app/javascript/src/Settings/defaults.jsx new file mode 100644 index 0000000000..b63962cbc4 --- /dev/null +++ b/app/javascript/src/Settings/defaults.jsx @@ -0,0 +1,263 @@ +const INTEGRATION_METHOD_DEFAULTS = { + value: 'hosted', + name: 'deployment_option', + catalog: { + hosted: 'APIcast 3scale managed', + self_managed: 'APIcast self-managed', + service_mesh_istio: 'Istio' + } +} +const AUTHENTICATION_METHOD_DEFAULTS = { + value: '1', + name: 'proxy_authentication_method', + catalog: { + '1': 'API Key (user_key)', + '2': 'App_ID and App_Key Pair', + oidc: 'OpenID Connect' + } +} + +const PROXY_ENDPOINTS_DEFAULTS = [ + { + defaultValue: 'https://api-2.staging.apicast.com', + placeholder: 'https://api.provider-name.com', + label: 'Staging Public Base URL', + name: 'sandbox_endpoint', + hint: 'Public address of your API gateway in the staging environment.', + value: 'https://custom.api.staging.provider-name.com', + inputType: 'url' + }, + { + defaultValue: 'https://api-2.apicast.com', + placeholder: 'https://api.provider-name.com', + label: 'Staging Public Base URL', + name: 'endpoint', + hint: 'Public address of your API gateway in the production environment.', + value: 'https://custom.api.provider-name.com', + inputType: 'url' + } +] + +const API_KEY_SETTINGS_DEFAULT = { + label: 'Auth user key', + name: 'auth_user_key', + value: 'user_key' +} + +const APP_ID_KEY_PAIR_SETTINGS_DEFAULT = [ + { + label: 'App ID parameter', + name: 'auth_app_id', + hint: 'Public address of your API gateway in the staging environment.', + value: 'app_id' + }, + { + label: 'App Key parameter', + name: 'auth_app_key', + value: 'app_key' + } +] + +const OIDC_BASICS_SETTINGS_DEFAULTS = { + type: { + value: 'keycloak', + name: 'oidc_issuer_type', + label: 'OpenID Connect Issuer Type', + catalog: { + keycloak: 'Red Hat Single Sign-On', + rest: 'REST API' + } + }, + item: { + value: '', + name: 'oidc_issuer_endpoint', + label: 'OpenID Connect Issuer', + placeholder: 'https://sso.example.com/auth/realms/gateway', + hint: 'Location of your OpenID Provider. The format of this endpoint is determined on your OpenID Provider setup. A common guidance would be "https://CLIENT_ID:CLIENT_SECRET@HOST:PORT/auth/realms/REALM_NAME".' + } +} + +const OIDC_FLOW_SETTINGS_DEFAULTS = [ + { name: 'service_accounts_enabled', label: 'Service Accounts Flow', checked: false }, + { name: 'standard_flow_enabled', label: 'Authorization Code Flow', checked: false }, + { name: 'implicit_flow_enabled', label: 'Implicit Flow', checked: false }, + { name: 'direct_access_grants_enabled', label: 'Direct Access Grant Flow', checked: false } +] + +const OIDC_JWT_SETTINGS_DEFAULTS = { + type: { + value: 'plain', + name: 'jwt_claim_with_client_id_type', + label: 'ClientID Token Claim Type', + hint: 'Process the ClientID Token Claim value as a string or as a liquid template. When set to "Liquid" you can define more complex rules. e.g. If "some_claim" is an array you can select the first value this like {{ some_claim | first }}.', + catalog: { + plain: 'plain', + liquid: 'liquid' + } + }, + item: { + value: 'azp', + name: 'jwt_claim_with_client_id', + label: 'ClientID Token Claim', + placeholder: 'azp', + hint: 'The Token Claim that contains the clientID. Defaults to "azp".' + } +} + +const OIDC_SETTINGS_DEFAULTS = { + basicSettings: OIDC_BASICS_SETTINGS_DEFAULTS, + flowSettings: OIDC_FLOW_SETTINGS_DEFAULTS, + jwtSettings: OIDC_JWT_SETTINGS_DEFAULTS +} + +const CREDENTIALS_LOCATION_DEFAULTS = { + value: 'headers', + name: 'credentials_location', + catalog: { + headers: 'As HTTP Headers', + query: 'As query parameters (GET) or body parameters (POST/PUT/DELETE)', + authorization: 'As HTTP Basic Authentication' + } +} + +const SECURITY_DEFAULTS = [ + { + defaultValue: '', + placeholder: 'https://api.provider-name.com', + label: 'Host Header', + name: 'hostname_rewrite', + hint: 'Lets you define a custom Host request header. This is needed if your API backend only accepts traffic from a specific host.', + value: '', + readOnly: false + }, + { + defaultValue: '', + placeholder: 'https://api.provider-name.com', + label: 'Secret Token', + name: 'secret_token', + hint: 'Enables you to block any direct developer requests to your API backend; each 3scale API gateway call to your API backend contains a request header called X-3scale-proxy-secret-token. The value of this header can be set by you here. It\'s up to you ensure your backend only allows calls with this secret header.', + value: '', + readOnly: false + } +] + +const GATEWAY_RESPONSE_DEFAULT = [ + { + legend: 'Authentication Failed Error', + collection: [ + { + label: 'Response Code', + name: 'error_status_auth_failed', + value: '403', + inputType: 'number' + }, + { + label: 'Content-type', + name: 'error_headers_auth_failed', + value: 'text/plain; charset=us-ascii', + inputType: 'text' + }, + { + label: 'Response Body', + name: 'error_auth_failed', + value: 'Authentication failed', + inputType: 'text' + } + ] + }, + { + legend: 'Authentication Missing Error', + collection: [ + { + label: 'Response Code', + name: 'error_status_auth_missing', + value: '403', + inputType: 'number' + }, + { + label: 'Content-type', + name: 'error_headers_auth_missing', + value: 'text/plain; charset=us-ascii', + inputType: 'text' + }, + { + label: 'Response Body', + name: 'error_auth_missing', + value: 'Authentication parameters missing', + inputType: 'text' + } + ] + }, + { + legend: 'Match Error', + collection: [ + { + label: 'Response Code', + name: 'error_status_no_match', + value: '404', + inputType: 'number' + }, + { + label: 'Content-type', + name: 'error_headers_no_match', + value: 'text/plain; charset=us-ascii', + inputType: 'text' + }, + { + label: 'Response Body', + name: 'error_no_match', + value: 'No Mapping Rule matched', + inputType: 'text' + } + ] + }, + { + legend: 'Usage limit exceeded error', + collection: [ + { + label: 'Response Code', + name: 'error_status_limits_exceeded', + value: '429', + inputType: 'number' + }, + { + label: 'Content-type', + name: 'error_headers_limits_exceeded', + value: 'text/plain; charset=us-ascii', + inputType: 'text' + }, + { + label: 'Response Body', + name: 'error_limits_exceeded', + value: 'Usage limit exceeded', + inputType: 'text' + } + ] + } +] + +const AUTHENTICATION_SETTINGS_DEFAULT = { + oidcSettings: OIDC_SETTINGS_DEFAULTS, + appIdKeyPairSettings: APP_ID_KEY_PAIR_SETTINGS_DEFAULT, + apiKeySettings: API_KEY_SETTINGS_DEFAULT +} + +const SETTINGS_DEFAULT = { + isProxyCustomUrlActive: false, + integrationMethod: INTEGRATION_METHOD_DEFAULTS, + authenticationMethod: AUTHENTICATION_METHOD_DEFAULTS, + proxyEndpoints: PROXY_ENDPOINTS_DEFAULTS, + authenticationSettings: AUTHENTICATION_SETTINGS_DEFAULT, + credentialsLocation: CREDENTIALS_LOCATION_DEFAULTS, + security: SECURITY_DEFAULTS, + gatewayResponse: GATEWAY_RESPONSE_DEFAULT +} + +export { + INTEGRATION_METHOD_DEFAULTS, + PROXY_ENDPOINTS_DEFAULTS, + AUTHENTICATION_METHOD_DEFAULTS, + AUTHENTICATION_SETTINGS_DEFAULT, + OIDC_SETTINGS_DEFAULTS, + SETTINGS_DEFAULT +} From 0a7534cea58fa732216db1ab7eb418c3ca02f5a9 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 29 Oct 2019 18:53:42 +0700 Subject: [PATCH 4/8] [settings] Common components * Crafted in order to ease the composition of Settings Form * Could be re-used for any other form using patterfly-react * When the time comes and it's used in other modules, should be moved to a different directory --- .../components/Common/FormCollection.jsx | 24 +++ .../components/Common/RadioFieldset.jsx | 50 +++++ .../components/Common/SelectGroup.jsx | 44 ++++ .../components/Common/TextInputGroup.jsx | 38 ++++ .../components/Common/TypeItemCombo.jsx | 26 +++ .../src/Settings/components/Common/index.jsx | 7 + .../components/Common/FormCollection.spec.jsx | 23 +++ .../components/Common/RadioFieldset.spec.jsx | 20 ++ .../components/Common/SelectGroup.spec.jsx | 38 ++++ .../components/Common/TextInputGroup.spec.jsx | 27 +++ .../components/Common/TypeItemCombo.spec.jsx | 29 +++ .../FormCollection.spec.jsx.snap | 31 +++ .../__snapshots__/RadioFieldset.spec.jsx.snap | 38 ++++ .../__snapshots__/SelectGroup.spec.jsx.snap | 192 ++++++++++++++++++ .../TextInputGroup.spec.jsx.snap | 23 +++ .../__snapshots__/TypeItemCombo.spec.jsx.snap | 30 +++ 16 files changed, 640 insertions(+) create mode 100644 app/javascript/src/Settings/components/Common/FormCollection.jsx create mode 100644 app/javascript/src/Settings/components/Common/RadioFieldset.jsx create mode 100644 app/javascript/src/Settings/components/Common/SelectGroup.jsx create mode 100644 app/javascript/src/Settings/components/Common/TextInputGroup.jsx create mode 100644 app/javascript/src/Settings/components/Common/TypeItemCombo.jsx create mode 100644 app/javascript/src/Settings/components/Common/index.jsx create mode 100644 spec/javascripts/Settings/components/Common/FormCollection.spec.jsx create mode 100644 spec/javascripts/Settings/components/Common/RadioFieldset.spec.jsx create mode 100644 spec/javascripts/Settings/components/Common/SelectGroup.spec.jsx create mode 100644 spec/javascripts/Settings/components/Common/TextInputGroup.spec.jsx create mode 100644 spec/javascripts/Settings/components/Common/TypeItemCombo.spec.jsx create mode 100644 spec/javascripts/Settings/components/Common/__snapshots__/FormCollection.spec.jsx.snap create mode 100644 spec/javascripts/Settings/components/Common/__snapshots__/RadioFieldset.spec.jsx.snap create mode 100644 spec/javascripts/Settings/components/Common/__snapshots__/SelectGroup.spec.jsx.snap create mode 100644 spec/javascripts/Settings/components/Common/__snapshots__/TextInputGroup.spec.jsx.snap create mode 100644 spec/javascripts/Settings/components/Common/__snapshots__/TypeItemCombo.spec.jsx.snap diff --git a/app/javascript/src/Settings/components/Common/FormCollection.jsx b/app/javascript/src/Settings/components/Common/FormCollection.jsx new file mode 100644 index 0000000000..e6c988deb7 --- /dev/null +++ b/app/javascript/src/Settings/components/Common/FormCollection.jsx @@ -0,0 +1,24 @@ +// @flow + +import * as React from 'react' +import { FormFieldset, FormLegend } from 'Form' +import type { FieldGroupProps } from 'Settings/types' + +type Props = { + collection: FieldGroupProps[], + ItemComponent: (FieldGroupProps) => React.Element, + legend: string +} + +const FormCollection = ({collection, ItemComponent, legend}: Props) => { + return ( + + {legend} + { collection.map(itemProps => ) } + + ) +} + +export { + FormCollection +} diff --git a/app/javascript/src/Settings/components/Common/RadioFieldset.jsx b/app/javascript/src/Settings/components/Common/RadioFieldset.jsx new file mode 100644 index 0000000000..b627e3919b --- /dev/null +++ b/app/javascript/src/Settings/components/Common/RadioFieldset.jsx @@ -0,0 +1,50 @@ +// @flow + +import * as React from 'react' +import { useState } from 'react' +import { FormFieldset, FormLegend } from 'Form' +import { Radio } from '@patternfly/react-core' +import type { FieldGroupProps, FieldCatalogProps } from 'Settings/types' + +type CheckEvent = SyntheticEvent + +type Props = FieldGroupProps & FieldCatalogProps + +const useSelectedOnChange = (value, onChange) => ( + typeof onChange === 'function' + ? [value, onChange] + : useState(value).map(x => typeof x === 'function' ? (_c, e: CheckEvent) => x(e.currentTarget.value) : x) +) + +const RadioFieldset = ({ + children, + legend, + name, + value, + catalog, + onChange, + ...props +}: Props) => { + const [selectedOnChange, setSelectedOnChange] = useSelectedOnChange(value, onChange) + return ( + + {legend} + {Object.keys(catalog).map(key => ( + + ))} + {children} + + ) +} + +export { + RadioFieldset +} diff --git a/app/javascript/src/Settings/components/Common/SelectGroup.jsx b/app/javascript/src/Settings/components/Common/SelectGroup.jsx new file mode 100644 index 0000000000..6ad1e3a851 --- /dev/null +++ b/app/javascript/src/Settings/components/Common/SelectGroup.jsx @@ -0,0 +1,44 @@ +// @flow + +import * as React from 'react' +import { useState } from 'react' +import { FormGroup, Select, SelectOption } from '@patternfly/react-core' +import type { FieldGroupProps, FieldCatalogProps } from 'Settings/types' + +type Props = FieldGroupProps & FieldCatalogProps + +const SelectGroup = ({ + label, + name, + hint, + value, + catalog +}: Props) => { + const [ selectedValue, setSelectedValue ] = useState(value) + const [ isExpanded, setIsExpanded ] = useState(false) + const onSelect = (_e, selection) => { + setIsExpanded(false) + setSelectedValue(selection.key) + } + + return ( + + + + + ) +} + +export { + SelectGroup +} diff --git a/app/javascript/src/Settings/components/Common/TextInputGroup.jsx b/app/javascript/src/Settings/components/Common/TextInputGroup.jsx new file mode 100644 index 0000000000..b056da0d27 --- /dev/null +++ b/app/javascript/src/Settings/components/Common/TextInputGroup.jsx @@ -0,0 +1,38 @@ +// @flow + +import * as React from 'react' +import { useState } from 'react' +import { FormGroup, TextInput } from '@patternfly/react-core' +import type { FieldGroupProps } from 'Settings/types' + +const TextInputGroup = ({ + defaultValue, + placeholder, + label, + name, + hint, + value, + isDefaultValue = false, + readOnly = false, + inputType = 'text' +}: FieldGroupProps) => { + const [ inputValue, setInputValue ] = useState(value) + const onChange = (value, _e) => setInputValue(value) + return ( + + + + ) +} + +export { + TextInputGroup +} diff --git a/app/javascript/src/Settings/components/Common/TypeItemCombo.jsx b/app/javascript/src/Settings/components/Common/TypeItemCombo.jsx new file mode 100644 index 0000000000..cd6ba371ab --- /dev/null +++ b/app/javascript/src/Settings/components/Common/TypeItemCombo.jsx @@ -0,0 +1,26 @@ +// @flow + +import * as React from 'react' +import { FormFieldset, FormLegend } from 'Form' +import { TextInputGroup, SelectGroup } from 'Settings/components/Common' +import type { FieldGroupProps, FieldCatalogProps } from 'Settings/types' + +type Props = { + type: FieldCatalogProps & FieldGroupProps, + item: FieldGroupProps, + legend: string +} + +const TypeItemCombo = ({ type, item, legend }: Props) => { + return ( + + {legend} + + + + ) +} + +export { + TypeItemCombo +} diff --git a/app/javascript/src/Settings/components/Common/index.jsx b/app/javascript/src/Settings/components/Common/index.jsx new file mode 100644 index 0000000000..487d9fa11e --- /dev/null +++ b/app/javascript/src/Settings/components/Common/index.jsx @@ -0,0 +1,7 @@ +// @flow + +export * from 'Settings/components/Common/FormCollection' +export * from 'Settings/components/Common/SelectGroup' +export * from 'Settings/components/Common/TextInputGroup' +export * from 'Settings/components/Common/RadioFieldset' +export * from 'Settings/components/Common/TypeItemCombo' diff --git a/spec/javascripts/Settings/components/Common/FormCollection.spec.jsx b/spec/javascripts/Settings/components/Common/FormCollection.spec.jsx new file mode 100644 index 0000000000..5bc5c92da0 --- /dev/null +++ b/spec/javascripts/Settings/components/Common/FormCollection.spec.jsx @@ -0,0 +1,23 @@ +import React from 'react' +import { shallow } from 'enzyme' + +import { FormCollection } from 'Settings/components/Common' + +it('should render correctly', () => { + const GoodBand = ({name}) => {name} rocks! + + const props = { + legend: 'Some Good Bands', + ItemComponent: GoodBand, + collection: [ + { name: 'The Rolling Stones' }, + { name: 'The Brian Jonestown Massacre' }, + { name: 'Radiohead' }, + { name: 'Blur' }, + { name: 'The Clash' } + ] + } + + const view = shallow() + expect(view).toMatchSnapshot() +}) diff --git a/spec/javascripts/Settings/components/Common/RadioFieldset.spec.jsx b/spec/javascripts/Settings/components/Common/RadioFieldset.spec.jsx new file mode 100644 index 0000000000..2481d2df14 --- /dev/null +++ b/spec/javascripts/Settings/components/Common/RadioFieldset.spec.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import { shallow } from 'enzyme' + +import { RadioFieldset } from 'Settings/components/Common' + +it('should render correctly', () => { + const props = { + legend: 'Foo Fighters', + value: 'pat', + name: 'foo_fighters', + catalog: { + dave: 'Grohl', + pat: 'Smear' + }, + onChange: jest.fn() + } + + const view = shallow(My Hero) + expect(view).toMatchSnapshot() +}) diff --git a/spec/javascripts/Settings/components/Common/SelectGroup.spec.jsx b/spec/javascripts/Settings/components/Common/SelectGroup.spec.jsx new file mode 100644 index 0000000000..9af64c5849 --- /dev/null +++ b/spec/javascripts/Settings/components/Common/SelectGroup.spec.jsx @@ -0,0 +1,38 @@ +import React from 'react' +import { mount } from 'enzyme' +import { act } from 'react-dom/test-utils' + +import { SelectGroup } from 'Settings/components/Common' + +const setup = () => { + const props = { + label: 'Jukebox', + name: 'jukebox', + hint: 'Pick your favourite', + value: 'nick_cave', + catalog: { + the_libertines: 'The Libertines', + tame_impala: 'Tame Impala', + massive_attack: 'Massive Attack', + nick_cave: 'Nick Cave' + } + } + const tree = mount() + return { props, tree } +} + +it('should render correctly', () => { + const { tree } = setup() + expect(tree).toMatchSnapshot() +}) + +it('should change the hidden input value as expected', () => { + const { tree } = setup() + const selectProps = tree.find('Select').props() + expect(tree.find('input[type="hidden"]').prop('value')).toBe('nick_cave') + + act(() => selectProps.onSelect({}, { key: 'massive_attack', toString: jest.fn() })) + tree.update() + + expect(tree.find('input[type="hidden"]').prop('value')).toBe('massive_attack') +}) diff --git a/spec/javascripts/Settings/components/Common/TextInputGroup.spec.jsx b/spec/javascripts/Settings/components/Common/TextInputGroup.spec.jsx new file mode 100644 index 0000000000..f6f06a0f33 --- /dev/null +++ b/spec/javascripts/Settings/components/Common/TextInputGroup.spec.jsx @@ -0,0 +1,27 @@ +import React from 'react' +import { shallow } from 'enzyme' + +import { TextInputGroup } from 'Settings/components/Common' + +const setup = (custom = {}) => { + const props = { + placeholder: 'You Favourite Dooz Kawa', + name: 'dooz_kawa_chanson', + hint: 'Enter your favourite Dooz Kawa tune', + value: 'Me Faire La Belle', + defaultValue: 'Le Monstre', + ...custom + } + const view = shallow() + return { props, view } +} + +it('should render correctly', () => { + const { view } = setup() + expect(view).toMatchSnapshot() +}) + +it('should default value when indicated', () => { + const { view } = setup({isDefaultValue: true}) + expect(view.find('TextInput').prop('value')).toBe('Le Monstre') +}) diff --git a/spec/javascripts/Settings/components/Common/TypeItemCombo.spec.jsx b/spec/javascripts/Settings/components/Common/TypeItemCombo.spec.jsx new file mode 100644 index 0000000000..e9c9179b17 --- /dev/null +++ b/spec/javascripts/Settings/components/Common/TypeItemCombo.spec.jsx @@ -0,0 +1,29 @@ +import React from 'react' +import { shallow } from 'enzyme' + +import { TypeItemCombo } from 'Settings/components/Common' + +it('should render correctly', () => { + const props = { + type: { + value: 'run_the_jewels_3', + name: 'run_the_jewels_album', + label: 'Your RTJ favourite album', + catalog: { + run_the_jewels_1: 'Run The Jewels I', + run_the_jewels_2: 'Run The Jewels II', + run_the_jewels_3: 'Run The Jewels III' + } + }, + item: { + value: 'Thursday in the Danger Room', + name: 'song_name', + label: 'Your favourite song from the album above ^', + placeholder: 'Oh Mama', + hint: 'Enter your favourite song from the album selected' + }, + legend: 'Legend has it!' + } + const view = shallow() + expect(view).toMatchSnapshot() +}) diff --git a/spec/javascripts/Settings/components/Common/__snapshots__/FormCollection.spec.jsx.snap b/spec/javascripts/Settings/components/Common/__snapshots__/FormCollection.spec.jsx.snap new file mode 100644 index 0000000000..cee6af8cf6 --- /dev/null +++ b/spec/javascripts/Settings/components/Common/__snapshots__/FormCollection.spec.jsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + + Some Good Bands + + + + + + + +`; diff --git a/spec/javascripts/Settings/components/Common/__snapshots__/RadioFieldset.spec.jsx.snap b/spec/javascripts/Settings/components/Common/__snapshots__/RadioFieldset.spec.jsx.snap new file mode 100644 index 0000000000..4cac4130c5 --- /dev/null +++ b/spec/javascripts/Settings/components/Common/__snapshots__/RadioFieldset.spec.jsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + + Foo Fighters + + + + + My Hero + + +`; diff --git a/spec/javascripts/Settings/components/Common/__snapshots__/SelectGroup.spec.jsx.snap b/spec/javascripts/Settings/components/Common/__snapshots__/SelectGroup.spec.jsx.snap new file mode 100644 index 0000000000..d70235eed7 --- /dev/null +++ b/spec/javascripts/Settings/components/Common/__snapshots__/SelectGroup.spec.jsx.snap @@ -0,0 +1,192 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + +
+ + + +
+ Pick your favourite +
+
+
+
+`; diff --git a/spec/javascripts/Settings/components/Common/__snapshots__/TextInputGroup.spec.jsx.snap b/spec/javascripts/Settings/components/Common/__snapshots__/TextInputGroup.spec.jsx.snap new file mode 100644 index 0000000000..95a75bc08d --- /dev/null +++ b/spec/javascripts/Settings/components/Common/__snapshots__/TextInputGroup.spec.jsx.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + + +`; diff --git a/spec/javascripts/Settings/components/Common/__snapshots__/TypeItemCombo.spec.jsx.snap b/spec/javascripts/Settings/components/Common/__snapshots__/TypeItemCombo.spec.jsx.snap new file mode 100644 index 0000000000..8c835bd49d --- /dev/null +++ b/spec/javascripts/Settings/components/Common/__snapshots__/TypeItemCombo.spec.jsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + + Legend has it! + + + + +`; From 6cf06eb6de7ccd399826ddfeba9f3ddeb7436e71 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 29 Oct 2019 18:55:14 +0700 Subject: [PATCH 5/8] [settings] OIDC Component --- .../src/Settings/components/OidcFieldset.jsx | 53 ++++++++ .../Settings/components/OidcFieldset.spec.jsx | 27 +++++ .../__snapshots__/OidcFieldset.spec.jsx.snap | 114 ++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 app/javascript/src/Settings/components/OidcFieldset.jsx create mode 100644 spec/javascripts/Settings/components/OidcFieldset.spec.jsx create mode 100644 spec/javascripts/Settings/components/__snapshots__/OidcFieldset.spec.jsx.snap diff --git a/app/javascript/src/Settings/components/OidcFieldset.jsx b/app/javascript/src/Settings/components/OidcFieldset.jsx new file mode 100644 index 0000000000..ee61ed8628 --- /dev/null +++ b/app/javascript/src/Settings/components/OidcFieldset.jsx @@ -0,0 +1,53 @@ +// @flow + +import React, {useState} from 'react' +import { FormFieldset, FormLegend } from 'Form' +import { Checkbox } from '@patternfly/react-core' +import { FormCollection, TypeItemCombo } from 'Settings/components/Common' +import type { TypeItemProps, FieldGroupProps } from 'Settings/types' + +const Basics = (props: TypeItemProps) => ( + +) + +const JsonWebToken = (props: TypeItemProps) => ( + +) + +const FlowItem = ({name, label, checked}: FieldGroupProps) => { + const [ isChecked, setIsChecked ] = useState(checked) + const onChange = (check, _e) => setIsChecked(check) + return ( + + ) +} + +const AuthorizationFlow = (props: {collection: FieldGroupProps[]}) => ( + +) + +type Props = { + isServiceMesh: boolean, + basicSettings: TypeItemProps, + jwtSettings: TypeItemProps, + flowSettings: FieldGroupProps[] +} + +const OidcFieldset = ({isServiceMesh, basicSettings, jwtSettings, flowSettings}: Props) => ( + + OPENID CONNECT (OIDC) + + { !isServiceMesh && } + { !isServiceMesh && } + +) + +export { + OidcFieldset +} diff --git a/spec/javascripts/Settings/components/OidcFieldset.spec.jsx b/spec/javascripts/Settings/components/OidcFieldset.spec.jsx new file mode 100644 index 0000000000..8ed67c0eb1 --- /dev/null +++ b/spec/javascripts/Settings/components/OidcFieldset.spec.jsx @@ -0,0 +1,27 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { OidcFieldset } from 'Settings/components/OidcFieldset' +import { OIDC_SETTINGS_DEFAULTS } from 'Settings/defaults' + +function setup (customProps = {}) { + const props = { + ...OIDC_SETTINGS_DEFAULTS, + isServiceMesh: false, + ...customProps + } + + const view = shallow() + + return { view, props } +} + +it('should render correctly', () => { + const { view } = setup() + expect(view).toMatchSnapshot() +}) + +it('should render only Basics when Service Mesh is active', () => { + const customProps = { isServiceMesh: true } + const { view } = setup(customProps) + expect(view).toMatchSnapshot() +}) diff --git a/spec/javascripts/Settings/components/__snapshots__/OidcFieldset.spec.jsx.snap b/spec/javascripts/Settings/components/__snapshots__/OidcFieldset.spec.jsx.snap new file mode 100644 index 0000000000..f53756596a --- /dev/null +++ b/spec/javascripts/Settings/components/__snapshots__/OidcFieldset.spec.jsx.snap @@ -0,0 +1,114 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + + OPENID CONNECT (OIDC) + + + + + +`; + +exports[`should render only Basics when Service Mesh is active 1`] = ` + + + OPENID CONNECT (OIDC) + + + +`; From 762d7963866ee5554560e7a71df0de41df9a36a7 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 30 Oct 2019 15:34:26 +0700 Subject: [PATCH 6/8] [settings] Authentication Settings component --- .../AuthenticationSettingsFieldset.jsx | 47 +++++++++++++++++++ .../AuthenticationSettingsFieldset.spec.jsx | 36 ++++++++++++++ ...thenticationSettingsFieldset.spec.jsx.snap | 35 ++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 app/javascript/src/Settings/components/AuthenticationSettingsFieldset.jsx create mode 100644 spec/javascripts/Settings/components/AuthenticationSettingsFieldset.spec.jsx create mode 100644 spec/javascripts/Settings/components/__snapshots__/AuthenticationSettingsFieldset.spec.jsx.snap diff --git a/app/javascript/src/Settings/components/AuthenticationSettingsFieldset.jsx b/app/javascript/src/Settings/components/AuthenticationSettingsFieldset.jsx new file mode 100644 index 0000000000..14200a8d4c --- /dev/null +++ b/app/javascript/src/Settings/components/AuthenticationSettingsFieldset.jsx @@ -0,0 +1,47 @@ +// @flow + +import * as React from 'react' +import { FormFieldset, FormLegend } from 'Form' +import { FormCollection, TextInputGroup } from 'Settings/components/Common' +import { OidcFieldset } from 'Settings/components/OidcFieldset' +import type { FieldGroupProps, TypeItemProps } from 'Settings/types' + +const OIDC_AUTH_METHOD = 'oidc' +const API_KEY_METHOD = '1' +const APP_ID_KEY_METHOD = '2' + +type Props = { + isServiceMesh: boolean, + authenticationMethod: string, + apiKeySettings: FieldGroupProps, + appIdKeyPairSettings: FieldGroupProps[], + oidcSettings: { + basicSettings: TypeItemProps, + flowSettings: FieldGroupProps[], + jwtSettings: TypeItemProps + } +} + +const AuthenticationSettingsFieldset = ({ + isServiceMesh, + authenticationMethod, + apiKeySettings, + appIdKeyPairSettings, + oidcSettings +}: Props) => { + const isOidc = authenticationMethod === OIDC_AUTH_METHOD + const isApiKey = authenticationMethod === API_KEY_METHOD + const isAppIdKey = authenticationMethod === APP_ID_KEY_METHOD + return ( + (!isServiceMesh || isOidc) && + Authentication Settings + { isApiKey && } + { isAppIdKey && } + { isOidc && } + + ) +} + +export { + AuthenticationSettingsFieldset +} diff --git a/spec/javascripts/Settings/components/AuthenticationSettingsFieldset.spec.jsx b/spec/javascripts/Settings/components/AuthenticationSettingsFieldset.spec.jsx new file mode 100644 index 0000000000..43ae417915 --- /dev/null +++ b/spec/javascripts/Settings/components/AuthenticationSettingsFieldset.spec.jsx @@ -0,0 +1,36 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { AuthenticationSettingsFieldset } from 'Settings/components/AuthenticationSettingsFieldset' +import { AUTHENTICATION_SETTINGS_DEFAULTS } from 'Settings/defaults' + +function setup (customProps = {}) { + const props = { + ...AUTHENTICATION_SETTINGS_DEFAULTS, + ...{ + isServiceMesh: false, + authenticationMethod: '1' + }, + ...customProps + } + + const view = shallow() + + return { view, props } +} + +it('should render correctly', () => { + const { view } = setup() + expect(view).toMatchSnapshot() +}) + +it('should not render when Service Mesh is active', () => { + const customProps = { isServiceMesh: true } + const { view } = setup(customProps) + expect(view).toMatchSnapshot() +}) + +it('should render only OIDC when Service Mesh is active and Oidc method is selected', () => { + const customProps = { isServiceMesh: true, authenticationMethod: 'oidc' } + const { view } = setup(customProps) + expect(view).toMatchSnapshot() +}) diff --git a/spec/javascripts/Settings/components/__snapshots__/AuthenticationSettingsFieldset.spec.jsx.snap b/spec/javascripts/Settings/components/__snapshots__/AuthenticationSettingsFieldset.spec.jsx.snap new file mode 100644 index 0000000000..154bcd9b9f --- /dev/null +++ b/spec/javascripts/Settings/components/__snapshots__/AuthenticationSettingsFieldset.spec.jsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not render when Service Mesh is active 1`] = `""`; + +exports[`should render correctly 1`] = ` + + + Authentication Settings + + + +`; + +exports[`should render only OIDC when Service Mesh is active and Oidc method is selected 1`] = ` + + + Authentication Settings + + + +`; From 5d9e091a2fb87e6434ccb643ae5b79bc8cbd75c0 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 30 Oct 2019 15:34:45 +0700 Subject: [PATCH 7/8] [settings] Settings Form component --- .../src/Settings/components/Form.jsx | 58 +++ app/javascript/src/Settings/index.jsx | 21 + .../Settings/components/Form.spec.jsx | 29 ++ .../__snapshots__/Form.spec.jsx.snap | 424 ++++++++++++++++++ 4 files changed, 532 insertions(+) create mode 100644 app/javascript/src/Settings/components/Form.jsx create mode 100644 app/javascript/src/Settings/index.jsx create mode 100644 spec/javascripts/Settings/components/Form.spec.jsx create mode 100644 spec/javascripts/Settings/components/__snapshots__/Form.spec.jsx.snap diff --git a/app/javascript/src/Settings/components/Form.jsx b/app/javascript/src/Settings/components/Form.jsx new file mode 100644 index 0000000000..3ff29e80d1 --- /dev/null +++ b/app/javascript/src/Settings/components/Form.jsx @@ -0,0 +1,58 @@ +// @flow + +import * as React from 'react' +import { useState } from 'react' +import { FormFieldset, FormLegend } from 'Form' +import { FormCollection, TextInputGroup, RadioFieldset } from 'Settings/components/Common' +import { AuthenticationSettingsFieldset } from 'Settings/components/AuthenticationSettingsFieldset' +import type { SettingsProps as Props } from 'Settings/types' + +const SERVICE_MESH_INTEGRATION = 'service_mesh_istio' +const PROXY_HOSTED_INTEGRATION = 'hosted' + +const Form = ({ + isProxyCustomUrlActive, + integrationMethod, + authenticationMethod, + proxyEndpoints, + authenticationSettings, + credentialsLocation, + security, + gatewayResponse +}: Props) => { + const [ selectedIntegrationMethod, setSelectedIntegrationMethod ] = useState(integrationMethod.value) + const [ selectedAuthenticationMethod, setSelectedAuthenticationMethod ] = useState(authenticationMethod.value) + const onChange = (setState) => (_checked, e) => setState(e.currentTarget.value) + const isServiceMesh = selectedIntegrationMethod === SERVICE_MESH_INTEGRATION + const isProxyHosted = selectedIntegrationMethod === PROXY_HOSTED_INTEGRATION + const isProxyUrlsReadOnly = !isProxyCustomUrlActive && isProxyHosted + const customProxyEndpoints = proxyEndpoints.map(endpoint => + ({ ...endpoint, readOnly: isProxyUrlsReadOnly, isDefaultValue: isProxyUrlsReadOnly })) + + return ( + + + { !isServiceMesh && } + + + { !isServiceMesh && + + + + Gateway Response + {gatewayResponse.map(settings => ( + + ))} + + } + + ) +} + +export { + Form +} diff --git a/app/javascript/src/Settings/index.jsx b/app/javascript/src/Settings/index.jsx new file mode 100644 index 0000000000..b5b1f1d842 --- /dev/null +++ b/app/javascript/src/Settings/index.jsx @@ -0,0 +1,21 @@ +// @flow + +import * as React from 'react' +import { createReactWrapper } from 'utilities/createReactWrapper' +import { Form } from 'Settings/components/Form' +import { SETTINGS_DEFAULT } from 'Settings/defaults' +import type { SettingsProps } from 'Settings/types' + +type Props = { + settings: SettingsProps, + elementId: string +} + +const initSettings = ({ + settings = SETTINGS_DEFAULT, + elementId +}: Props) => createReactWrapper(
, elementId) + +export { + initSettings +} diff --git a/spec/javascripts/Settings/components/Form.spec.jsx b/spec/javascripts/Settings/components/Form.spec.jsx new file mode 100644 index 0000000000..4dba25e04d --- /dev/null +++ b/spec/javascripts/Settings/components/Form.spec.jsx @@ -0,0 +1,29 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { Form } from 'Settings/components/Form' +import { + SETTINGS_DEFAULT, + INTEGRATION_METHOD_DEFAULTS +} from 'Settings/defaults' + +function setup (customProps = {}) { + const props = { + ...SETTINGS_DEFAULT, + ...customProps + } + + const view = shallow() + + return { view, props } +} + +it('should render correctly', () => { + const { view } = setup() + expect(view).toMatchSnapshot() +}) + +it('should not render Auth Settings, Security, Credential Locations and API Gateway when Istio is selected', () => { + const customProps = { integrationMethod: {...INTEGRATION_METHOD_DEFAULTS, value: 'service_mesh_istio'} } + const { view } = setup(customProps) + expect(view).toMatchSnapshot() +}) diff --git a/spec/javascripts/Settings/components/__snapshots__/Form.spec.jsx.snap b/spec/javascripts/Settings/components/__snapshots__/Form.spec.jsx.snap new file mode 100644 index 0000000000..04c255ff6c --- /dev/null +++ b/spec/javascripts/Settings/components/__snapshots__/Form.spec.jsx.snap @@ -0,0 +1,424 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should not render Auth Settings, Security, Credential Locations and API Gateway when Istio is selected 1`] = ` + + + + + +`; + +exports[`should render correctly 1`] = ` + + + + + + + + + + Gateway Response + + + + + + + +`; From e1a3fd6990cea569a5c4fbaa8800b2a4526492cb Mon Sep 17 00:00:00 2001 From: Damian Peralta Date: Wed, 6 Nov 2019 10:59:44 +0100 Subject: [PATCH 8/8] [patternfly] Include PF4 styles for settings page --- app/javascript/packs/settingsPageStyles.js | 1 + app/javascript/src/patternflyStyles/settingsPage.css | 7 +++++++ app/views/layouts/provider.html.slim | 1 + 3 files changed, 9 insertions(+) create mode 100644 app/javascript/packs/settingsPageStyles.js create mode 100644 app/javascript/src/patternflyStyles/settingsPage.css diff --git a/app/javascript/packs/settingsPageStyles.js b/app/javascript/packs/settingsPageStyles.js new file mode 100644 index 0000000000..76e6321fa1 --- /dev/null +++ b/app/javascript/packs/settingsPageStyles.js @@ -0,0 +1 @@ +import 'patternflyStyles/settingsPage' diff --git a/app/javascript/src/patternflyStyles/settingsPage.css b/app/javascript/src/patternflyStyles/settingsPage.css new file mode 100644 index 0000000000..04e5345cac --- /dev/null +++ b/app/javascript/src/patternflyStyles/settingsPage.css @@ -0,0 +1,7 @@ +@import '~@patternfly/patternfly/components/Button/button.css'; +@import '~@patternfly/patternfly/components/Check/check.css'; +@import '~@patternfly/patternfly/components/Form/form.css'; +@import '~@patternfly/patternfly/components/FormControl/form-control.css'; +@import '~@patternfly/patternfly/components/InputGroup/input-group.css'; +@import '~@patternfly/patternfly/components/Radio/radio.css'; +@import '~@patternfly/patternfly/components/Select/select.css'; diff --git a/app/views/layouts/provider.html.slim b/app/views/layouts/provider.html.slim index e101b583e0..36b65b8236 100644 --- a/app/views/layouts/provider.html.slim +++ b/app/views/layouts/provider.html.slim @@ -9,6 +9,7 @@ html[lang="en"] = javascript_pack_tag 'PF4Styles/base' = render 'provider/theme' = render 'provider/analytics' + = javascript_pack_tag 'settingsPageStyles' = javascript_include_tag 'provider/layout/provider' = yield :javascripts