diff --git a/src/plugins/credential_management/public/components/common/components/create_credential_form/create_credential_form.tsx b/src/plugins/credential_management/public/components/common/components/create_credential_form/create_credential_form.tsx index 592a8d9dc524..91971f9b9b6f 100644 --- a/src/plugins/credential_management/public/components/common/components/create_credential_form/create_credential_form.tsx +++ b/src/plugins/credential_management/public/components/common/components/create_credential_form/create_credential_form.tsx @@ -16,6 +16,7 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; import { DocLinksStart } from 'src/core/public'; import { context as contextType } from '../../../../../../opensearch_dashboards_react/public'; @@ -150,11 +151,14 @@ export class CredentialForm extends React.Component; + return
; } renderContent = () => { diff --git a/src/plugins/credential_management/public/components/common/components/header/header.tsx b/src/plugins/credential_management/public/components/common/components/header/header.tsx index 95cccfa99c2d..ec19d6cfbbef 100644 --- a/src/plugins/credential_management/public/components/common/components/header/header.tsx +++ b/src/plugins/credential_management/public/components/common/components/header/header.tsx @@ -16,58 +16,70 @@ import { CredentialManagementContext } from '../../../../types'; export const Header = ({ prompt, docLinks, + headerTitle, + isBeta = false, + isCreateCredential = false, }: { prompt?: React.ReactNode; - docLinks: DocLinksStart; + docLinks?: DocLinksStart; + headerTitle: string; + isBeta?: boolean; + isCreateCredential: boolean; }) => { const changeTitle = useOpenSearchDashboards().services.chrome .docTitle.change; - const createCredentialHeader = i18n.translate('credentialManagement.createIndexPatternHeader', { - defaultMessage: 'Create Stored Credential', - }); - changeTitle(createCredentialHeader); + changeTitle(headerTitle); return (

- {createCredentialHeader} - <> - {' '} - - + {headerTitle} + + {isBeta ? ( + <> + + + ) : null}

- -

- username-password-credential, - first: os-service-log, - second: os-application-log, - }} - /> -
- - - -

-
+ + {isCreateCredential ? ( + <> + +

+ username-password-credential, + first: os-service-log, + second: os-application-log, + }} + /> +
+ + + + +

+
+ + ) : null} + {prompt ? ( <> diff --git a/src/plugins/credential_management/public/components/common/text_content/text_content.ts b/src/plugins/credential_management/public/components/common/text_content/text_content.ts index f9543cee8243..1cae69955668 100644 --- a/src/plugins/credential_management/public/components/common/text_content/text_content.ts +++ b/src/plugins/credential_management/public/components/common/text_content/text_content.ts @@ -54,3 +54,24 @@ export const confirmButtonOnDeleteComfirmText = i18n.translate( defaultMessage: 'Delete', } ); + +export const credentialEditPageAuthType = i18n.translate( + 'credentialManagement.textContent.credentialEditPageAuthType', + { + defaultMessage: 'Username & password', + } +); + +export const credentialEditPageAuthTitle = i18n.translate( + 'credentialManagement.textContent.credentialEditPageAuthTitle', + { + defaultMessage: 'Authentication Details', + } +); + +export const credentialEditPageInfoTitle = i18n.translate( + 'credentialManagement.textContent.credentialEditPageInfoTitle', + { + defaultMessage: 'Saved Credential Information', + } +); diff --git a/src/plugins/credential_management/public/components/credential_table/credentials_table.tsx b/src/plugins/credential_management/public/components/credential_table/credentials_table.tsx index 9d51bbe21d64..38c7cb208d25 100644 --- a/src/plugins/credential_management/public/components/credential_table/credentials_table.tsx +++ b/src/plugins/credential_management/public/components/credential_table/credentials_table.tsx @@ -22,13 +22,11 @@ import { EuiBadgeGroup, EuiPageContent, EuiTitle, - EuiSearchBar, EuiConfirmModal, EuiLoadingSpinner, EuiOverlayMask, EuiGlobalToastList, EuiGlobalToastListToast, - Query, } from '@elastic/eui'; import { @@ -66,23 +64,13 @@ interface Props extends RouteComponentProps { export const CredentialsTable = ({ canSave, history }: Props) => { const [credentials, setCredentials] = React.useState([]); const [selectedCredentials, setSelectedCredentials] = React.useState([]); - const { setBreadcrumbs } = useOpenSearchDashboards().services; - - /* Update breadcrumb*/ - useEffectOnce(() => { - setBreadcrumbs(getListBreadcrumbs()); - }); - const [isLoading, setIsLoading] = React.useState(false); const [isDeleting, setIsDeleting] = React.useState(false); const [confirmDeleteVisible, setConfirmDeleteVisible] = React.useState(false); const [toasts, setToasts] = React.useState([]); - const [searchText, setSearchText] = React.useState(''); - const { savedObjects, uiSettings } = useOpenSearchDashboards< - CredentialManagementContext - >().services; + const { savedObjects } = useOpenSearchDashboards().services; const columns = [ { @@ -140,28 +128,6 @@ export const CredentialsTable = ({ canSave, history }: Props) => { onSelectionChange, }; - const renderDeleteButton = () => { - let deleteButtonMsg = 'Delete'; - - if (selectedCredentials.length === 1) { - deleteButtonMsg = `${deleteButtonMsg} ${selectedCredentials.length} Credential`; - } else if (selectedCredentials.length > 1) { - deleteButtonMsg = `${deleteButtonMsg} ${selectedCredentials.length} Credentials`; - } - return ( - { - setConfirmDeleteVisible(true); - }} - disabled={selectedCredentials.length === 0} - > - {deleteButtonMsg} - - ); - }; - const onClickDelete = async () => { try { setIsDeleting(true); @@ -193,25 +159,72 @@ export const CredentialsTable = ({ canSave, history }: Props) => { } }; - const deleteButton = renderDeleteButton(); + const removeToast = (id: string) => { + setToasts(toasts.filter((toast) => toast.id !== id)); + }; + + const createButton = canSave ? : <>; + + const renderDeleteButton = () => { + let deleteButtonMsg = 'Delete'; + + if (selectedCredentials.length === 1) { + deleteButtonMsg = `${deleteButtonMsg} ${selectedCredentials.length} Credential`; + } else if (selectedCredentials.length > 1) { + deleteButtonMsg = `${deleteButtonMsg} ${selectedCredentials.length} Credentials`; + } + return ( + { + setConfirmDeleteVisible(true); + }} + disabled={selectedCredentials.length === 0} + > + {deleteButtonMsg} + + ); + }; + + /* create a button to the right of search bar*/ + const renderToolsRight = () => { + return ( + + {renderDeleteButton()} + + ); + }; - React.useEffect(() => { + const search = { + toolsRight: renderToolsRight(), + box: { + incremental: true, + schema: { + fields: { title: { type: 'string' } }, + }, + }, + }; + + /* Update breadcrumb*/ + useEffectOnce(() => { + setBreadcrumbs(getListBreadcrumbs()); + }); + + /* fetch credential*/ + useEffectOnce(() => { (async () => { setIsLoading(true); const fetchedCredentials: CredentialsTableItem[] = await getCredentials(savedObjects.client); - const fetchedCredentialsResults = fetchedCredentials.filter((row) => { - return row.title.includes(searchText); - }); + setCredentials(fetchedCredentials); - setCredentials(fetchedCredentialsResults); setIsLoading(false); })(); - }, [history.push, credentials.length, uiSettings, savedObjects.client, searchText]); - - const createButton = canSave ? : <>; + }); - const tableRenderDeleteModal = () => { + /* render delete modal*/ + const renderTableDeleteModal = () => { return confirmDeleteVisible ? ( { ) : null; }; - const onSearchChange = ({ - query, - }: { - query: Query | null; - error: { message: string } | null; - }) => { - setSearchText(query!.text); - }; - - const removeToast = (id: string) => { - setToasts(toasts.filter((toast) => toast.id !== id)); - }; - const renderContent = () => { return ( @@ -272,17 +272,7 @@ export const CredentialsTable = ({ canSave, history }: Props) => { {createButton} - onSearchChange(e)} - toolsRight={[ - - {deleteButton} - , - ]} - /> - - {tableRenderDeleteModal()} + {renderTableDeleteModal()} @@ -295,6 +285,7 @@ export const CredentialsTable = ({ canSave, history }: Props) => { columns={columns} pagination={pagination} sorting={sorting} + search={search} loading={isLoading} /> diff --git a/src/plugins/credential_management/public/components/edit_credential_wizard/components/edit_credential.tsx b/src/plugins/credential_management/public/components/edit_credential_wizard/components/edit_credential.tsx index 4b036d7d2e87..6a75e6785f3e 100644 --- a/src/plugins/credential_management/public/components/edit_credential_wizard/components/edit_credential.tsx +++ b/src/plugins/credential_management/public/components/edit_credential_wizard/components/edit_credential.tsx @@ -14,8 +14,6 @@ import { EuiDescribedFormGroup, EuiFormRow, EuiFieldText, - EuiSelect, - EuiLink, EuiButton, EuiPageContent, EuiFieldPassword, @@ -25,6 +23,16 @@ import { EuiConfirmModal, EuiLoadingSpinner, EuiOverlayMask, + EuiFlexGroup, + EuiText, + EuiSpacer, + EuiBottomBar, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, } from '@elastic/eui'; import { DocLinksStart } from 'src/core/public'; import { @@ -34,20 +42,22 @@ import { import { getCreateBreadcrumbs } from '../../breadcrumbs'; import { CredentialManagmentContextValue } from '../../../types'; -// TODO: Add Header https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2051 import { context as contextType } from '../../../../../opensearch_dashboards_react/public'; import { EditCredentialItem } from '../../types'; import { LocalizedContent } from '../../common/text_content'; +import { Header } from '../../common/components/header'; interface EditCredentialState { credentialName: string; credentialMaterialsType: string; + credentialDescription: string; username?: string; password?: string; dual: boolean; toasts: EuiGlobalToastListToast[]; docLinks: DocLinksStart; - isVisible: boolean; + isDeleteModalVisible: boolean; + isUpdateModalVisible: boolean; isLoading: boolean; } @@ -63,76 +73,42 @@ export class EditCredentialComponent extends React.Component< public readonly context!: CredentialManagmentContextValue; constructor(props: EditCredentialProps, context: CredentialManagmentContextValue) { super(props, context); - context.services.setBreadcrumbs(getCreateBreadcrumbs()); this.state = { credentialName: props.credential.title, credentialMaterialsType: CredentialMaterialsType.UsernamePasswordType, - username: undefined, + credentialDescription: props.credential.description || '', + username: props.credential.username || '', password: undefined, dual: true, toasts: [], docLinks: context.services.docLinks, - isVisible: false, + isDeleteModalVisible: false, + isUpdateModalVisible: false, isLoading: false, }; } - - confirmDelete = async () => { - const { savedObjects } = this.context.services; - this.setState({ isLoading: true }); - try { - await savedObjects.client.delete(CREDENTIAL_SAVED_OBJECT_TYPE, this.props.credential.id); - this.props.history.push(''); - } catch (e) { - const deleteCredentialFailMsg = ( - - ); - this.setState((prevState) => ({ - toasts: prevState.toasts.concat([ - { - title: deleteCredentialFailMsg, - id: deleteCredentialFailMsg.props.id, - color: 'warning', - iconType: 'alert', - }, - ]), - })); - } - this.setState({ isLoading: false }); - }; - - delelteButtonRender() { + /* Render methods */ + renderDelelteButton() { return ( <> -
- - - - - -
+ + + + + - {this.state.isVisible ? ( + {this.state.isDeleteModalVisible ? ( { - this.setState({ isVisible: false }); + this.setState({ isDeleteModalVisible: false }); }} onConfirm={this.confirmDelete} cancelButtonText={LocalizedContent.cancelButtonOnDeleteCancelText} @@ -147,93 +123,155 @@ export class EditCredentialComponent extends React.Component< ); } + renderHeader() { + return
; + } + + renderUpdatePasswordModal() { + const closeModal = () => this.setState({ isUpdateModalVisible: false }); + return ( + <> + Update Password + + {this.state.isUpdateModalVisible ? ( + + + +

Update password

+
+
+ + + this.setState({ password: e.target.value })} + /> + + + + Cancel + + Update + + +
+ ) : null} + + ); + } - renderContent() { - const options = [ - { - value: CredentialMaterialsType.UsernamePasswordType, - text: 'Username and Password Credential', - }, - ]; + renderContent = () => { + const header = this.renderHeader(); + const deleteButton = this.renderDelelteButton(); return ( - - {this.delelteButtonRender()} - - + + + + {header} + {deleteButton} + + + + + + + +

{LocalizedContent.credentialEditPageInfoTitle}

+
+
+
+ + Credential Name} - description={

The name of credential that you want to create

} + title={

Credential Details

} + description={ +

+ The credential information is used for reference in tables and when adding to a data + source connection +

+ } > - + this.setState({ credentialName: e.target.value })} /> + + this.setState({ credentialDescription: e.target.value })} + /> +
+
+
+ + + + +

{LocalizedContent.credentialEditPageAuthTitle}

+
+
+
+ + Credential Type} + title={

Authentication Details

} description={ -
-

- The type of credential that you want to create{' '} - - Credential Types Supported - -

-
    -
  • - For username_password_credential type: this type can be used for{' '} - credentials in format of username, password.{' '} -
  • -
  • Ex: OpenSearch basic auth
  • -
-
    -
  • - For aws_iam_credential type: this type can only be used for aws iam - credential, with aws_access_key_id, aws_secret_access_key, and region (optional) -
  • -
-
+

Modify these to update the authentication type and associated details

} > - - this.setState({ credentialMaterialsType: e.target.value })} - options={options} - /> + + {LocalizedContent.credentialEditPageAuthType} - + + + this.setState({ username: e.target.value })} /> - this.setState({ password: e.target.value })} - /> + + + ************* + + {this.renderUpdatePasswordModal()} +
- - Update - -
-
- ); - } - removeToast = (id: string) => { - this.setState((prevState) => ({ - toasts: prevState.toasts.filter((toast) => toast.id !== id), - })); + + + + + Cancel changes + + + + + Save changes + + + + + + + ); }; render() { @@ -256,31 +294,85 @@ export class EditCredentialComponent extends React.Component< ); } + /* Events */ + removeToast = (id: string) => { + this.setState((prevState) => ({ + toasts: prevState.toasts.filter((toast) => toast.id !== id), + })); + }; removeCredential = async () => { - this.setState({ isVisible: true }); + this.setState({ isDeleteModalVisible: true }); + }; + + updateCredentialPassword = async () => { + this.setState({ isUpdateModalVisible: true }); }; - updateCredential = async () => { + updateCredentialInformation = async () => { const { savedObjects } = this.context.services; this.setState({ isLoading: true }); + try { - await savedObjects.client.update('credential', this.props.credential.id, { + const credentialAttributes = { title: this.state.credentialName, + description: this.state.credentialDescription, credentialMaterials: { credentialMaterialsType: this.state.credentialMaterialsType, credentialMaterialsContent: { username: this.state.username, + }, + }, + }; + await savedObjects.client.update( + CREDENTIAL_SAVED_OBJECT_TYPE, + this.props.credential.id, + credentialAttributes + ); + this.props.history.push(''); + } catch (e) { + const editCredentialFailMsg = ( + + ); + this.setState((prevState) => ({ + toasts: prevState.toasts.concat([ + { + title: editCredentialFailMsg, + id: editCredentialFailMsg.props.id, + color: 'warning', + iconType: 'alert', + }, + ]), + })); + } + this.setState({ isLoading: false }); + }; + + updateCredentialPasswordField = async () => { + const { savedObjects } = this.context.services; + this.setState({ isLoading: true, isUpdateModalVisible: false }); + try { + const credentialAttributes = { + credentialMaterials: { + credentialMaterialsContent: { password: this.state.password, }, }, - }); + }; + await savedObjects.client.update( + CREDENTIAL_SAVED_OBJECT_TYPE, + this.props.credential.id, + credentialAttributes + ); this.props.history.push(''); } catch (e) { const editCredentialFailMsg = ( ); this.setState((prevState) => ({ @@ -296,6 +388,33 @@ export class EditCredentialComponent extends React.Component< } this.setState({ isLoading: false }); }; + + confirmDelete = async () => { + const { savedObjects } = this.context.services; + this.setState({ isLoading: true }); + try { + await savedObjects.client.delete(CREDENTIAL_SAVED_OBJECT_TYPE, this.props.credential.id); + this.props.history.push(''); + } catch (e) { + const deleteCredentialFailMsg = ( + + ); + this.setState((prevState) => ({ + toasts: prevState.toasts.concat([ + { + title: deleteCredentialFailMsg, + id: deleteCredentialFailMsg.props.id, + color: 'warning', + iconType: 'alert', + }, + ]), + })); + } + this.setState({ isLoading: false }); + }; } export const EditCredential = withRouter(EditCredentialComponent); diff --git a/src/plugins/data_source/server/saved_objects/credential_saved_objects_client_wrapper.ts b/src/plugins/data_source/server/saved_objects/credential_saved_objects_client_wrapper.ts index 4e116ce61023..b9edbb5221ef 100644 --- a/src/plugins/data_source/server/saved_objects/credential_saved_objects_client_wrapper.ts +++ b/src/plugins/data_source/server/saved_objects/credential_saved_objects_client_wrapper.ts @@ -138,15 +138,27 @@ export class CredentialSavedObjectsClientWrapper { } private async validateAndEncryptPartialAttributes(attributes: T) { - this.validateCredentialMaterials(attributes.credentialMaterials); + const { credentialMaterials } = attributes; + const { credentialMaterialsContent } = credentialMaterials; + + if ('password' in credentialMaterialsContent) { + this.validatePassword(credentialMaterialsContent.password); + return { + ...attributes, + credentialMaterials: await this.encryptUsernamePasswordTypedCredentialMaterials( + credentialMaterials + ), + }; + } else { + this.validateAttributes(attributes); + } return await this.encryptCredentialMaterials(attributes); } private validateAttributes(attributes: T) { const { title, credentialMaterials } = attributes; - - if (title === undefined) { + if (!title) { throw SavedObjectsErrorHelpers.createBadRequestError('attribute "title" required'); } @@ -175,33 +187,24 @@ export class CredentialSavedObjectsClientWrapper { } } - private validateUsernamePasswordTypedContent(credentialMaterialsContent: T) { - const { username, password } = credentialMaterialsContent; - - if (username === undefined) { - throw SavedObjectsErrorHelpers.createBadRequestError('attribute "username" required'); - } - - if (password === undefined) { - throw SavedObjectsErrorHelpers.createBadRequestError('attribute "password" required'); - } - - return; - } - private async encryptCredentialMaterials(attributes: T) { const { credentialMaterials } = attributes; const { credentialMaterialsType, credentialMaterialsContent } = credentialMaterials; + const { username } = credentialMaterialsContent; switch (credentialMaterialsType) { case CredentialMaterialsType.UsernamePasswordType: - this.validateUsernamePasswordTypedContent(credentialMaterialsContent); + this.validateUsername(username); + return { ...attributes, - credentialMaterials: await this.encryptUsernamePasswordTypedCredentialMaterials( - credentialMaterials - ), + credentialMaterials: { + credentialMaterialsType, + credentialMaterialsContent: { + username, + }, + }, }; default: throw SavedObjectsErrorHelpers.createBadRequestError( @@ -210,6 +213,20 @@ export class CredentialSavedObjectsClientWrapper { } } + private validateUsername(username: T) { + if (!username) { + throw SavedObjectsErrorHelpers.createBadRequestError('attribute "username" required'); + } + return; + } + + private validatePassword(password: T) { + if (!password) { + throw SavedObjectsErrorHelpers.createBadRequestError('attribute "password" required'); + } + return; + } + private async encryptUsernamePasswordTypedCredentialMaterials( credentialMaterials: T ) {