Skip to content

Commit

Permalink
Added support for links to tech-insight checks
Browse files Browse the repository at this point in the history
This includes UI components to render a popup menu with these links, by default on the result icon in the scorecards view.

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
  • Loading branch information
grantila committed Sep 10, 2024
1 parent 3d6411c commit 4a164ed
Show file tree
Hide file tree
Showing 19 changed files with 444 additions and 15 deletions.
5 changes: 5 additions & 0 deletions workspaces/tech-insights/.changeset/tall-garlics-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage-community/plugin-tech-insights': patch
---

Added support for links for checks. Static links are defined in the backend for each check. Dynamic links (based on the entity, e.g. to go to github repos, sonarqube projects, etc) are defined with functions in the frontend, when registering the tech-insights API. Two new components are added, TechInsightsCheckIcon and TechInsightsLinksMenu. The former to wrap a result icon with a popup menu with links, the second is the component to show the popup with links (which can be arbitrarily componsed in other UI views).
7 changes: 7 additions & 0 deletions workspaces/tech-insights/.changeset/unlucky-olives-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@backstage-community/plugin-tech-insights-backend-module-jsonfc': patch
'@backstage-community/plugin-tech-insights-common': patch
'@backstage-community/plugin-tech-insights-node': patch
---

Added links property for checks, to allow the UI to render links for users to click and get more information about individual checks, what they mean, how to adhere to them, etc.
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ export class JsonRulesEngineFactChecker
? techInsightCheck.successMetadata
: techInsightCheck.failureMetadata,
rule: { conditions: {} },
links: techInsightCheck.links,
};

if ('toJSON' in result) {
Expand Down
16 changes: 16 additions & 0 deletions workspaces/tech-insights/plugins/tech-insights-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
import { DateTime } from 'luxon';
import { JsonValue } from '@backstage/types';

/**
* @public
*
* Response type for check links.
*/
export type CheckLink = {
title: string;
url: string;
};

/**
* @public
*
Expand Down Expand Up @@ -54,6 +64,12 @@ export interface CheckResponse {
* Currently loosely typed, but in the future when patterns emerge, key shapes can be defined
*/
metadata?: Record<string, any>;

/**
* An array of links to display for the check, for users to be able to read
* more about the check.
*/
links?: CheckLink[];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
* limitations under the License.
*/
import { TechInsightsStore } from './persistence';
import { CheckResult } from '@backstage-community/plugin-tech-insights-common';
import {
CheckLink,
CheckResult,
} from '@backstage-community/plugin-tech-insights-common';

/**
* A factory wrapper to construct FactChecker implementations.
Expand Down Expand Up @@ -138,6 +141,12 @@ export interface TechInsightCheck {
* Can contain links, description texts or other actionable items
*/
failureMetadata?: Record<string, any>;

/**
* An array of links to display for the check, for users to be able to read
* more about the check.
*/
links?: CheckLink[];
}

/**
Expand Down
54 changes: 54 additions & 0 deletions workspaces/tech-insights/plugins/tech-insights/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,57 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
...
);
```

## Custom views rendering tech-insights results

If you create a custom view which renders tech-insights results, you can use the `TechInsightsCheckIcon`, which also (by default) is clickable and opens a popup menu with links for this particular check and entity.

You can also render the icon using the tech-insights renderer or pass the `disableLinksMenu` prop to `TechInsightsCheckIcon` to disable the menu, and render it elsewhere, by importing `TechInsightsLinksMenu`.

### Render the check icon with a popup menu for links

```tsx
import { TechInsightsCheckIcon } from '@backstage-community/plugin-tech-insights';

export const MyComponent = () => {
const entity = getEntitySomehow();
const result = getCheckResultSomehow();

return <TechInsightsCheckIcon result={result} entity={entity} />;
};
```

### Render the popup menu for links

You can render a custom component (like a button) which opens the popup menu with links.

The menu will be anchored to an element, likely the button being pressed, or icon being clicked. The `setMenu` prop is used to get a function to open the menu.

```tsx
import {
TechInsightsLinksMenu,
ResultLinksMenuInfo,
} from '@backstage-community/plugin-tech-insights';

export const MyComponent = () => {
const entity = getEntitySomehow();
const result = getCheckResultSomehow();

const [menu, setMenu] = useState<ResultLinksMenuInfo | undefined>();

return (
<>
<Button
title="Show links"
disabled={!menu}
onClick={event => menu?.open(event.currentTarget)}
/>
<TechInsightsLinksMenu
result={result}
entity={entity}
setMenu={setMenu}
/>
</>
);
};
```
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ createDevApp()
bulkCheckResponse,
getFacts: async (_: CompoundEntityRef, __: string[]) => '' as any,
getFactSchemas: async () => [],
getLinksForEntity: () => [],
} as TechInsightsApi),
})
.addPage({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import {
CheckResult,
BulkCheckResponse,
FactSchema,
CheckLink,
} from '@backstage-community/plugin-tech-insights-common';
import { Check, InsightFacts } from './types';
import { CheckResultRenderer } from '../components/CheckResultRenderer';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';

/**
* {@link @backstage/core-plugin-api#ApiRef} for the {@link TechInsightsApi}
Expand Down Expand Up @@ -52,4 +53,9 @@ export interface TechInsightsApi {
): Promise<BulkCheckResponse>;
getFacts(entity: CompoundEntityRef, facts: string[]): Promise<InsightFacts>;
getFactSchemas(): Promise<FactSchema[]>;
getLinksForEntity(
result: CheckResult,
entity: Entity,
options?: { includeStaticLinks?: boolean },
): CheckLink[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,37 @@
import { TechInsightsApi } from './TechInsightsApi';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { TechInsightsClient as TechInsightsClientBase } from '@backstage-community/plugin-tech-insights-common/client';
import { CheckResult } from '@backstage-community/plugin-tech-insights-common';
import {
CheckLink,
CheckResult,
} from '@backstage-community/plugin-tech-insights-common';

import {
CheckResultRenderer,
jsonRulesEngineCheckResultRenderer,
} from '../components/CheckResultRenderer';
import { Entity } from '@backstage/catalog-model';

/** @public */
export class TechInsightsClient
extends TechInsightsClientBase
implements TechInsightsApi
{
private readonly renderers?: CheckResultRenderer[];
private readonly customGetEntityLinks: (
result: CheckResult,
entity: Entity,
) => CheckLink[];

constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
renderers?: CheckResultRenderer[];
getEntityLinks?: (result: CheckResult, entity: Entity) => CheckLink[];
}) {
super(options);
this.renderers = options.renderers;
this.customGetEntityLinks = options.getEntityLinks ?? (() => []);
}

