diff --git a/public/locales/en.js b/public/locales/en.js index 767a9293..98f4d3cd 100644 --- a/public/locales/en.js +++ b/public/locales/en.js @@ -1,6 +1,9 @@ export default { en: { translation: { + aws: { + ssmParameter: 'SSM Parameter' + }, common: { all: 'All', cancel: 'Cancel', @@ -44,6 +47,7 @@ export default { textClass: 'Text Class', turnOff: 'Turn Off', turnOn: 'Turn On', + type: 'Type', updatedBy: 'Updated By', value: 'Value', welcome: 'Welcome' diff --git a/src/js/views/Project/Configuration.jsx b/src/js/views/Project/Configuration.jsx deleted file mode 100644 index 51e4ae0b..00000000 --- a/src/js/views/Project/Configuration.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types' -import React, { useContext, useEffect } from 'react' - -import { Context } from '../../state' -import { WishedFutureState } from '../../components' - -function Configuration({ urlPath }) { - const [state, dispatch] = useContext(Context) - useEffect(() => { - dispatch({ - type: 'SET_CURRENT_PAGE', - payload: { - title: 'common.configuration', - url: new URL(`${urlPath}/configuration`, state.baseURL) - } - }) - }, []) - return ( -
- - This tab will provide an abstracted interface for editing the - configuration in Consul, Vault, AWS SSM Parameter Store, and K8s - Configuration Maps. - -
- ) -} -Configuration.propTypes = { - urlPath: PropTypes.string.isRequired -} -export { Configuration } diff --git a/src/js/views/Project/Configuration/Configuration.jsx b/src/js/views/Project/Configuration/Configuration.jsx new file mode 100644 index 00000000..d05e8e62 --- /dev/null +++ b/src/js/views/Project/Configuration/Configuration.jsx @@ -0,0 +1,40 @@ +import React, { useContext, useEffect } from 'react' +import { Context } from '../../../state' +import PropTypes from 'prop-types' +import { SSMConfiguration } from './SSMConfiguration' +import { WishedFutureState } from '../../../components' + +function Configuration({ urlPath, project }) { + const [globalState, dispatch] = useContext(Context) + + useEffect(() => { + dispatch({ + type: 'SET_CURRENT_PAGE', + payload: { + title: 'common.configuration', + url: new URL(`${urlPath}/configuration`, globalState.baseURL) + } + }) + }, []) + + switch (project.configuration_type) { + case 'ssm': + return + default: + return ( +
+ + This tab will provide an abstracted interface for editing the + configuration in Consul, Vault, AWS SSM Parameter Store, and K8s + Configuration Maps. + +
+ ) + } +} + +Configuration.propTypes = { + urlPath: PropTypes.string.isRequired, + project: PropTypes.object.isRequired +} +export { Configuration } diff --git a/src/js/views/Project/Configuration/DisplaySSMParam.jsx b/src/js/views/Project/Configuration/DisplaySSMParam.jsx new file mode 100644 index 00000000..2f40ee59 --- /dev/null +++ b/src/js/views/Project/Configuration/DisplaySSMParam.jsx @@ -0,0 +1,34 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import PropTypes from 'prop-types' +import { DescriptionList } from '../../../components/DescriptionList/DescriptionList' +import { Definition } from '../../../components/DescriptionList/Definition' + +function DisplaySSMParam({ param }) { + const { t } = useTranslation() + + return ( + <> + + {param.name} + {param.type} + +

Values

+ + {param.values.map(({ environment, value }, i) => { + return ( + + {value} + + ) + })} + + + ) +} + +DisplaySSMParam.propTypes = { + param: PropTypes.object.isRequired +} + +export { DisplaySSMParam } diff --git a/src/js/views/Project/Configuration/DisplaySecureStringParam.jsx b/src/js/views/Project/Configuration/DisplaySecureStringParam.jsx new file mode 100644 index 00000000..e0bd7b6d --- /dev/null +++ b/src/js/views/Project/Configuration/DisplaySecureStringParam.jsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import PropTypes from 'prop-types' +import { DescriptionList } from '../../../components/DescriptionList/DescriptionList' +import { Definition } from '../../../components/DescriptionList/Definition' +import { Toggle } from '../../../components/Form/Toggle' + +function DisplaySecureStringParam({ param }) { + const { t } = useTranslation() + const [isShown, setIsShown] = useState(false) + + return ( + <> + + {param.name} + {param.type} + +
+

Values

+
+

Show decrypted value

+ setIsShown(value)} + name="is-hidden" + value={isShown} + /> +
+
+ + + {param.values.map(({ environment, value }, i) => { + return ( + + {isShown ? value : '********'} + + ) + })} + + + ) +} + +DisplaySecureStringParam.propTypes = { + param: PropTypes.object.isRequired +} + +export { DisplaySecureStringParam } diff --git a/src/js/views/Project/Configuration/SSMConfiguration.jsx b/src/js/views/Project/Configuration/SSMConfiguration.jsx new file mode 100644 index 00000000..36ea2bf3 --- /dev/null +++ b/src/js/views/Project/Configuration/SSMConfiguration.jsx @@ -0,0 +1,180 @@ +import PropTypes from 'prop-types' +import React, { useContext, useEffect, useRef, useState } from 'react' + +import { Context } from '../../../state' +import { Alert, Icon, Loading, Table } from '../../../components' +import { useTranslation } from 'react-i18next' +import { SlideOver } from '../../../components/SlideOver/SlideOver' +import { ViewSSMParam } from './ViewSSMParam' +import { useSearchParams } from 'react-router-dom' +import { httpGet } from '../../../utils' + +function cloneParams(searchParams) { + const newParams = new URLSearchParams() + for (const [key, value] of searchParams) { + newParams.set(key, value) + } + return newParams +} + +function SSMConfiguration({ project }) { + const [globalState, dispatch] = useContext(Context) + const { t } = useTranslation() + const [searchParams, setSearchParams] = useSearchParams() + const [onFetch, setOnFetch] = useState(true) + const [fetching, setFetching] = useState(false) + const [errorMessage, setErrorMessage] = useState() + const [rows, setRows] = useState([]) + const [slideOverOpen, setSlideOverOpen] = useState(false) + const [selectedIndex, setSelectedIndex] = useState() + const [slideOverFocusTrigger, setSlideOverFocusTrigger] = useState({}) + const [showArrows, setShowArrows] = useState(false) + const arrowLeftRef = useRef(null) + const arrowRightRef = useRef(null) + + if (searchParams.get('v') && !slideOverOpen && rows.length > 0) { + setSlideOverOpen(true) + setShowArrows(true) + setSelectedIndex( + rows.findIndex((row) => row.name === searchParams.get('v')) + ) + } else if (!searchParams.get('v') && slideOverOpen) { + setSlideOverOpen(false) + setShowArrows(false) + } + + function move(index) { + setSelectedIndex(index) + const newParams = cloneParams(searchParams) + newParams.set('v', rows[index].name) + setSearchParams(newParams) + } + + useEffect(() => { + if (fetching || !onFetch) return + setFetching(true) + + httpGet( + globalState.fetch, + new URL(`/projects/${project.id}/configuration/ssm`, globalState.baseURL), + ({ data }) => { + setRows(data.sort((a, b) => (a.name > b.name ? 1 : -1))) + setFetching(false) + }, + (message) => { + setErrorMessage(message) + setFetching(false) + } + ) + setOnFetch(false) + }, [onFetch]) + + if (fetching) return + if (errorMessage) return {errorMessage} + + return ( +
+ { + const newParams = cloneParams(searchParams) + newParams.set('v', rows[index].name) + setSearchParams(newParams) + setSlideOverOpen(true) + setSelectedIndex(index) + setShowArrows(true) + }} + checkIsHighlighted={(row) => row.name === searchParams.get('v')} + /> + + {t('aws.ssmParameter')} + {selectedIndex !== undefined && showArrows && ( + <> + + + + + )} + + } + focusTrigger={slideOverFocusTrigger} + onClose={() => { + const newParams = cloneParams(searchParams) + newParams.delete('v') + setSearchParams(newParams) + setSlideOverOpen(false) + }} + onKeyDown={(e) => { + if (!showArrows) return + if (selectedIndex > 0 && e.key === 'ArrowLeft') { + arrowLeftRef.current?.focus() + move(selectedIndex - 1) + } else if ( + selectedIndex < rows.length - 1 && + e.key === 'ArrowRight' + ) { + arrowRightRef.current?.focus() + move(selectedIndex + 1) + } + }}> + + + + ) +} + +SSMConfiguration.propTypes = { + project: PropTypes.object.isRequired +} +export { SSMConfiguration } diff --git a/src/js/views/Project/Configuration/ViewSSMParam.jsx b/src/js/views/Project/Configuration/ViewSSMParam.jsx new file mode 100644 index 00000000..6ed6105b --- /dev/null +++ b/src/js/views/Project/Configuration/ViewSSMParam.jsx @@ -0,0 +1,18 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { DisplaySSMParam } from './DisplaySSMParam' +import { DisplaySecureStringParam } from './DisplaySecureStringParam' + +function ViewSSMParam({ param }) { + if (param.type === 'SecureString') { + return + } else { + return + } +} + +ViewSSMParam.propTypes = { + param: PropTypes.object +} + +export { ViewSSMParam } diff --git a/src/js/views/Project/Project.jsx b/src/js/views/Project/Project.jsx index 485e67b5..e3e9c403 100644 --- a/src/js/views/Project/Project.jsx +++ b/src/js/views/Project/Project.jsx @@ -21,13 +21,13 @@ import { Tooltip } from '../../components' -import { Configuration } from './Configuration' import { Dependencies } from './Dependencies' import { Logs } from './Logs' import { Notes } from './Notes' import { Overview } from './Overview' import { Settings } from './Settings' import { OperationsLog } from '../OperationsLog' +import { Configuration } from './Configuration/Configuration' function ProjectPage({ project, factTypes, refresh }) { const [state, dispatch] = useContext(Context) @@ -132,7 +132,7 @@ function ProjectPage({ project, factTypes, refresh }) { /> } + element={} />