@@ -1,7 +1,7 @@
|
||||
import { Fragment } from 'react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import { Text } from '@grafana/ui';
|
||||
import { ScrollContainer, Text } from '@grafana/ui';
|
||||
import { SkeletonComponent, attachSkeleton } from '@grafana/ui/src/unstable';
|
||||
|
||||
import { Settings } from './AdminSettings';
|
||||
@@ -12,28 +12,30 @@ interface Props {
|
||||
|
||||
const AdminSettingsTableComponent = ({ settings }: Props) => {
|
||||
return (
|
||||
<table className="filter-table">
|
||||
<tbody>
|
||||
{Object.entries(settings).map(([sectionName, sectionSettings], i) => (
|
||||
<Fragment key={`section-${i}`}>
|
||||
<tr>
|
||||
<td>
|
||||
<Text color="info" weight="bold">
|
||||
{sectionName}
|
||||
</Text>
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
{Object.entries(sectionSettings).map(([settingName, settingValue], j) => (
|
||||
<tr key={`property-${j}`}>
|
||||
<td style={{ paddingLeft: '25px' }}>{settingName}</td>
|
||||
<td style={{ whiteSpace: 'break-spaces' }}>{settingValue}</td>
|
||||
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
|
||||
<table className="filter-table">
|
||||
<tbody>
|
||||
{Object.entries(settings).map(([sectionName, sectionSettings], i) => (
|
||||
<Fragment key={`section-${i}`}>
|
||||
<tr>
|
||||
<td>
|
||||
<Text color="info" weight="bold">
|
||||
{sectionName}
|
||||
</Text>
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{Object.entries(sectionSettings).map(([settingName, settingValue], j) => (
|
||||
<tr key={`property-${j}`}>
|
||||
<td style={{ paddingLeft: '25px' }}>{settingName}</td>
|
||||
<td style={{ whiteSpace: 'break-spaces' }}>{settingValue}</td>
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</ScrollContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -42,34 +44,36 @@ const randomValues = new Array(50).fill(null).map(() => Math.random());
|
||||
|
||||
const AdminSettingsTableSkeleton: SkeletonComponent = ({ rootProps }) => {
|
||||
return (
|
||||
<table className="filter-table" {...rootProps}>
|
||||
<tbody>
|
||||
{randomValues.map((randomValue, index) => {
|
||||
const isSection = index === 0 || randomValue > 0.9;
|
||||
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
|
||||
<table className="filter-table" {...rootProps}>
|
||||
<tbody>
|
||||
{randomValues.map((randomValue, index) => {
|
||||
const isSection = index === 0 || randomValue > 0.9;
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{isSection && (
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{isSection && (
|
||||
<tr>
|
||||
<td className="admin-settings-section">
|
||||
<Skeleton width={getRandomInRange(40, 80, randomValue)} />
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className="admin-settings-section">
|
||||
<Skeleton width={getRandomInRange(40, 80, randomValue)} />
|
||||
<td style={{ paddingLeft: '25px' }}>
|
||||
<Skeleton width={getRandomInRange(60, 100, randomValue)} />
|
||||
</td>
|
||||
<td>
|
||||
<Skeleton width={getRandomInRange(80, 320, randomValue)} />
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td style={{ paddingLeft: '25px' }}>
|
||||
<Skeleton width={getRandomInRange(60, 100, randomValue)} />
|
||||
</td>
|
||||
<td>
|
||||
<Skeleton width={getRandomInRange(80, 320, randomValue)} />
|
||||
</td>
|
||||
</tr>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</ScrollContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
|
||||
return {
|
||||
container: css({
|
||||
padding: '36px 79px',
|
||||
padding: theme.spacing(4),
|
||||
background: theme.components.panel.background,
|
||||
}),
|
||||
footer: css({
|
||||
@@ -22,7 +22,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
}),
|
||||
header: css({
|
||||
height: '137px',
|
||||
padding: '40px 0 0 79px',
|
||||
padding: theme.spacing(4, 0, 0, 4),
|
||||
position: 'relative',
|
||||
background: `url('${backgroundUrl}') right`,
|
||||
}),
|
||||
@@ -52,7 +52,7 @@ export function LicenseChrome({ header, editionNotice, subheader, children }: Pr
|
||||
background: '#0A1C36',
|
||||
position: 'absolute',
|
||||
top: '19px',
|
||||
left: '71%',
|
||||
right: '5%',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config, GrafanaBootConfig } from '@grafana/runtime';
|
||||
import { LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { LinkButton, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
@@ -43,7 +43,13 @@ export const ServerStats = () => {
|
||||
<Trans i18nKey="admin.server-settings.not-found">No stats found.</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<div className={styles.row}>
|
||||
<Stack
|
||||
gap={2}
|
||||
direction={{
|
||||
xs: 'column',
|
||||
md: 'row',
|
||||
}}
|
||||
>
|
||||
<ServerStatsCard
|
||||
isLoading={isLoading}
|
||||
content={[
|
||||
@@ -59,7 +65,7 @@ export const ServerStats = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className={styles.doubleRow}>
|
||||
<Stack direction="column" gap={2}>
|
||||
<ServerStatsCard
|
||||
isLoading={isLoading}
|
||||
content={[{ name: 'Data sources', value: stats?.datasources }]}
|
||||
@@ -80,7 +86,7 @@ export const ServerStats = () => {
|
||||
</LinkButton>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
<ServerStatsCard
|
||||
isLoading={isLoading}
|
||||
content={[
|
||||
@@ -98,7 +104,7 @@ export const ServerStats = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -133,27 +139,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
title: css({
|
||||
marginBottom: theme.spacing(4),
|
||||
}),
|
||||
row: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
|
||||
'& > div:not(:last-of-type)': {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
|
||||
'& > div': {
|
||||
width: '33.3%',
|
||||
},
|
||||
}),
|
||||
doubleRow: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
'& > div:first-of-type': {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
notFound: css({
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
textAlign: 'center',
|
||||
|
||||
@@ -50,6 +50,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
margin: 0,
|
||||
padding: theme.spacing(2),
|
||||
}),
|
||||
indent: css({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { ConfirmModal, EmptyState, TextLink } from '@grafana/ui';
|
||||
import { ConfirmModal, EmptyState, ScrollContainer, TextLink } from '@grafana/ui';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { getDashboardSnapshotSrv, Snapshot } from 'app/features/dashboard/services/SnapshotSrv';
|
||||
|
||||
@@ -62,7 +62,7 @@ export const SnapshotListTable = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
|
||||
<table className="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -112,6 +112,6 @@ export const SnapshotListTable = () => {
|
||||
setRemoveSnapshot(undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Button, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { Button, LoadingPlaceholder, ScrollContainer } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { UserDTO, UserOrg } from 'app/types';
|
||||
|
||||
@@ -30,46 +30,48 @@ export class UserOrganizations extends PureComponent<Props> {
|
||||
<Trans i18nKey="user-orgs.title">Organizations</Trans>
|
||||
</h3>
|
||||
|
||||
<table className="filter-table form-inline" data-testid={selectors.components.UserProfile.orgsTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Trans i18nKey="user-orgs.name-column">Name</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-orgs.role-column">Role</Trans>
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orgs.map((org: UserOrg, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>{org.name}</td>
|
||||
<td>{org.role}</td>
|
||||
<td className="text-right">
|
||||
{org.orgId === user?.orgId ? (
|
||||
<Button variant="secondary" size="sm" disabled>
|
||||
<Trans i18nKey="user-orgs.current-org-button">Current</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
this.props.setUserOrg(org);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="user-orgs.select-org-button">Select organisation</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
|
||||
<table className="filter-table form-inline" data-testid={selectors.components.UserProfile.orgsTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Trans i18nKey="user-orgs.name-column">Name</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-orgs.role-column">Role</Trans>
|
||||
</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orgs.map((org: UserOrg, index) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>{org.name}</td>
|
||||
<td>{org.role}</td>
|
||||
<td className="text-right">
|
||||
{org.orgId === user?.orgId ? (
|
||||
<Button variant="secondary" size="sm" disabled>
|
||||
<Trans i18nKey="user-orgs.current-org-button">Current</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
this.props.setUserOrg(org);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="user-orgs.select-org-button">Select organisation</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Button, Icon, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { Button, Icon, LoadingPlaceholder, ScrollContainer } from '@grafana/ui';
|
||||
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { formatDate } from 'app/core/internationalization/dates';
|
||||
@@ -28,55 +28,57 @@ class UserSessions extends PureComponent<Props> {
|
||||
{sessions.length > 0 && (
|
||||
<>
|
||||
<h3 className="page-sub-heading">Sessions</h3>
|
||||
<table className="filter-table form-inline" data-testid={selectors.components.UserProfile.sessionsTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.seen-at-column">Last seen</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.created-at-column">Logged on</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.ip-column">IP address</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.browser-column">Browser & OS</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.identity-provider-column">Identity Provider</Trans>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{sessions.map((session: UserSession, index) => (
|
||||
<tr key={index}>
|
||||
{session.isActive ? <td>Now</td> : <td>{session.seenAt}</td>}
|
||||
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{session.clientIp}</td>
|
||||
<td>
|
||||
{session.browser} on {session.os} {session.osVersion}
|
||||
</td>
|
||||
<td>
|
||||
{session.authModule && <TagBadge label={session.authModule} removeIcon={false} count={0} />}
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
tooltip={t('user-session.revoke', 'Revoke user session')}
|
||||
onClick={() => revokeUserSession(session.id)}
|
||||
aria-label={t('user-session.revoke', 'Revoke user session')}
|
||||
>
|
||||
<Icon name="power" />
|
||||
</Button>
|
||||
</td>
|
||||
<ScrollContainer overflowY="visible" overflowX="auto" width="100%">
|
||||
<table className="filter-table form-inline" data-testid={selectors.components.UserProfile.sessionsTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.seen-at-column">Last seen</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.created-at-column">Logged on</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.ip-column">IP address</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.browser-column">Browser & OS</Trans>
|
||||
</th>
|
||||
<th>
|
||||
<Trans i18nKey="user-session.identity-provider-column">Identity Provider</Trans>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{sessions.map((session: UserSession, index) => (
|
||||
<tr key={index}>
|
||||
{session.isActive ? <td>Now</td> : <td>{session.seenAt}</td>}
|
||||
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{session.clientIp}</td>
|
||||
<td>
|
||||
{session.browser} on {session.os} {session.osVersion}
|
||||
</td>
|
||||
<td>
|
||||
{session.authModule && <TagBadge label={session.authModule} removeIcon={false} count={0} />}
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
tooltip={t('user-session.revoke', 'Revoke user session')}
|
||||
onClick={() => revokeUserSession(session.id)}
|
||||
aria-label={t('user-session.revoke', 'Revoke user session')}
|
||||
>
|
||||
<Icon name="power" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</ScrollContainer>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import { css } from '@emotion/css';
|
||||
import pluralize from 'pluralize';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { OrgRole } from '@grafana/data';
|
||||
import { ConfirmModal, FilterInput, LinkButton, RadioButtonGroup, InlineField, EmptyState, Box } from '@grafana/ui';
|
||||
import { GrafanaTheme2, OrgRole } from '@grafana/data';
|
||||
import {
|
||||
ConfirmModal,
|
||||
FilterInput,
|
||||
LinkButton,
|
||||
RadioButtonGroup,
|
||||
InlineField,
|
||||
EmptyState,
|
||||
Box,
|
||||
Stack,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import config from 'app/core/config';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
@@ -78,6 +89,7 @@ export const ServiceAccountsListPageUnconnected = ({
|
||||
const [isDisableModalOpen, setIsDisableModalOpen] = useState(false);
|
||||
const [newToken, setNewToken] = useState('');
|
||||
const [currentServiceAccount, setCurrentServiceAccount] = useState<ServiceAccountDTO | null>(null);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
fetchServiceAccounts({ withLoadingIndicator: true });
|
||||
@@ -192,13 +204,13 @@ export const ServiceAccountsListPageUnconnected = ({
|
||||
}
|
||||
>
|
||||
<Page.Contents>
|
||||
<div className="page-action-bar">
|
||||
<Stack justifyContent="space-between" wrap="wrap">
|
||||
<InlineField grow>
|
||||
<FilterInput
|
||||
className={styles.filterInput}
|
||||
placeholder="Search service account by name"
|
||||
value={query}
|
||||
onChange={onQueryChange}
|
||||
width={50}
|
||||
/>
|
||||
</InlineField>
|
||||
<Box marginBottom={1}>
|
||||
@@ -208,7 +220,7 @@ export const ServiceAccountsListPageUnconnected = ({
|
||||
value={serviceAccountStateFilter}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
</Stack>
|
||||
{!isLoading && !noServiceAccountsCreated && serviceAccounts.length === 0 && (
|
||||
<EmptyState
|
||||
variant="not-found"
|
||||
@@ -291,5 +303,11 @@ export const ServiceAccountsListPageUnconnected = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
filterInput: css({
|
||||
maxWidth: theme.spacing(50),
|
||||
}),
|
||||
});
|
||||
|
||||
const ServiceAccountsListPage = connector(ServiceAccountsListPageUnconnected);
|
||||
export default ServiceAccountsListPage;
|
||||
|
||||
Reference in New Issue
Block a user