getCheckResultRenderers(types: string[]): CheckResultRenderer[] {
Expand All @@ -54,4 +64,16 @@ export class TechInsightsClient
}
return true;
}

getLinksForEntity(
result: CheckResult,
entity: Entity,
options: { includeStaticLinks?: boolean } = {},
): CheckLink[] {
const links = this.customGetEntityLinks(result, entity);
if (options.includeStaticLinks) {
links.push(...(result.check.links ?? []));
}
return links;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2024 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 React, {
ComponentProps,
ComponentType,
MouseEventHandler,
ReactNode,
useState,
} from 'react';

import { useApi } from '@backstage/core-plugin-api';
import { CheckResult } from '@backstage-community/plugin-tech-insights-common';
import { Entity } from '@backstage/catalog-model';

import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import IconButton from '@material-ui/core/IconButton';
import Alert from '@material-ui/lab/Alert';

import { techInsightsApiRef } from '../../api';
import { CheckResultRenderer } from '../CheckResultRenderer';
import { ResultLinksMenu, ResultLinksMenuInfo } from '../ResultLinksMenu';

type BaseComponent = ComponentType<{ onClick?: MouseEventHandler | undefined }>;

/**
* ResultCheckIcon props
*
* The only necessary prop is {@link result}, but if {@link entity} is provided,
* the popup menu with links will also include links specifically for this
* entity.
*
* {@link disableLinksMenu} will disable the popup menu.
*
* {@link checkResultRenderer} can optionally be provided, with a small
* performance improvement, if it is already cashed upstream.
*
* {@link component} and {@link componentProps} can be specified to override the
* default `ListItemSecondaryAction` component, e.g. with a `div` (and
* optionally its props).
*
* {@link missingRendererComponent} can be overridden with a component to
* display when no icon renderer was found for this result.
*
* @public
*/
export interface ResultCheckIconProps<C extends BaseComponent> {
result: CheckResult;
entity?: Entity;
checkResultRenderer?: CheckResultRenderer;
disableLinksMenu?: boolean;
component?: C;
componentProps?: Omit<ComponentProps<C>, 'onClick'>;
missingRendererComponent?: ReactNode;
}

export const ResultCheckIcon = <
C extends BaseComponent = typeof ListItemSecondaryAction,
>(
props: ResultCheckIconProps<C>,
) => {
const {
result,
entity,
disableLinksMenu,
componentProps,
missingRendererComponent = <Alert severity="error">Unknown type.</Alert>,
} = props;

const Component = props.component ?? ListItemSecondaryAction;

const api = useApi(techInsightsApiRef);

const checkResultRenderer =
props.checkResultRenderer ??
api.getCheckResultRenderers([result.check.type])[0];

const [menu, setMenu] = useState<ResultLinksMenuInfo | undefined>();

const iconComponent = checkResultRenderer?.component(result);

const wrapActions = (component: React.ReactElement): ReactNode => {
if (!menu) {
return component;
}
return (
<Component
{...componentProps}
onClick={event => menu?.open(event.currentTarget)}
>
<IconButton edge="end" aria-label="comments">
{component}
</IconButton>
</Component>
);
};

return (
<>
{!disableLinksMenu && (
<ResultLinksMenu result={result} entity={entity} setMenu={setMenu} />
)}
{wrapActions(iconComponent ?? missingRendererComponent)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2024 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 type { ResultCheckIconProps } from './ResultCheckIcon';
export { ResultCheckIcon } from './ResultCheckIcon';
Loading

0 comments on commit 4a164ed

Please sign in to comment.