Skip to content
5 changes: 5 additions & 0 deletions common/constants/data_connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ export const OPENSEARCH_DOCUMENTATION_URL =

export const OPENSEARCH_ACC_DOCUMENTATION_URL =
'https://opensearch.org/docs/latest/data-acceleration/index';

export const QUERY_RESTRICT = 'query-restrict';
export const QUERY_ALL = 'query-all';
export const ACCELERATION_RESTRICT = 'acceleration-restrict';
export const ACCELERATION_ALL = 'acceleration-all';
15 changes: 15 additions & 0 deletions common/types/data_connections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { EuiRadioGroupOption } from '@elastic/eui';

export interface PermissionsFlexItem {
roles: Array<{ label: string }>;
selectedRoles: Array<{ label: string }>;
selectedRadio: string;
setSelectedRoles: (selectedRoles: Array<{ label: string }>) => void;
setSelectedRadio: (selectedRadio: string) => void;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but Ideally types here should be something like: React.Dispatch<React.SetStateAction<Array<{ label: string }>>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in: fc96304, thanks! I didn't know this

radios: EuiRadioGroupOption[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
EuiComboBox,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiRadioGroup,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import React from 'react';
import { PermissionsFlexItem } from '../../../../common/types/data_connections';
import {
OPENSEARCH_DOCUMENTATION_URL,
QUERY_ALL,
} from '../../../../common/constants/data_connections';

export const AccelerationPermissionsFlexItem = (props: PermissionsFlexItem) => {
const { roles, setSelectedRoles, selectedRoles, selectedRadio, radios, setSelectedRadio } = props;
return (
<EuiFlexItem>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiText className="overview-title">Acceleration Permissions</EuiText>
<EuiText size="s" className="overview-content">
Control which OpenSearch roles have permissions to accelerate external data through
OpenSearch indexing.{' '}
<EuiLink external={true} href={OPENSEARCH_DOCUMENTATION_URL} target="_blank">
Learn more
</EuiLink>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiRadioGroup
options={radios}
idSelected={selectedRadio}
onChange={(id) => setSelectedRadio(id)}
name="acceleration-radio-group"
legend={{
children: <span>Access level</span>,
}}
/>
{selectedRadio === QUERY_ALL ? (
<div>
<EuiSpacer size="s" />
<EuiText>OpenSearch Roles</EuiText>
<EuiText size="xs">
Select one or more OpenSearch roles that can query this data connection.
</EuiText>
<EuiComboBox
placeholder="Select one or more options"
options={roles}
selectedOptions={selectedRoles}
onChange={setSelectedRoles}
isClearable={true}
data-test-subj="query-permissions-combo-box"
/>
</div>
) : (
<></>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { EuiCallOut } from '@elastic/eui';
import React from 'react';

export const AccessControlCallout = () => {
return (
<EuiCallOut title="Configurations may be managed elsewhere." iconType="iInCircle">
Access to data can be managed in other systems outside of OpenSearch. Check with your
administrator for additional configurations.
</EuiCallOut>
);
};
205 changes: 205 additions & 0 deletions public/components/data_connections/components/access_control_tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
EuiHorizontalRule,
EuiBottomBar,
EuiButtonEmpty,
} from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import { EuiPanel } from '@elastic/eui';
import {
ACCELERATION_ALL,
ACCELERATION_RESTRICT,
QUERY_ALL,
QUERY_RESTRICT,
} from 'common/constants/data_connections';
import { AccessControlCallout } from './access_control_callout';
import { coreRefs } from '../../../../public/framework/core_refs';
import { QueryPermissionsFlexItem } from './query_permissions_flex_item';
import { AccelerationPermissionsFlexItem } from './acceleration_permissions_flex_item';

export const AccessControlTab = () => {
const [mode, setMode] = useState<'view' | 'edit'>('view');
const [roles, setRoles] = useState<Array<{ label: string }>>([]);
const [selectedQueryPermissionRoles, setSelectedQueryPermissionRoles] = useState<
Array<{ label: string }>
>([]);
const [selectedAccelerationPermissionRoles, setSelectedAccelerationPermissionRoles] = useState<
Array<{ label: string }>
>([]);
const [queryPermissionRadioSelected, setQueryRadioIdSelected] = useState(`query-1`);
const [accelerationPermissionRadioSelected, setAccelerationRadioIdSelected] = useState(
`acceleration-1`
);

const [finalQueryPermissionsPlaceHolder, setFinalQueryPermissionsPlaceHolder] = useState<
Array<{ label: string }>
>([]);
const [
finalAccelerationPermissionsPlaceHolder,
setFinalAccelerationPermissionsPlaceHolder,
] = useState<Array<{ label: string }>>([]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Placeholder to update the state (this will be replaced with API call to POST, and then showing the list will be the result of the API call to GET)


useEffect(() => {
coreRefs.http!.get('/api/v1/configuration/roles').then((data) =>
setRoles(
Object.keys(data.data).map((key) => {
return { label: key };
})
)
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assumes security plugin is installed and user has permission to access it (will be addressed with the permission PR)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if security plugin is not installed. Are we not showing the whole data connections plugin? or Do we have a fallback view?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think current plan is to show as much as possible:

  • if users have access to datasources APIs, we show them the page, and if they do not then we will show Fallback UI
  • if users have access to datasource APIs, but do not have access to security plugin or security plugin is not installed, we will not be able to show the dropdown with pre-defined roles, but we will still allow them to manually type in the roles. Created a follow up issue to avoid scope-creep: [FEATURE] Access Control flow if Security Plugin is not installed #1022. Will not implement manually type to create roles in this PR, but in follow up PR.

}, []);

const queryRadios = [
{
id: QUERY_RESTRICT,
label: 'Restricted - accessible by users with specific OpenSearch roles',
},
{
id: QUERY_ALL,
label: 'Everyone - accessible by all users on this cluster',
},
];
const accelerationRadios = [
{
id: ACCELERATION_RESTRICT,
label: 'Restricted - accessible by users with specific OpenSearch roles',
},
{
id: ACCELERATION_ALL,
label: 'Everyone - accessible by all users on this cluster',
},
];

const renderViewAccessControlDetails = () => {
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiText className="overview-title">Query access</EuiText>
<EuiText size="s" className="overview-content">
{finalQueryPermissionsPlaceHolder.length
? `Restricted to ${finalQueryPermissionsPlaceHolder.map((role) => role.label)}`
: '-'}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiText className="overview-title">Acceleration permissions</EuiText>
<EuiText size="s" className="overview-content">
{finalAccelerationPermissionsPlaceHolder.length
? `Restricted to ${finalAccelerationPermissionsPlaceHolder.map(
(role) => role.label
)}`
: '-'}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};

const renderEditAccessControlDetails = () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please turn this into a formal React FC.
This is a quality issue for us throughout our plugin.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to address this in: fc96304, please let me know if I understood what you meant correctly.

return (
<EuiFlexGroup direction="column">
<QueryPermissionsFlexItem

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not call these components "...FlexItem". The component's purpose is not a consequence of it's inclusion in a flex-group.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Done in: fc96304, thanks for the suggestion.

roles={roles}
selectedRoles={selectedQueryPermissionRoles}
setSelectedRoles={setSelectedQueryPermissionRoles}
selectedRadio={queryPermissionRadioSelected}
setSelectedRadio={setQueryRadioIdSelected}
radios={queryRadios}
/>
<AccelerationPermissionsFlexItem
roles={roles}
selectedRoles={selectedAccelerationPermissionRoles}
setSelectedRoles={setSelectedAccelerationPermissionRoles}
selectedRadio={accelerationPermissionRadioSelected}
setSelectedRadio={setAccelerationRadioIdSelected}
radios={accelerationRadios}
/>
</EuiFlexGroup>
);
};

const saveChanges = () => {
setFinalAccelerationPermissionsPlaceHolder(selectedAccelerationPermissionRoles);
setFinalQueryPermissionsPlaceHolder(selectedQueryPermissionRoles);
setMode('view');
};

return (
<>
<EuiSpacer />
<AccessControlCallout />
<EuiSpacer />
<EuiPanel>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiText size="m">
<h2 className="panel-title">Access Control</h2>
Control which OpenSearch users have access to this data source.
</EuiText>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="createButton"
onClick={
mode === 'view'
? () => {
setMode('edit');
}
: () => {
setMode('view');
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can make it more readable or change it to a function.

const changeMode = () => { 
     mode === 'view' ? setMode('edit') : setMode('view');
}
onClick = {changeMode}

OR 

onClick = { () => setMode(mode === 'view' ? 'edit' : 'view')}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in: fc96304, thanks!

}
fill={mode === 'view' ? true : false}
>
{mode === 'view' ? 'Edit' : 'Cancel'}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule />
{mode === 'view' ? renderViewAccessControlDetails() : renderEditAccessControlDetails()}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider setting this value to a component-constant (AccessControlDetails). You have several of these conditions - they can all be collected into one place... thereby reducing cyclomatic complexity.

Containing these conditions into one location will also raise the visibility of refactoring / simplification opportunities.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure I understand what you are suggesting/how it would be cleaner than having a state variable. Can you point me to some examples I can look at?

</EuiPanel>
<EuiSpacer />
{mode === 'edit' ? (
<EuiBottomBar affordForDisplacement={false}>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={() => {
setMode('view');
}}
color="ghost"
size="s"
iconType="cross"
>
Discard change(s)
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={saveChanges} size="s" iconType="check" fill>
Save
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>
) : null}

@ps48 ps48 Sep 13, 2023

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, this can be changed to mode === 'edit' && <view/> no need to use null.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in: fc96304, thanks!

</>
);
};
8 changes: 6 additions & 2 deletions public/components/data_connections/components/datasource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import {
EuiIcon,
EuiCard,
EuiTab,
EuiTabs,
EuiTabbedContent,
} from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import { DATASOURCES_BASE } from '../../../../common/constants/shared';
import { AccessControlTab } from './access_control_tab';

interface DatasourceDetails {
allowedRoles: string[];
Expand Down Expand Up @@ -52,16 +53,19 @@ export const DataSource = (props: any) => {
id: 'data',
name: 'Data',
disabled: false,
content: <></>,
},
{
id: 'access_control',
name: 'Access control',
disabled: false,
content: <AccessControlTab />,
},
{
id: 'connection_configuration',
name: 'Connection configuration',
disabled: false,
content: <></>,
},
];

Expand Down Expand Up @@ -169,7 +173,7 @@ export const DataSource = (props: any) => {
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
<EuiTabs>{renderTabs()}</EuiTabs>
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} autoFocus="selected" />

<EuiSpacer />
</EuiPageBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const DataConnectionsHeader = () => {
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
Connect and manage compatible OpenSearch Dashboard data sources and compute.{' '}
<EuiLink external={true} href={OPENSEARCH_DOCUMENTATION_URL} target="blank">
<EuiLink external={true} href={OPENSEARCH_DOCUMENTATION_URL} target="_blank">
Learn more
</EuiLink>
</EuiText>
Expand Down
Loading