mirror of https://github.com/grafana/grafana
ServiceAccounts: Add token view for Service Accounts (#45013)
* fix SA creation scope * add writer action to SA fixed role * ServiceAccounts: Add token table to SA detail page * ServiceAccounts: Allow deletion of tokens from token table * refactor service account page * avoid using store for deletepull/44973/head
parent
f885c2ede9
commit
8c49e96439
@ -1,67 +1,89 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import React, { useEffect } from 'react'; |
||||
import { connect, ConnectedProps } from 'react-redux'; |
||||
import { NavModel } from '@grafana/data'; |
||||
import { getNavModel } from 'app/core/selectors/navModel'; |
||||
import Page from 'app/core/components/Page/Page'; |
||||
import { ServiceAccountProfile } from './ServiceAccountProfile'; |
||||
import { StoreState, ServiceAccountDTO } from 'app/types'; |
||||
import { StoreState, ServiceAccountDTO, ApiKey } from 'app/types'; |
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; |
||||
import { loadServiceAccount } from './state/actions'; |
||||
import { deleteServiceAccountToken, loadServiceAccount, loadServiceAccountTokens } from './state/actions'; |
||||
import { ServiceAccountTokensTable } from './ServiceAccountTokensTable'; |
||||
import { getTimeZone, NavModel } from '@grafana/data'; |
||||
|
||||
interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { |
||||
navModel: NavModel; |
||||
serviceAccount?: ServiceAccountDTO; |
||||
tokens: ApiKey[]; |
||||
isLoading: boolean; |
||||
} |
||||
|
||||
export class ServiceAccountPage extends PureComponent<Props> { |
||||
async componentDidMount() { |
||||
const { match } = this.props; |
||||
this.props.loadServiceAccount(parseInt(match.params.id, 10)); |
||||
} |
||||
|
||||
render() { |
||||
const { navModel, serviceAccount, isLoading } = this.props; |
||||
|
||||
return ( |
||||
<Page navModel={navModel}> |
||||
<Page.Contents isLoading={isLoading}> |
||||
{serviceAccount && ( |
||||
<> |
||||
<ServiceAccountProfile |
||||
serviceaccount={serviceAccount} |
||||
onServiceAccountDelete={() => { |
||||
console.log(`not implemented`); |
||||
}} |
||||
onServiceAccountUpdate={() => { |
||||
console.log(`not implemented`); |
||||
}} |
||||
onServiceAccountDisable={() => { |
||||
console.log(`not implemented`); |
||||
}} |
||||
onServiceAccountEnable={() => { |
||||
console.log(`not implemented`); |
||||
}} |
||||
/> |
||||
</> |
||||
)} |
||||
</Page.Contents> |
||||
</Page> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function mapStateToProps(state: StoreState) { |
||||
return { |
||||
navModel: getNavModel(state.navIndex, 'serviceaccounts'), |
||||
serviceAccount: state.serviceAccountProfile.serviceAccount, |
||||
tokens: state.serviceAccountProfile.tokens, |
||||
isLoading: state.serviceAccountProfile.isLoading, |
||||
timezone: getTimeZone(state.user), |
||||
}; |
||||
} |
||||
const mapDispatchToProps = { |
||||
loadServiceAccount, |
||||
loadServiceAccountTokens, |
||||
deleteServiceAccountToken, |
||||
}; |
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps); |
||||
type Props = OwnProps & ConnectedProps<typeof connector>; |
||||
export default connector(ServiceAccountPage); |
||||
|
||||
const ServiceAccountPageUnconnected = ({ |
||||
navModel, |
||||
match, |
||||
serviceAccount, |
||||
tokens, |
||||
timezone, |
||||
isLoading, |
||||
loadServiceAccount, |
||||
loadServiceAccountTokens, |
||||
deleteServiceAccountToken, |
||||
}: Props) => { |
||||
useEffect(() => { |
||||
const serviceAccountId = parseInt(match.params.id, 10); |
||||
loadServiceAccount(serviceAccountId); |
||||
loadServiceAccountTokens(serviceAccountId); |
||||
}, [match, loadServiceAccount, loadServiceAccountTokens]); |
||||
|
||||
const onDeleteServiceAccountToken = (key: ApiKey) => { |
||||
deleteServiceAccountToken(parseInt(match.params.id, 10), key.id!); |
||||
}; |
||||
|
||||
return ( |
||||
<Page navModel={navModel}> |
||||
<Page.Contents isLoading={isLoading}> |
||||
{serviceAccount && ( |
||||
<> |
||||
<ServiceAccountProfile |
||||
serviceaccount={serviceAccount} |
||||
onServiceAccountDelete={() => { |
||||
console.log(`not implemented`); |
||||
}} |
||||
onServiceAccountUpdate={() => { |
||||
console.log(`not implemented`); |
||||
}} |
||||
onServiceAccountDisable={() => { |
||||
console.log(`not implemented`); |
||||
}} |
||||
onServiceAccountEnable={() => { |
||||
console.log(`not implemented`); |
||||
}} |
||||
/> |
||||
</> |
||||
)} |
||||
<h3 className="page-heading">Tokens</h3> |
||||
{tokens && ( |
||||
<ServiceAccountTokensTable tokens={tokens} timeZone={timezone} onDelete={onDeleteServiceAccountToken} /> |
||||
)} |
||||
</Page.Contents> |
||||
</Page> |
||||
); |
||||
}; |
||||
|
||||
export const ServiceAccountPage = connector(ServiceAccountPageUnconnected); |
||||
|
@ -0,0 +1,70 @@ |
||||
import React, { FC } from 'react'; |
||||
import { DeleteButton, Icon, Tooltip, useTheme2 } from '@grafana/ui'; |
||||
import { dateTimeFormat, GrafanaTheme2, TimeZone } from '@grafana/data'; |
||||
|
||||
import { ApiKey } from '../../types'; |
||||
import { css } from '@emotion/css'; |
||||
|
||||
interface Props { |
||||
tokens: ApiKey[]; |
||||
timeZone: TimeZone; |
||||
onDelete: (token: ApiKey) => void; |
||||
} |
||||
|
||||
export const ServiceAccountTokensTable: FC<Props> = ({ tokens, timeZone, onDelete }) => { |
||||
const theme = useTheme2(); |
||||
const styles = getStyles(theme); |
||||
|
||||
return ( |
||||
<> |
||||
<table className="filter-table"> |
||||
<thead> |
||||
<tr> |
||||
<th>Name</th> |
||||
<th>Expires</th> |
||||
<th style={{ width: '34px' }} /> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{tokens.map((key) => { |
||||
const isExpired = !!(key.expiration && Date.now() > new Date(key.expiration).getTime()); |
||||
return ( |
||||
<tr key={key.id} className={styles.tableRow(isExpired)}> |
||||
<td>{key.name}</td> |
||||
<td> |
||||
{formatDate(timeZone, key.expiration)} |
||||
{isExpired && ( |
||||
<span className={styles.tooltipContainer}> |
||||
<Tooltip content="This API key has expired."> |
||||
<Icon name="exclamation-triangle" /> |
||||
</Tooltip> |
||||
</span> |
||||
)} |
||||
</td> |
||||
<td> |
||||
<DeleteButton aria-label="Delete API key" size="sm" onConfirm={() => onDelete(key)} /> |
||||
</td> |
||||
</tr> |
||||
); |
||||
})} |
||||
</tbody> |
||||
</table> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
function formatDate(timeZone: TimeZone, expiration?: string): string { |
||||
if (!expiration) { |
||||
return 'No expiration date'; |
||||
} |
||||
return dateTimeFormat(expiration, { timeZone }); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
tableRow: (isExpired: boolean) => css` |
||||
color: ${isExpired ? theme.colors.text.secondary : theme.colors.text.primary}; |
||||
`,
|
||||
tooltipContainer: css` |
||||
margin-left: ${theme.spacing(1)}; |
||||
`,
|
||||
}); |
Loading…
Reference in new issue