feat: add api definition dialog

Signed-off-by: Jasper Herzberg <jasper.herzberg@sda.se>
This commit is contained in:
Jasper Herzberg
2023-01-12 14:11:33 +01:00
parent a4eb191168
commit 095e755d9b
6 changed files with 236 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-api-docs': patch
---
Add ApiDefinitionDialog component for fast access to the API definition in ProvidedApiCards and ConsumedApiCards.
+7
View File
@@ -20,6 +20,13 @@ import { UserListFilterKind } from '@backstage/plugin-catalog-react';
// @public (undocumented)
export const ApiDefinitionCard: () => JSX.Element;
// @public
export function ApiDefinitionDialog(props: {
open: boolean;
entity: ApiEntity;
onClose: () => void;
}): JSX.Element;
// @public (undocumented)
export type ApiDefinitionWidget = {
type: string;
@@ -0,0 +1,176 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ApiEntity } from '@backstage/catalog-model';
import { useApi } from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
makeStyles,
Tab,
Tabs,
Typography,
} from '@material-ui/core';
import React, { useEffect } from 'react';
import { apiDocsConfigRef } from '../../config';
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget';
const useStyles = makeStyles<BackstageTheme>(theme => ({
fullHeightDialog: {
height: 'calc(100% - 64px)',
},
root: {
display: 'flex',
flexGrow: 1,
width: '100%',
height: '100%',
},
tabs: {
borderRight: `1px solid ${theme.palette.divider}`,
flexShrink: 0,
},
tabContents: {
flexGrow: 1,
overflowX: 'auto',
},
title: {
color: theme.palette.text.primary,
wordBreak: 'break-word',
fontSize: theme.typography.h3.fontSize,
marginBottom: 0,
},
type: {
textTransform: 'uppercase',
fontSize: 11,
opacity: 0.8,
marginBottom: theme.spacing(1),
color: theme.palette.text.primary,
},
}));
function TabPanel(props: {
children?: React.ReactNode;
index: number;
value: number;
}) {
const { children, value, index, ...other } = props;
const classes = useStyles();
return (
<div
role="tabpanel"
hidden={value !== index}
id={`vertical-tabpanel-${index}`}
aria-labelledby={`vertical-tab-${index}`}
className={classes.tabContents}
{...other}
>
{value === index && (
<Box pl={3} pr={3}>
{children}
</Box>
)}
</div>
);
}
function a11yProps(index: number) {
return {
id: `vertical-tab-${index}`,
'aria-controls': `vertical-tabpanel-${index}`,
};
}
/**
* A dialog that lets users inspect the API definition.
*
* @public
*/
export function ApiDefinitionDialog(props: {
open: boolean;
entity: ApiEntity;
onClose: () => void;
}) {
const { open, entity, onClose } = props;
const [activeTab, setActiveTab] = React.useState(0);
const classes = useStyles();
useEffect(() => {
setActiveTab(0);
}, [open]);
const config = useApi(apiDocsConfigRef);
const definitionWidget = config.getApiDefinitionWidget(entity);
let tabIndex = 0;
let tabPanelIndex = 0;
return (
<Dialog
fullWidth
maxWidth="xl"
open={open}
onClose={onClose}
aria-labelledby="api-definition-dialog-title"
PaperProps={{ className: classes.fullHeightDialog }}
>
<DialogTitle id="api-definition-dialog-title" disableTypography>
<Typography className={classes.type}>
API - {definitionWidget?.title ?? 'Raw'}
</Typography>
<Typography className={classes.title} variant="h1">
{entity.metadata.title ?? entity.metadata.name}
</Typography>
</DialogTitle>
<DialogContent dividers className={classes.root}>
<Tabs
orientation="vertical"
variant="scrollable"
value={activeTab}
onChange={(_, newValue) => setActiveTab(newValue)}
aria-label="API definition options"
className={classes.tabs}
>
{definitionWidget ? (
<Tab label={definitionWidget.title} {...a11yProps(tabIndex++)} />
) : null}
<Tab label="Raw" {...a11yProps(tabIndex++)} />
</Tabs>
{definitionWidget ? (
<TabPanel value={activeTab} index={tabPanelIndex++}>
{definitionWidget.component(entity.spec.definition)}
</TabPanel>
) : null}
<TabPanel value={activeTab} index={tabPanelIndex++}>
<PlainApiDefinitionWidget
definition={entity.spec.definition}
language={definitionWidget?.rawLanguage ?? entity.spec.type}
/>
</TabPanel>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
);
}
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ApiDefinitionDialog } from './ApiDefinitionDialog';
@@ -15,10 +15,13 @@
*/
import { ApiEntity } from '@backstage/catalog-model';
import { EntityTable } from '@backstage/plugin-catalog-react';
import React from 'react';
import { ApiTypeTitle } from '../ApiDefinitionCard';
import { TableColumn } from '@backstage/core-components';
import { EntityTable } from '@backstage/plugin-catalog-react';
import ExtensionIcon from '@material-ui/icons/Extension';
import { ToggleButton } from '@material-ui/lab';
import React, { useState } from 'react';
import { ApiTypeTitle } from '../ApiDefinitionCard';
import { ApiDefinitionDialog } from '../ApiDefinitionDialog';
export function createSpecApiTypeColumn(): TableColumn<ApiEntity> {
return {
@@ -28,9 +31,29 @@ export function createSpecApiTypeColumn(): TableColumn<ApiEntity> {
};
}
// TODO: This could be moved to plugin-catalog-react if we wouldn't have a
// special createSpecApiTypeColumn. But this is required to use ApiTypeTitle to
// resolve the display name of an entity. Is the display name really worth it?
const ApiDefinitionButton = ({ apiEntity }: { apiEntity: ApiEntity }) => {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<>
<ToggleButton onClick={() => setDialogOpen(!dialogOpen)}>
<ExtensionIcon />
</ToggleButton>
<ApiDefinitionDialog
entity={apiEntity}
open={dialogOpen}
onClose={() => setDialogOpen(false)}
/>
</>
);
};
function createApiDefinitionColumn(): TableColumn<ApiEntity> {
return {
title: 'API Definition',
render: entity => <ApiDefinitionButton apiEntity={entity} />,
};
}
export const apiEntityColumns: TableColumn<ApiEntity>[] = [
EntityTable.columns.createEntityRefColumn({ defaultKind: 'API' }),
@@ -39,4 +62,5 @@ export const apiEntityColumns: TableColumn<ApiEntity>[] = [
createSpecApiTypeColumn(),
EntityTable.columns.createSpecLifecycleColumn(),
EntityTable.columns.createMetadataDescriptionColumn(),
createApiDefinitionColumn(),
];
+1
View File
@@ -16,6 +16,7 @@
export * from './ApiExplorerPage';
export * from './ApiDefinitionCard';
export * from './ApiDefinitionDialog';
export * from './ApisCards';
export * from './AsyncApiDefinitionWidget';
export * from './ComponentsCards';