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) => {
const columns = useMemo<Array<Column<ServerInfo>>>(
() => [
{
id: 'host',
header: 'Host',
disableGrow: true,
},
{
id: 'port',
header: 'Port',
disableGrow: true,
},
{
id: 'available',
cell: (serverInfo: CellProps<ServerInfo>) => {
return serverInfo.cell.value ? (
<Stack justifyContent="end">
<Tooltip content="Connection is available">
<Icon name="check" className="pull-right" />
</Tooltip>
</Stack>
) : (
<Stack justifyContent="end">
<Tooltip content="Connection is not available">
<Icon name="exclamation-triangle" />
</Tooltip>
</Stack>
);
},
},
],
[]
);
const data = useMemo<ServerInfo[]>(() => ldapConnectionInfo, [ldapConnectionInfo]);
return ( return (
<> <section>
<h3 className="page-heading">LDAP Connection</h3> <Stack direction="column" gap={2}>
<div className="gf-form-group"> <Text color="primary" element="h3">
<div className="gf-form"> LDAP Connection
<table className="filter-table form-inline"> </Text>
<thead> <InteractiveTable data={data} columns={columns} getRowId={(serverInfo) => serverInfo.host + serverInfo.port} />
<tr> <LdapErrorBox ldapConnectionInfo={ldapConnectionInfo} />
<th>Host</th> </Stack>
<th colSpan={2}>Port</th> </section>
</tr>
</thead>
<tbody>
{ldapConnectionInfo &&
ldapConnectionInfo.map((serverInfo, index) => (
<tr key={index}>
<td>{serverInfo.host}</td>
<td>{serverInfo.port}</td>
<td>
{serverInfo.available ? (
<Icon name="check" className="pull-right" />
) : (
<Icon name="exclamation-triangle" className="pull-right" />
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="gf-form-group">
<LdapErrorBox ldapConnectionInfo={ldapConnectionInfo} />
</div>
</div>
</>
); );
}; };

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 = { const nextSyncTime = dateTimeFormat(ldapSyncInfo.nextSync, { format });
isSyncing: false,
};
handleSyncClick = () => { const columns = [{ id: 'syncAttribute' }, { id: 'syncValue' }];
this.setState({ isSyncing: !this.state.isSyncing }); const data = [
}; {
syncAttribute: 'Active synchronization',
syncValue: ldapSyncInfo.enabled ? 'Enabled' : 'Disabled',
},
{
syncAttribute: 'Scheduled',
syncValue: ldapSyncInfo.schedule,
},
{
syncAttribute: 'Next synchronization',
syncValue: nextSyncTime,
},
];
render() { return (
const { ldapSyncInfo } = this.props; <section>
const { isSyncing } = this.state; <Text element="h3">LDAP Synchronization</Text>
const nextSyncTime = dateTimeFormat(ldapSyncInfo.nextSync, { format }); <InteractiveTable data={data} columns={columns} getRowId={(sync) => sync.syncAttribute} />
</section>
return ( );
<> };
<h3 className="page-heading">
LDAP Synchronisation
<Button className="pull-right" onClick={this.handleSyncClick} hidden>
<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,52 +1,59 @@
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 ? (
<>
<Icon name="shield" /> Yes
</>
) : (
'No'
)}
</td>
</tr>
<tr>
<td className="width-16">Status</td>
<td>
{permissions.isDisabled ? (
<>
<Icon name="times" /> Inactive
</>
) : (
<>
<Icon name="check" /> Active
</>
)}
</td>
</tr>
</tbody>
</table>
</div>
</div>
); );
const data = useMemo<TableRow[]>(
() => [
{
permission: 'Grafana admin',
value: permissions.isGrafanaAdmin ? (
<>
<Icon name="shield" /> Yes
</>
) : (
'No'
),
},
{
permission: 'Status',
value: permissions.isDisabled ? (
<>
<Icon name="times" /> Inactive
</>
) : (
<>
<Icon name="check" /> Active
</>
),
},
],
[permissions]
);
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>>>(
() => [
return ( {
<div className="gf-form-group"> id: 'groupDN',
<div className="gf-form"> header: 'LDAP Group',
<table className="filter-table form-inline"> },
<thead> {
<tr> id: 'orgName',
{showAttributeMapping && <th>LDAP Group</th>} header: 'Organization',
<th>Organisation</th> cell: ({
<th>Team</th> row: {
</tr> original: { orgName },
</thead> },
<tbody> }: CellProps<LdapTeam, void>) => <>{orgName || 'No matching teams found'}</>,
{items.map((team, index) => { },
return ( {
<tr key={`${team.teamName}-${index}`}> id: 'teamName',
{showAttributeMapping && ( header: 'Team',
<> cell: ({
<td>{team.groupDN}</td> row: {
{!team.orgName && ( original: { teamName, orgName },
<> },
<td /> }: CellProps<LdapTeam, void>) => (teamName && orgName ? teamName : ''),
<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>
); );
return <InteractiveTable data={teams} columns={columns} getRowId={(row) => row.teamName} />;
}; };