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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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"]
],
"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": [
[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';
interface Props {
ldapConnectionInfo: LdapConnectionInfo;
}
interface ServerInfo {
host: string;
port: number;
available: boolean;
}
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 (
<>
<h3 className="page-heading">LDAP Connection</h3>
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<thead>
<tr>
<th>Host</th>
<th colSpan={2}>Port</th>
</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>
</>
<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} />
</Stack>
</section>
);
};

View File

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

View File

@ -1,63 +1,38 @@
import React, { PureComponent } from 'react';
import React from 'react';
import { dateTimeFormat } from '@grafana/data';
import { Button, Spinner } from '@grafana/ui';
import { InteractiveTable, Text } from '@grafana/ui';
import { SyncInfo } from 'app/types';
interface Props {
ldapSyncInfo: SyncInfo;
}
interface State {
isSyncing: boolean;
}
const format = 'dddd YYYY-MM-DD HH:mm zz';
export class LdapSyncInfo extends PureComponent<Props, State> {
state = {
isSyncing: false,
};
export const LdapSyncInfo = ({ ldapSyncInfo }: Props) => {
const nextSyncTime = dateTimeFormat(ldapSyncInfo.nextSync, { format });
handleSyncClick = () => {
this.setState({ isSyncing: !this.state.isSyncing });
};
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,
},
];
render() {
const { ldapSyncInfo } = this.props;
const { isSyncing } = this.state;
const nextSyncTime = dateTimeFormat(ldapSyncInfo.nextSync, { format });
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>
</>
);
}
}
return (
<section>
<Text element="h3">LDAP Synchronization</Text>
<InteractiveTable data={data} columns={columns} getRowId={(sync) => sync.syncAttribute} />
</section>
);
};

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';
interface Props {
groups: LdapRole[];
showAttributeMapping?: boolean;
}
export const LdapUserGroups = ({ groups, showAttributeMapping }: Props) => {
const items = showAttributeMapping ? groups : groups.filter((item) => item.orgRole);
export const LdapUserGroups = ({ groups }: Props) => {
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 (
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<thead>
<tr>
{showAttributeMapping && <th>LDAP Group</th>}
<th>
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>
<InteractiveTable
headerTooltips={{
orgName: { content: 'Only the first match for an Organization will be used', iconName: 'info-circle' },
}}
columns={columns}
data={items}
getRowId={(row) => row.orgId + row.orgRole}
/>
);
};

View File

@ -1,5 +1,6 @@
import React from 'react';
import { Box, Stack, Text } from '@grafana/ui';
import { LdapUser } from 'app/types';
import { LdapUserGroups } from './LdapUserGroups';
@ -9,33 +10,22 @@ import { LdapUserTeams } from './LdapUserTeams';
interface Props {
ldapUser: LdapUser;
showAttributeMapping?: boolean;
}
export const LdapUserInfo = ({ ldapUser, showAttributeMapping }: Props) => {
export const LdapUserInfo = ({ ldapUser }: Props) => {
return (
<>
<LdapUserMappingInfo info={ldapUser.info} showAttributeMapping={showAttributeMapping} />
<Stack direction="column" gap={4}>
<LdapUserMappingInfo info={ldapUser.info} />
<LdapUserPermissions permissions={ldapUser.permissions} />
{ldapUser.roles && ldapUser.roles.length > 0 && (
<LdapUserGroups groups={ldapUser.roles} showAttributeMapping={showAttributeMapping} />
)}
{ldapUser.roles && ldapUser.roles.length > 0 && <LdapUserGroups groups={ldapUser.roles} />}
{ldapUser.teams && ldapUser.teams.length > 0 ? (
<LdapUserTeams teams={ldapUser.teams} showAttributeMapping={showAttributeMapping} />
<LdapUserTeams teams={ldapUser.teams} />
) : (
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<tbody>
<tr>
<td>No teams found via LDAP</td>
</tr>
</tbody>
</table>
</div>
</div>
<Box>
<Text>No teams found via LDAP</Text>
</Box>
)}
</>
</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';
interface Props {
info: LdapUserInfo;
showAttributeMapping?: boolean;
}
export const LdapUserMappingInfo = ({ info, showAttributeMapping }: Props) => {
return (
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<thead>
<tr>
<th colSpan={2}>User information</th>
{showAttributeMapping && <th>LDAP attribute</th>}
</tr>
</thead>
<tbody>
<tr>
<td className="width-16">First name</td>
<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>
export const LdapUserMappingInfo = ({ info }: Props) => {
const columns = useMemo(
() => [
{
id: 'userInfo',
header: 'User Information',
disableGrow: true,
},
{
id: 'ldapValue',
},
{
id: 'cfgAttrValue',
header: 'LDAP attribute',
},
],
[]
);
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';
interface Props {
permissions: LdapPermissions;
}
interface TableRow {
permission: string;
value: React.ReactNode;
}
export const LdapUserPermissions = ({ permissions }: Props) => {
return (
<div className="gf-form-group">
<div className="gf-form">
<table className="filter-table form-inline">
<thead>
<tr>
<th colSpan={1}>Permissions</th>
</tr>
</thead>
<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 columns = useMemo<Array<Column<TableRow>>>(
() => [
{
id: 'permission',
header: 'Permissions',
disableGrow: true,
},
{
id: 'value',
},
],
[]
);
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';
interface Props {
teams: LdapTeam[];
showAttributeMapping?: boolean;
}
export const LdapUserTeams = ({ teams, showAttributeMapping }: Props) => {
const items = showAttributeMapping ? teams : teams.filter((item) => item.teamName);
return (
<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>
export const LdapUserTeams = ({ teams }: Props) => {
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 <InteractiveTable data={teams} columns={columns} getRowId={(row) => row.teamName} />;
};