Merge branch 'master' of github.com:backstage/backstage into kubernetes-promises
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Make the "Copied!" message disappear automatically after some time
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Handle difference in expiration time between Microsoft session and Backstage session which caused the Backstage token to be invalid during a time frame.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-shortcuts': patch
|
||||
---
|
||||
|
||||
Fixed bug in LocalStoredShortcuts client where adding new Shortcut results in replacing entire shortcut list.
|
||||
|
||||
Refactored LocalStoredShortcuts client to listen to `storageApi` updates to ensure that local state is always up to date.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-firehydrant': minor
|
||||
---
|
||||
|
||||
Allow firehydrant to use component annotation
|
||||
@@ -50,6 +50,8 @@ import {
|
||||
import { Logger } from 'winston';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const BACKSTAGE_SESSION_EXPIRATION = 3600;
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
};
|
||||
@@ -145,12 +147,17 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
|
||||
|
||||
const { profile } = await this.authHandler(result, this.resolverContext);
|
||||
|
||||
const expiresInSeconds =
|
||||
result.params.expires_in === undefined
|
||||
? BACKSTAGE_SESSION_EXPIRATION
|
||||
: Math.min(result.params.expires_in, BACKSTAGE_SESSION_EXPIRATION);
|
||||
|
||||
const response: OAuthResponse = {
|
||||
providerInfo: {
|
||||
idToken: result.params.id_token,
|
||||
accessToken: result.accessToken,
|
||||
scope: result.params.scope,
|
||||
expiresInSeconds: result.params.expires_in,
|
||||
expiresInSeconds,
|
||||
},
|
||||
profile,
|
||||
};
|
||||
|
||||
@@ -90,9 +90,13 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
|
||||
const alertApi = useApi(alertApiRef);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
window.navigator.clipboard
|
||||
.writeText(window.location.toString())
|
||||
.then(() => alertApi.post({ message: 'Copied!', severity: 'info' }));
|
||||
window.navigator.clipboard.writeText(window.location.toString()).then(() =>
|
||||
alertApi.post({
|
||||
message: 'Copied!',
|
||||
severity: 'info',
|
||||
display: 'transient',
|
||||
}),
|
||||
);
|
||||
}, [alertApi]);
|
||||
|
||||
const extraItems = UNSTABLE_extraContextMenuItems && [
|
||||
|
||||
@@ -56,3 +56,11 @@ proxy:
|
||||
# Supply the token you generated from https://app.firehydrant.io/organizations/bots
|
||||
Authorization: Bearer fhb-e4911b22bcd788c4a4afeb0c111ffbfa
|
||||
```
|
||||
|
||||
4. Optionally add an annotation to the yaml config file of a component
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
annotations:
|
||||
firehydrant.com/service-name: <firehydrant-service-name>
|
||||
```
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
/// <reference types="react" />
|
||||
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -19,4 +20,7 @@ export const firehydrantPlugin: BackstagePlugin<
|
||||
{},
|
||||
{}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const isFireHydrantAvailable: (entity: Entity) => boolean;
|
||||
```
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"clean": "backstage-cli package clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface FireHydrantAPI {
|
||||
|
||||
getServiceDetails(options: {
|
||||
serviceName: string;
|
||||
lookupByName: boolean;
|
||||
}): Promise<ServiceDetailsResponse>;
|
||||
|
||||
getServiceIncidents(options: {
|
||||
@@ -77,11 +78,13 @@ export class FireHydrantAPIClient implements FireHydrantAPI {
|
||||
|
||||
async getServiceDetails(options: {
|
||||
serviceName: string;
|
||||
lookupByName: boolean;
|
||||
}): Promise<ServiceDetailsResponse> {
|
||||
const queryOpt = options.lookupByName ? 'name' : 'query';
|
||||
const query = new URLSearchParams();
|
||||
query.set(queryOpt, options.serviceName);
|
||||
const proxyUrl = await this.getApiUrl();
|
||||
const response = await fetch(
|
||||
`${proxyUrl}/services?query=${options.serviceName}`,
|
||||
);
|
||||
const response = await fetch(`${proxyUrl}/services?${query}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
|
||||
@@ -30,7 +30,9 @@ const apis = TestApiRegistry.from([fireHydrantApiRef, mockFireHydrantApi]);
|
||||
|
||||
jest.mock('@backstage/plugin-catalog-react', () => ({
|
||||
useEntity: () => {
|
||||
return { entity: { metadata: { name: 'service-example' } } };
|
||||
return {
|
||||
entity: { kind: 'Component', metadata: { name: 'service-example' } },
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { configApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { isFireHydrantAvailable, getFireHydrantServiceName } from '../hooks';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
button: {
|
||||
@@ -149,14 +150,14 @@ export const ServiceDetailsCard = () => {
|
||||
const startDate = DateTime.now().minus({ days: 30 }).toUTC();
|
||||
const endDate = DateTime.now().toUTC();
|
||||
|
||||
// The service name is provided by an annotation or a Backstage generated service name.
|
||||
// The Backstage service name in FireHydrant is a unique formatted string
|
||||
// that requires the entity's kind, name, and namespace.
|
||||
const fireHydrantServiceName = `${entity?.kind}:${
|
||||
entity?.metadata?.namespace ?? 'default'
|
||||
}/${entity?.metadata?.name}`;
|
||||
const fireHydrantServiceName = getFireHydrantServiceName(entity);
|
||||
|
||||
const { loading, value, error } = useServiceDetails({
|
||||
serviceName: fireHydrantServiceName,
|
||||
lookupByName: isFireHydrantAvailable(entity),
|
||||
});
|
||||
|
||||
const activeIncidents: string[] = value?.service?.active_incidents ?? [];
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Entity } from '@backstage/catalog-model';
|
||||
import { isFireHydrantAvailable, getFireHydrantServiceName } from './hooks';
|
||||
|
||||
describe('firehydrant-hooks-isFireHydrantAvailable', () => {
|
||||
it('should find an annotation', () => {
|
||||
const e: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'test-fh-name',
|
||||
annotations: {
|
||||
'firehydrant.com/service-name': 'test-fh-name',
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(isFireHydrantAvailable(e)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not find an annotation', () => {
|
||||
const e: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'test-fh-name',
|
||||
},
|
||||
};
|
||||
expect(isFireHydrantAvailable(e)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('firehydrant-hooks-getFireHydrantServiceName', () => {
|
||||
it('should return annotation service name', () => {
|
||||
const expected = 'test-fh-name';
|
||||
const e: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'test-fh-name',
|
||||
annotations: {
|
||||
'firehydrant.com/service-name': expected,
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getFireHydrantServiceName(e)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return generated service name', () => {
|
||||
const expected = 'component:default/test-fh-name';
|
||||
const e: Entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
namespace: 'default',
|
||||
name: 'test-fh-name',
|
||||
annotations: {},
|
||||
},
|
||||
};
|
||||
expect(getFireHydrantServiceName(e)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
export const FIREHYDRANT_SERVICE_NAME_ANNOTATION =
|
||||
'firehydrant.com/service-name';
|
||||
/** @public */
|
||||
export const isFireHydrantAvailable = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[FIREHYDRANT_SERVICE_NAME_ANNOTATION]);
|
||||
export const getFireHydrantServiceName = (entity: Entity) =>
|
||||
entity?.metadata.annotations?.[FIREHYDRANT_SERVICE_NAME_ANNOTATION] ??
|
||||
stringifyEntityRef(entity);
|
||||
@@ -17,13 +17,22 @@ import useAsyncRetry from 'react-use/lib/useAsyncRetry';
|
||||
import { fireHydrantApiRef } from '../api';
|
||||
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export const useServiceDetails = ({ serviceName }: { serviceName: string }) => {
|
||||
export const useServiceDetails = ({
|
||||
serviceName,
|
||||
lookupByName,
|
||||
}: {
|
||||
serviceName: string;
|
||||
lookupByName: boolean;
|
||||
}) => {
|
||||
const api = useApi(fireHydrantApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const { loading, value, error, retry } = useAsyncRetry(async () => {
|
||||
try {
|
||||
return await api.getServiceDetails({ serviceName: serviceName });
|
||||
return await api.getServiceDetails({
|
||||
serviceName: serviceName,
|
||||
lookupByName: lookupByName,
|
||||
});
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
return Promise.reject(e);
|
||||
|
||||
@@ -20,4 +20,8 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { firehydrantPlugin, FirehydrantCard } from './plugin';
|
||||
export {
|
||||
firehydrantPlugin,
|
||||
FirehydrantCard,
|
||||
isFireHydrantAvailable,
|
||||
} from './plugin';
|
||||
|
||||
@@ -50,3 +50,6 @@ export const FirehydrantCard = firehydrantPlugin.provide(
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export { isFireHydrantAvailable } from './components/hooks';
|
||||
|
||||
@@ -9,24 +9,25 @@ import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { default as Observable_2 } from 'zen-observable';
|
||||
import { StorageApi } from '@backstage/core-plugin-api';
|
||||
import { Shortcut as Shortcut_2 } from '@backstage/plugin-shortcuts';
|
||||
import { ShortcutApi as ShortcutApi_2 } from '@backstage/plugin-shortcuts';
|
||||
import type { StorageApi } from '@backstage/core-plugin-api';
|
||||
|
||||
// @public
|
||||
export class LocalStoredShortcuts implements ShortcutApi {
|
||||
export class LocalStoredShortcuts implements ShortcutApi_2 {
|
||||
constructor(storageApi: StorageApi);
|
||||
// (undocumented)
|
||||
add(shortcut: Omit<Shortcut, 'id'>): Promise<void>;
|
||||
add(shortcut: Omit<Shortcut_2, 'id'>): Promise<void>;
|
||||
// (undocumented)
|
||||
get(): Shortcut[];
|
||||
get(): Shortcut_2[];
|
||||
// (undocumented)
|
||||
getColor(url: string): string;
|
||||
// (undocumented)
|
||||
remove(id: string): Promise<void>;
|
||||
// (undocumented)
|
||||
shortcut$(): Observable_2<Shortcut[]>;
|
||||
shortcut$(): Observable<Shortcut_2[]>;
|
||||
// (undocumented)
|
||||
update(shortcut: Shortcut): Promise<void>;
|
||||
update(shortcut: Shortcut_2): Promise<void>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"@types/zen-observable": "^0.8.2",
|
||||
"react-hook-form": "^7.12.2",
|
||||
"react-use": "^17.2.4",
|
||||
"uuid": "^8.3.2",
|
||||
@@ -49,6 +48,7 @@
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/zen-observable": "^0.8.2",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
|
||||
@@ -36,8 +36,9 @@ describe('LocalStoredShortcuts', () => {
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
shortcutApi.add(shortcut);
|
||||
});
|
||||
observerNextHandler.mockClear(); // handler is called with current state to start
|
||||
await shortcutApi.add(shortcut);
|
||||
|
||||
expect(observerNextHandler).toHaveBeenCalledTimes(1);
|
||||
expect(observerNextHandler).toHaveBeenCalledWith(
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
|
||||
import { pageTheme } from '@backstage/theme';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ShortcutApi } from './ShortcutApi';
|
||||
import { Shortcut } from '../types';
|
||||
import { StorageApi } from '@backstage/core-plugin-api';
|
||||
import Observable from 'zen-observable';
|
||||
import { ShortcutApi, type Shortcut } from '@backstage/plugin-shortcuts';
|
||||
import type { StorageApi } from '@backstage/core-plugin-api';
|
||||
import type { Observable } from '@backstage/types';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
|
||||
/**
|
||||
* Implementation of the ShortcutApi that uses the StorageApi to store shortcuts.
|
||||
@@ -27,27 +27,41 @@ import Observable from 'zen-observable';
|
||||
* @public
|
||||
*/
|
||||
export class LocalStoredShortcuts implements ShortcutApi {
|
||||
private readonly shortcuts: Observable<Shortcut[]>;
|
||||
private shortcuts: Shortcut[];
|
||||
private readonly subscribers = new Set<
|
||||
ZenObservable.SubscriptionObserver<Shortcut[]>
|
||||
>();
|
||||
|
||||
private readonly observable = new ObservableImpl<Shortcut[]>(subscriber => {
|
||||
// forward the the latest value
|
||||
subscriber.next(this.shortcuts);
|
||||
|
||||
this.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
});
|
||||
|
||||
constructor(private readonly storageApi: StorageApi) {
|
||||
this.shortcuts = Observable.from(
|
||||
this.storageApi.observe$<Shortcut[]>('items'),
|
||||
).map(snapshot => snapshot.value ?? []);
|
||||
this.shortcuts = this.storageApi.snapshot<Shortcut[]>('items').value ?? [];
|
||||
this.storageApi.observe$<Shortcut[]>('items').subscribe({
|
||||
next: next => {
|
||||
this.shortcuts = next.value ?? [];
|
||||
this.notifyChanges();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
shortcut$() {
|
||||
return this.shortcuts;
|
||||
shortcut$(): Observable<Shortcut[]> {
|
||||
return this.observable;
|
||||
}
|
||||
|
||||
get() {
|
||||
return Array.from(
|
||||
this.storageApi.snapshot<Shortcut[]>('items').value ?? [],
|
||||
).sort((a, b) => (a.title >= b.title ? 1 : -1));
|
||||
return this.shortcuts;
|
||||
}
|
||||
|
||||
async add(shortcut: Omit<Shortcut, 'id'>) {
|
||||
const shortcuts = this.get();
|
||||
shortcuts.push({ ...shortcut, id: uuid() });
|
||||
const shortcuts = this.sort([...this.get(), { ...shortcut, id: uuid() }]);
|
||||
|
||||
await this.storageApi.set('items', shortcuts);
|
||||
}
|
||||
@@ -78,4 +92,14 @@ export class LocalStoredShortcuts implements ShortcutApi {
|
||||
catalog: 'home',
|
||||
docs: 'documentation',
|
||||
};
|
||||
|
||||
private sort(shortcuts: Shortcut[]): Shortcut[] {
|
||||
return shortcuts.slice().sort((a, b) => (a.title >= b.title ? 1 : -1));
|
||||
}
|
||||
|
||||
private notifyChanges() {
|
||||
for (const subscription of this.subscribers) {
|
||||
subscription.next(this.shortcuts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6249,6 +6249,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-firehydrant@workspace:plugins/firehydrant"
|
||||
dependencies:
|
||||
"@backstage/catalog-model": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/core-app-api": "workspace:^"
|
||||
"@backstage/core-components": "workspace:^"
|
||||
|
||||
Reference in New Issue
Block a user