@@ -417,7 +417,7 @@ export interface EntityRefPresentation {
|
||||
// @public
|
||||
export interface EntityRefPresentationSnapshot {
|
||||
entityRef: string;
|
||||
Icon?: IconComponent | undefined;
|
||||
Icon?: IconComponent | undefined | false;
|
||||
primaryTitle: string;
|
||||
secondaryTitle?: string;
|
||||
}
|
||||
|
||||
@@ -84,8 +84,13 @@ export interface EntityRefPresentationSnapshot {
|
||||
* both in larger formats such as in a header, or in smaller formats such as
|
||||
* inline with regular text, so bear in mind that the legibility should be
|
||||
* high in both cases.
|
||||
*
|
||||
* A value of `false` here indicates the desire to not have an icon present
|
||||
* for the given implementation. A value of `undefined` leaves it at the
|
||||
* discretion of the display layer to choose what to do (such as for example
|
||||
* showing a fallback icon).
|
||||
*/
|
||||
Icon?: IconComponent | undefined;
|
||||
Icon?: IconComponent | undefined | false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -251,6 +251,7 @@ export interface DefaultEntityPresentationApiOptions {
|
||||
batchDelay?: HumanDuration;
|
||||
cacheTtl?: HumanDuration;
|
||||
catalogApi?: CatalogApi;
|
||||
kindIcons?: Record<string, IconComponent>;
|
||||
renderer?: DefaultEntityPresentationApiRenderer;
|
||||
}
|
||||
|
||||
@@ -266,7 +267,7 @@ export interface DefaultEntityPresentationApiRenderer {
|
||||
defaultNamespace?: string;
|
||||
};
|
||||
}) => {
|
||||
snapshot: Omit<EntityRefPresentationSnapshot, 'entityRef' | 'entity'>;
|
||||
snapshot: Omit<EntityRefPresentationSnapshot, 'entityRef'>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,23 +14,28 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
Entity,
|
||||
parseEntityRef,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
CatalogApi,
|
||||
EntityPresentationApi,
|
||||
EntityRefPresentation,
|
||||
EntityRefPresentationSnapshot,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import { HumanDuration, durationToMilliseconds } from '@backstage/types';
|
||||
import DataLoader from 'dataloader';
|
||||
import ExpiryMap from 'expiry-map';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
import {
|
||||
DEFAULT_BATCH_DELAY,
|
||||
DEFAULT_CACHE_TTL,
|
||||
DEFAULT_ICONS,
|
||||
createDefaultRenderer,
|
||||
} from './defaults';
|
||||
import { durationToMs } from './util';
|
||||
|
||||
/**
|
||||
* A custom renderer for the {@link DefaultEntityPresentationApi}.
|
||||
@@ -82,7 +87,7 @@ export interface DefaultEntityPresentationApiRenderer {
|
||||
defaultNamespace?: string;
|
||||
};
|
||||
}) => {
|
||||
snapshot: Omit<EntityRefPresentationSnapshot, 'entityRef' | 'entity'>;
|
||||
snapshot: Omit<EntityRefPresentationSnapshot, 'entityRef'>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,6 +128,19 @@ export interface DefaultEntityPresentationApiOptions {
|
||||
*/
|
||||
batchDelay?: HumanDuration;
|
||||
|
||||
/**
|
||||
* A mapping from kinds to icons.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The keys are kinds (case insensitive) that map to icon values to represent
|
||||
* kinds by.
|
||||
*
|
||||
* If you do not supply a set of icons here, a set of fallback icons will be
|
||||
* used. If you supply the empty object, no fallback icons will be used.
|
||||
*/
|
||||
kindIcons?: Record<string, IconComponent>;
|
||||
|
||||
/**
|
||||
* A custom renderer, if any.
|
||||
*/
|
||||
@@ -167,6 +185,7 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
readonly #cache: Map<string, CacheEntry>;
|
||||
readonly #cacheTtlMs: number;
|
||||
readonly #loader: DataLoader<string, Entity | undefined> | undefined;
|
||||
readonly #kindIcons: Record<string, IconComponent>; // lowercased kinds
|
||||
readonly #renderer: DefaultEntityPresentationApiRenderer;
|
||||
|
||||
private constructor(options: DefaultEntityPresentationApiOptions) {
|
||||
@@ -174,6 +193,13 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
const batchDelay = options.batchDelay ?? DEFAULT_BATCH_DELAY;
|
||||
const renderer = options.renderer ?? createDefaultRenderer({ async: true });
|
||||
|
||||
const kindIcons: Record<string, IconComponent> = {};
|
||||
Object.entries(options.kindIcons ?? DEFAULT_ICONS).forEach(
|
||||
([kind, icon]) => {
|
||||
kindIcons[kind.toLocaleLowerCase('en-US')] = icon;
|
||||
},
|
||||
);
|
||||
|
||||
if (renderer.async) {
|
||||
if (!options.catalogApi) {
|
||||
throw new TypeError(`Asynchronous rendering requires a catalog API`);
|
||||
@@ -186,8 +212,9 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
});
|
||||
}
|
||||
|
||||
this.#cacheTtlMs = durationToMs(cacheTtl);
|
||||
this.#cacheTtlMs = durationToMilliseconds(cacheTtl);
|
||||
this.#cache = new Map();
|
||||
this.#kindIcons = kindIcons;
|
||||
this.#renderer = renderer;
|
||||
}
|
||||
|
||||
@@ -199,25 +226,43 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
defaultNamespace?: string;
|
||||
},
|
||||
): EntityRefPresentation {
|
||||
const { entityRef, entity, needsLoad } =
|
||||
const { entityRef, kind, entity, needsLoad } =
|
||||
this.#getEntityForInitialRender(entityOrRef);
|
||||
|
||||
let rendered: Omit<EntityRefPresentationSnapshot, 'entityRef' | 'entity'>;
|
||||
try {
|
||||
const output = this.#renderer.render({
|
||||
// Make a wrapping helper for rendering
|
||||
const render = (options: {
|
||||
loading: boolean;
|
||||
entity?: Entity;
|
||||
}): EntityRefPresentationSnapshot => {
|
||||
const { snapshot } = this.#renderer.render({
|
||||
entityRef: entityRef,
|
||||
loading: needsLoad,
|
||||
entity: entity,
|
||||
loading: options.loading,
|
||||
entity: options.entity,
|
||||
context: context || {},
|
||||
});
|
||||
rendered = output.snapshot;
|
||||
return {
|
||||
...snapshot,
|
||||
entityRef: entityRef,
|
||||
Icon: this.#maybeFallbackIcon(snapshot.Icon, kind),
|
||||
};
|
||||
};
|
||||
|
||||
// First the initial render
|
||||
let initialSnapshot: EntityRefPresentationSnapshot;
|
||||
try {
|
||||
initialSnapshot = render({
|
||||
loading: needsLoad,
|
||||
entity: entity,
|
||||
});
|
||||
} catch {
|
||||
// This is what gets presented if the renderer throws an error
|
||||
rendered = {
|
||||
initialSnapshot = {
|
||||
primaryTitle: entityRef,
|
||||
entityRef: entityRef,
|
||||
};
|
||||
}
|
||||
|
||||
// And then the following snapshot
|
||||
const observable = !needsLoad
|
||||
? undefined
|
||||
: new ObservableImpl<EntityRefPresentationSnapshot>(subscriber => {
|
||||
@@ -231,16 +276,11 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
newEntity &&
|
||||
newEntity.metadata.etag !== entity?.metadata.etag
|
||||
) {
|
||||
const output = this.#renderer.render({
|
||||
entityRef: entityRef,
|
||||
const updatedSnapshot = render({
|
||||
loading: false,
|
||||
entity: newEntity,
|
||||
context: context || {},
|
||||
});
|
||||
subscriber.next({
|
||||
...output.snapshot,
|
||||
entityRef: entityRef,
|
||||
});
|
||||
subscriber.next(updatedSnapshot);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -261,16 +301,14 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
});
|
||||
|
||||
return {
|
||||
snapshot: {
|
||||
...rendered,
|
||||
entityRef: entityRef,
|
||||
},
|
||||
snapshot: initialSnapshot,
|
||||
update$: observable,
|
||||
};
|
||||
}
|
||||
|
||||
#getEntityForInitialRender(entityOrRef: Entity | string): {
|
||||
entity: Entity | undefined;
|
||||
kind: string;
|
||||
entityRef: string;
|
||||
needsLoad: boolean;
|
||||
} {
|
||||
@@ -281,6 +319,7 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
if (typeof entityOrRef !== 'string') {
|
||||
return {
|
||||
entity: entityOrRef,
|
||||
kind: entityOrRef.kind,
|
||||
entityRef: stringifyEntityRef(entityOrRef),
|
||||
needsLoad: false,
|
||||
};
|
||||
@@ -297,6 +336,7 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
|
||||
return {
|
||||
entity: cachedEntity,
|
||||
kind: parseEntityRef(entityOrRef).kind,
|
||||
entityRef: entityOrRef,
|
||||
needsLoad,
|
||||
};
|
||||
@@ -308,8 +348,8 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
batchDelay: HumanDuration;
|
||||
renderer: DefaultEntityPresentationApiRenderer;
|
||||
}): DataLoader<string, Entity | undefined> {
|
||||
const cacheTtlMs = durationToMs(options.cacheTtl);
|
||||
const batchDelayMs = durationToMs(options.batchDelay);
|
||||
const cacheTtlMs = durationToMilliseconds(options.cacheTtl);
|
||||
const batchDelayMs = durationToMilliseconds(options.batchDelay);
|
||||
|
||||
return new DataLoader(
|
||||
async (entityRefs: readonly string[]) => {
|
||||
@@ -328,7 +368,7 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
return items;
|
||||
},
|
||||
{
|
||||
name: DefaultEntityPresentationApi.name,
|
||||
name: 'DefaultEntityPresentationApi',
|
||||
// This cache is the one that the data loader uses internally for
|
||||
// memoizing requests; essentially what it achieves is that multiple
|
||||
// requests for the same entity ref will be batched up into a single
|
||||
@@ -345,4 +385,17 @@ export class DefaultEntityPresentationApi implements EntityPresentationApi {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#maybeFallbackIcon(
|
||||
renderedIcon: IconComponent | false | undefined,
|
||||
kind: string,
|
||||
): IconComponent | false | undefined {
|
||||
if (renderedIcon) {
|
||||
return renderedIcon;
|
||||
} else if (renderedIcon === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.#kindIcons[kind.toLocaleLowerCase('en-US')];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* 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 { HumanDuration } from '@backstage/types';
|
||||
|
||||
export function durationToMs(duration: HumanDuration): number {
|
||||
const {
|
||||
years = 0,
|
||||
months = 0,
|
||||
weeks = 0,
|
||||
days = 0,
|
||||
hours = 0,
|
||||
minutes = 0,
|
||||
seconds = 0,
|
||||
milliseconds = 0,
|
||||
} = duration;
|
||||
|
||||
const totalDays = years * 365 + months * 30 + weeks * 7 + days;
|
||||
const totalHours = totalDays * 24 + hours;
|
||||
const totalMinutes = totalHours * 60 + minutes;
|
||||
const totalSeconds = totalMinutes * 60 + seconds;
|
||||
const totalMilliseconds = totalSeconds * 1000 + milliseconds;
|
||||
|
||||
return totalMilliseconds;
|
||||
}
|
||||
Reference in New Issue
Block a user