LDAP: Use InteractiveTable and remove gf-form usage (#80291)

* Use InteractiveTable

* Remove unused return

* Fix icon alignment

* InteractiveTable in LdapUserMappingInfo

* Update no teams text

* InteractiveTable in LdapUserGroups

* Remove unused code

* Cleanup

* LdapSyncInfo to InteractiveTable

* Update more tables

* Memoize

* Fix connection status

* Update lockfile

* Refactor LdapSyncInfo

* Fix lockfile

* Remove showAttributeMapping as it is always true
This commit is contained in:
Tobias Skarhed
2024-01-25 17:01:22 +01:00
committed by GitHub
parent 5195e5347e
commit add5a5c01e
9 changed files with 277 additions and 328 deletions

View File

@@ -6718,35 +6718,6 @@ exports[`no gf-form usage`] = {
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
], ],
"public/app/features/admin/ldap/LdapConnectionStatus.tsx:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],
"public/app/features/admin/ldap/LdapSyncInfo.tsx:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],
"public/app/features/admin/ldap/LdapUserGroups.tsx:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],
"public/app/features/admin/ldap/LdapUserInfo.tsx:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],
"public/app/features/admin/ldap/LdapUserMappingInfo.tsx:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],
"public/app/features/admin/ldap/LdapUserPermissions.tsx:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],
"public/app/features/admin/ldap/LdapUserTeams.tsx:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"]
],
"public/app/features/admin/partials/edit_org.html:5381": [ "public/app/features/admin/partials/edit_org.html:5381": [
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],
[0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"], [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"],

View File

@@ -1,48 +1,65 @@
import React from 'react'; import React, { useMemo } from 'react';
import { Alert, Icon } from '@grafana/ui'; import { Alert, CellProps, Column, Icon, InteractiveTable, Stack, Text, Tooltip } from '@grafana/ui';
import { AppNotificationSeverity, LdapConnectionInfo, LdapServerInfo } from 'app/types'; import { AppNotificationSeverity, LdapConnectionInfo, LdapServerInfo } from 'app/types';
interface Props { interface Props {
ldapConnectionInfo: LdapConnectionInfo; ldapConnectionInfo: LdapConnectionInfo;
} }
interface ServerInfo {
host: string;
port: number;
available: boolean;
}
export const LdapConnectionStatus = ({ ldapConnectionInfo }: Props) => { export const LdapConnectionStatus = ({ ldapConnectionInfo }: Props) => {
return ( const columns = useMemo<Array<Column<ServerInfo>>>(
<> () => [
<h3 className="page-heading">LDAP Connection</h3> {
<div className="gf-form-group"> id: 'host',
<div className="gf-form"> header: 'Host',
<table className="filter-table form-inline"> disableGrow: true,
<thead> },
<tr> {
<th>Host</th> id: 'port',
<th colSpan={2}>Port</th> header: 'Port',
</tr> disableGrow: true,
</thead> },
<tbody> {
{ldapConnectionInfo && id: 'available',
ldapConnectionInfo.map((serverInfo, index) => ( cell: (serverInfo: CellProps<ServerInfo>) => {
<tr key={index}> return serverInfo.cell.value ? (
<td>{serverInfo.host}</td> <Stack justifyContent="end">
<td>{serverInfo.port}</td> <Tooltip content="Connection is available">
<td>
{serverInfo.available ? (
<Icon name="check" className="pull-right" /> <Icon name="check" className="pull-right" />
</Tooltip>
</Stack>
) : ( ) : (
<Icon name="exclamation-triangle" className="pull-right" /> <Stack justifyContent="end">
)} <Tooltip content="Connection is not available">
</td> <Icon name="exclamation-triangle" />
</tr> </Tooltip>
))} </Stack>
</tbody> );
</table> },
</div> },
<div className="gf-form-group"> ],
[]
);
const data = useMemo<ServerInfo[]>(() => ldapConnectionInfo, [ldapConnectionInfo]);
return (
<section>
<Stack direction="column" gap={2}>
<Text color="primary" element="h3">
LDAP Connection
</Text>
<InteractiveTable data={data} columns={columns} getRowId={(serverInfo) => serverInfo.host + serverInfo.port} />
<LdapErrorBox ldapConnectionInfo={ldapConnectionInfo} /> <LdapErrorBox ldapConnectionInfo={ldapConnectionInfo} />
</div> </Stack>
</div> </section>
</>
); );
}; };

View File

@@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { NavModelItem } from '@grafana/data'; import { NavModelItem } from '@grafana/data';
import { featureEnabled } from '@grafana/runtime'; import { featureEnabled } from '@grafana/runtime';
import { Alert, Button, Field, Form, HorizontalGroup, Input } from '@grafana/ui'; import { Alert, Button, Field, Form, Input, Stack } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
@@ -97,7 +97,7 @@ export class LdapPage extends PureComponent<Props, State> {
return ( return (
<Page navId="authentication" pageNav={pageNav}> <Page navId="authentication" pageNav={pageNav}>
<Page.Contents isLoading={isLoading}> <Page.Contents isLoading={isLoading}>
<> <Stack direction="column" gap={4}>
{ldapError && ldapError.title && ( {ldapError && ldapError.title && (
<Alert title={ldapError.title} severity={AppNotificationSeverity.Error}> <Alert title={ldapError.title} severity={AppNotificationSeverity.Error}>
{ldapError.body} {ldapError.body}
@@ -109,23 +109,24 @@ export class LdapPage extends PureComponent<Props, State> {
{featureEnabled('ldapsync') && ldapSyncInfo && <LdapSyncInfo ldapSyncInfo={ldapSyncInfo} />} {featureEnabled('ldapsync') && ldapSyncInfo && <LdapSyncInfo ldapSyncInfo={ldapSyncInfo} />}
{canReadLDAPUser && ( {canReadLDAPUser && (
<> <section>
<h3>Test user mapping</h3> <h3>Test user mapping</h3>
<Form onSubmit={(data: FormModel) => this.search(data.username)}> <Form onSubmit={(data: FormModel) => this.search(data.username)}>
{({ register }) => ( {({ register }) => (
<HorizontalGroup>
<Field label="Username"> <Field label="Username">
<Input <Input
{...register('username', { required: true })} {...register('username', { required: true })}
width={34}
id="username" id="username"
type="text" type="text"
defaultValue={queryParams.username} defaultValue={queryParams.username}
/> addonAfter={
</Field>
<Button variant="primary" type="submit"> <Button variant="primary" type="submit">
Run Run
</Button> </Button>
</HorizontalGroup> }
/>
</Field>
)} )}
</Form> </Form>
{userError && userError.title && ( {userError && userError.title && (
@@ -137,10 +138,10 @@ export class LdapPage extends PureComponent<Props, State> {
{userError.body} {userError.body}
</Alert> </Alert>
)} )}
{ldapUser && <LdapUserInfo ldapUser={ldapUser} showAttributeMapping={true} />} {ldapUser && <LdapUserInfo ldapUser={ldapUser} />}
</> </section>
)} )}
</> </Stack>
</Page.Contents> </Page.Contents>
</Page> </Page>
); );

View File

@@ -1,63 +1,38 @@
import React, { PureComponent } from 'react'; import React from 'react';
import { dateTimeFormat } from '@grafana/data'; import { dateTimeFormat } from '@grafana/data';
import { Button, Spinner } from '@grafana/ui'; import { InteractiveTable, Text } from '@grafana/ui';
import { SyncInfo } from 'app/types'; import { SyncInfo } from 'app/types';
interface Props { interface Props {
ldapSyncInfo: SyncInfo; ldapSyncInfo: SyncInfo;
} }
interface State {
isSyncing: boolean;
}
const format = 'dddd YYYY-MM-DD HH:mm zz'; const format = 'dddd YYYY-MM-DD HH:mm zz';
export class LdapSyncInfo extends PureComponent<Props, State> { export const LdapSyncInfo = ({ ldapSyncInfo }: Props) => {
state = {
isSyncing: false,
};
handleSyncClick = () => {
this.setState({ isSyncing: !this.state.isSyncing });
};
render() {
const { ldapSyncInfo } = this.props;
const { isSyncing } = this.state;
const nextSyncTime = dateTimeFormat(ldapSyncInfo.nextSync, { format }); const nextSyncTime = dateTimeFormat(ldapSyncInfo.nextSync, { format });
const columns = [{ id: 'syncAttribute' }, { id: 'syncValue' }];
const data = [
{
syncAttribute: 'Active synchronization',
syncValue: ldapSyncInfo.enabled ? 'Enabled' : 'Disabled',
},
{
syncAttribute: 'Scheduled',
syncValue: ldapSyncInfo.schedule,
},
{
syncAttribute: 'Next synchronization',
syncValue: nextSyncTime,
},
];
return ( return (
<> <section>
<h3 className="page-heading"> <Text element="h3">LDAP Synchronization</Text>
LDAP Synchronisation <InteractiveTable data={data} columns={columns} getRowId={(sync) => sync.syncAttribute} />
<Button className="pull-right" onClick={this.handleSyncClick} hidden> </section>
<span className="btn-title">Bulk-sync now</span>
{isSyncing && <Spinner inline={true} />}
</Button>
</h3>
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<tbody>
<tr>
<td>Active synchronisation</td>
<td colSpan={2}>{ldapSyncInfo.enabled ? 'Enabled' : 'Disabled'}</td>
</tr>
<tr>
<td>Scheduled</td>
<td>{ldapSyncInfo.schedule}</td>
</tr>
<tr>
<td>Next scheduled synchronisation</td>
<td>{nextSyncTime}</td>
</tr>
</tbody>
</table>
</div>
</div>
</>
); );
} };
}

View File

@@ -1,54 +1,52 @@
import React from 'react'; import React, { useMemo } from 'react';
import { Tooltip, Icon } from '@grafana/ui'; import { Tooltip, Icon, InteractiveTable, type CellProps, Column } from '@grafana/ui';
import { LdapRole } from 'app/types'; import { LdapRole } from 'app/types';
interface Props { interface Props {
groups: LdapRole[]; groups: LdapRole[];
showAttributeMapping?: boolean;
} }
export const LdapUserGroups = ({ groups, showAttributeMapping }: Props) => { export const LdapUserGroups = ({ groups }: Props) => {
const items = showAttributeMapping ? groups : groups.filter((item) => item.orgRole); const items = useMemo(() => groups, [groups]);
const columns = useMemo<Array<Column<LdapRole>>>(
() => [
{
id: 'groupDN',
header: 'LDAP Group',
},
{
id: 'orgName',
header: 'Organization',
cell: (props: CellProps<LdapRole, string | undefined>) =>
props.value && props.row.original.orgRole ? props.value : '',
},
{
id: 'orgRole',
header: 'Role',
cell: (props: CellProps<LdapRole, string | undefined>) =>
props.value || (
<>
No match{' '}
<Tooltip content="No matching organizations found">
<Icon name="info-circle" />
</Tooltip>
</>
),
},
],
[]
);
return ( return (
<div className="gf-form-group"> <InteractiveTable
<div className="gf-form"> headerTooltips={{
<table className="filter-table form-inline"> orgName: { content: 'Only the first match for an Organization will be used', iconName: 'info-circle' },
<thead> }}
<tr> columns={columns}
{showAttributeMapping && <th>LDAP Group</th>} data={items}
<th> getRowId={(row) => row.orgId + row.orgRole}
Organization />
<Tooltip placement="top" content="Only the first match for an Organization will be used" theme={'info'}>
<Icon name="info-circle" />
</Tooltip>
</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{items.map((group, index) => {
return (
<tr key={`${group.orgId}-${index}`}>
{showAttributeMapping && <td>{group.groupDN}</td>}
{group.orgName && group.orgRole ? <td>{group.orgName}</td> : <td />}
{group.orgRole ? (
<td>{group.orgRole}</td>
) : (
<td>
<span className="text-warning">No match</span>
<Tooltip placement="top" content="No matching groups found" theme={'info'}>
<Icon name="info-circle" />
</Tooltip>
</td>
)}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
); );
}; };

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Box, Stack, Text } from '@grafana/ui';
import { LdapUser } from 'app/types'; import { LdapUser } from 'app/types';
import { LdapUserGroups } from './LdapUserGroups'; import { LdapUserGroups } from './LdapUserGroups';
@@ -9,33 +10,22 @@ import { LdapUserTeams } from './LdapUserTeams';
interface Props { interface Props {
ldapUser: LdapUser; ldapUser: LdapUser;
showAttributeMapping?: boolean;
} }
export const LdapUserInfo = ({ ldapUser, showAttributeMapping }: Props) => { export const LdapUserInfo = ({ ldapUser }: Props) => {
return ( return (
<> <Stack direction="column" gap={4}>
<LdapUserMappingInfo info={ldapUser.info} showAttributeMapping={showAttributeMapping} /> <LdapUserMappingInfo info={ldapUser.info} />
<LdapUserPermissions permissions={ldapUser.permissions} /> <LdapUserPermissions permissions={ldapUser.permissions} />
{ldapUser.roles && ldapUser.roles.length > 0 && ( {ldapUser.roles && ldapUser.roles.length > 0 && <LdapUserGroups groups={ldapUser.roles} />}
<LdapUserGroups groups={ldapUser.roles} showAttributeMapping={showAttributeMapping} />
)}
{ldapUser.teams && ldapUser.teams.length > 0 ? ( {ldapUser.teams && ldapUser.teams.length > 0 ? (
<LdapUserTeams teams={ldapUser.teams} showAttributeMapping={showAttributeMapping} /> <LdapUserTeams teams={ldapUser.teams} />
) : ( ) : (
<div className="gf-form-group"> <Box>
<div className="gf-form"> <Text>No teams found via LDAP</Text>
<table className="filter-table form-inline"> </Box>
<tbody>
<tr>
<td>No teams found via LDAP</td>
</tr>
</tbody>
</table>
</div>
</div>
)} )}
</> </Stack>
); );
}; };

View File

@@ -1,47 +1,56 @@
import React from 'react'; import React, { useMemo } from 'react';
import { InteractiveTable } from '@grafana/ui';
import { LdapUserInfo } from 'app/types'; import { LdapUserInfo } from 'app/types';
interface Props { interface Props {
info: LdapUserInfo; info: LdapUserInfo;
showAttributeMapping?: boolean;
} }
export const LdapUserMappingInfo = ({ info, showAttributeMapping }: Props) => { export const LdapUserMappingInfo = ({ info }: Props) => {
return ( const columns = useMemo(
<div className="gf-form-group"> () => [
<div className="gf-form"> {
<table className="filter-table form-inline"> id: 'userInfo',
<thead> header: 'User Information',
<tr> disableGrow: true,
<th colSpan={2}>User information</th> },
{showAttributeMapping && <th>LDAP attribute</th>} {
</tr> id: 'ldapValue',
</thead> },
<tbody> {
<tr> id: 'cfgAttrValue',
<td className="width-16">First name</td> header: 'LDAP attribute',
<td>{info.name.ldapValue}</td> },
{showAttributeMapping && <td>{info.name.cfgAttrValue}</td>} ],
</tr> []
<tr>
<td className="width-16">Surname</td>
<td>{info.surname.ldapValue}</td>
{showAttributeMapping && <td>{info.surname.cfgAttrValue}</td>}
</tr>
<tr>
<td className="width-16">Username</td>
<td>{info.login.ldapValue}</td>
{showAttributeMapping && <td>{info.login.cfgAttrValue}</td>}
</tr>
<tr>
<td className="width-16">Email</td>
<td>{info.email.ldapValue}</td>
{showAttributeMapping && <td>{info.email.cfgAttrValue}</td>}
</tr>
</tbody>
</table>
</div>
</div>
); );
const rows = useMemo(
() => [
{
userInfo: 'First name',
ldapValue: info.name.ldapValue,
cfgAttrValue: info.name.cfgAttrValue,
},
{
userInfo: 'Surname',
ldapValue: info.surname.ldapValue,
cfgAttrValue: info.surname.cfgAttrValue,
},
{
userInfo: 'Username',
ldapValue: info.login.ldapValue,
cfgAttrValue: info.login.cfgAttrValue,
},
{
userInfo: 'Email',
ldapValue: info.email.ldapValue,
cfgAttrValue: info.email.cfgAttrValue,
},
],
[info]
);
return <InteractiveTable columns={columns} data={rows} getRowId={(row) => row.userInfo} />;
}; };

View File

@@ -1,39 +1,47 @@
import React from 'react'; import React, { useMemo } from 'react';
import { Icon } from '@grafana/ui'; import { Column, Icon, InteractiveTable } from '@grafana/ui';
import { LdapPermissions } from 'app/types'; import { LdapPermissions } from 'app/types';
interface Props { interface Props {
permissions: LdapPermissions; permissions: LdapPermissions;
} }
interface TableRow {
permission: string;
value: React.ReactNode;
}
export const LdapUserPermissions = ({ permissions }: Props) => { export const LdapUserPermissions = ({ permissions }: Props) => {
return ( const columns = useMemo<Array<Column<TableRow>>>(
<div className="gf-form-group"> () => [
<div className="gf-form"> {
<table className="filter-table form-inline"> id: 'permission',
<thead> header: 'Permissions',
<tr> disableGrow: true,
<th colSpan={1}>Permissions</th> },
</tr> {
</thead> id: 'value',
<tbody> },
<tr> ],
<td className="width-16"> Grafana admin</td> []
<td> );
{permissions.isGrafanaAdmin ? (
const data = useMemo<TableRow[]>(
() => [
{
permission: 'Grafana admin',
value: permissions.isGrafanaAdmin ? (
<> <>
<Icon name="shield" /> Yes <Icon name="shield" /> Yes
</> </>
) : ( ) : (
'No' 'No'
)} ),
</td> },
</tr> {
<tr> permission: 'Status',
<td className="width-16">Status</td> value: permissions.isDisabled ? (
<td>
{permissions.isDisabled ? (
<> <>
<Icon name="times" /> Inactive <Icon name="times" /> Inactive
</> </>
@@ -41,12 +49,11 @@ export const LdapUserPermissions = ({ permissions }: Props) => {
<> <>
<Icon name="check" /> Active <Icon name="check" /> Active
</> </>
)} ),
</td> },
</tr> ],
</tbody> [permissions]
</table>
</div>
</div>
); );
return <InteractiveTable data={data} columns={columns} getRowId={(row) => row.permission} />;
}; };

View File

@@ -1,59 +1,40 @@
import React from 'react'; import React, { useMemo } from 'react';
import { Tooltip, Icon } from '@grafana/ui'; import { Column, InteractiveTable, CellProps } from '@grafana/ui';
import { LdapTeam } from 'app/types'; import { LdapTeam } from 'app/types';
interface Props { interface Props {
teams: LdapTeam[]; teams: LdapTeam[];
showAttributeMapping?: boolean;
} }
export const LdapUserTeams = ({ teams, showAttributeMapping }: Props) => { export const LdapUserTeams = ({ teams }: Props) => {
const items = showAttributeMapping ? teams : teams.filter((item) => item.teamName); const columns = useMemo<Array<Column<LdapTeam>>>(
() => [
{
id: 'groupDN',
header: 'LDAP Group',
},
{
id: 'orgName',
header: 'Organization',
cell: ({
row: {
original: { orgName },
},
}: CellProps<LdapTeam, void>) => <>{orgName || 'No matching teams found'}</>,
},
{
id: 'teamName',
header: 'Team',
cell: ({
row: {
original: { teamName, orgName },
},
}: CellProps<LdapTeam, void>) => (teamName && orgName ? teamName : ''),
},
],
[]
);
return ( return <InteractiveTable data={teams} columns={columns} getRowId={(row) => row.teamName} />;
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<thead>
<tr>
{showAttributeMapping && <th>LDAP Group</th>}
<th>Organisation</th>
<th>Team</th>
</tr>
</thead>
<tbody>
{items.map((team, index) => {
return (
<tr key={`${team.teamName}-${index}`}>
{showAttributeMapping && (
<>
<td>{team.groupDN}</td>
{!team.orgName && (
<>
<td />
<td>
<span className="text-warning">No match</span>
<Tooltip placement="top" content="No matching teams found" theme={'info'}>
<Icon name="info-circle" />
</Tooltip>
</td>
</>
)}
</>
)}
{team.orgName && (
<>
<td>{team.orgName}</td>
<td>{team.teamName}</td>
</>
)}
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}; };