plugins/app: add default implementation of DialogApi
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-app': patch
|
||||
---
|
||||
|
||||
Added implementation of the new `DialogApi`.
|
||||
@@ -43,6 +43,7 @@
|
||||
"@backstage/integration-react": "workspace:^",
|
||||
"@backstage/plugin-permission-react": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2025 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 { DialogApi, DialogApiDialog } from '@backstage/frontend-plugin-api';
|
||||
|
||||
export type OnShowDialog = (options: {
|
||||
component: (props: { dialog: DialogApiDialog<any> }) => React.JSX.Element;
|
||||
modal: boolean;
|
||||
}) => DialogApiDialog;
|
||||
|
||||
/**
|
||||
* Default implementation for the {@link DialogApi}.
|
||||
* @internal
|
||||
*/
|
||||
export class DefaultDialogApi implements DialogApi {
|
||||
#onShow?: OnShowDialog;
|
||||
|
||||
show<TResult = {}>(
|
||||
elementOrComponent:
|
||||
| JSX.Element
|
||||
| ((props: {
|
||||
dialog: DialogApiDialog<TResult | undefined>;
|
||||
}) => JSX.Element),
|
||||
): DialogApiDialog<TResult | undefined> {
|
||||
if (!this.#onShow) {
|
||||
throw new Error('Dialog API has not been connected');
|
||||
}
|
||||
return this.#onShow({
|
||||
component:
|
||||
typeof elementOrComponent === 'function'
|
||||
? elementOrComponent
|
||||
: () => elementOrComponent,
|
||||
modal: false,
|
||||
}) as DialogApiDialog<TResult | undefined>;
|
||||
}
|
||||
|
||||
showModal<TResult = {}>(
|
||||
elementOrComponent:
|
||||
| JSX.Element
|
||||
| ((props: { dialog: DialogApiDialog<TResult> }) => JSX.Element),
|
||||
): DialogApiDialog<TResult> {
|
||||
if (!this.#onShow) {
|
||||
throw new Error('Dialog API has not been connected');
|
||||
}
|
||||
return this.#onShow({
|
||||
component:
|
||||
typeof elementOrComponent === 'function'
|
||||
? elementOrComponent
|
||||
: () => elementOrComponent,
|
||||
modal: true,
|
||||
}) as DialogApiDialog<TResult>;
|
||||
}
|
||||
|
||||
connect(onShow: OnShowDialog): void {
|
||||
this.#onShow = onShow;
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ import {
|
||||
atlassianAuthApiRef,
|
||||
vmwareCloudAuthApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ApiBlueprint } from '@backstage/frontend-plugin-api';
|
||||
import { ApiBlueprint, dialogApiRef } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
ScmAuth,
|
||||
ScmIntegrationsApi,
|
||||
@@ -70,8 +70,19 @@ import {
|
||||
permissionApiRef,
|
||||
IdentityPermissionApi,
|
||||
} from '@backstage/plugin-permission-react';
|
||||
import { DefaultDialogApi } from './apis/DefaultDialogApi';
|
||||
|
||||
export const apis = [
|
||||
ApiBlueprint.make({
|
||||
name: 'dialog',
|
||||
params: {
|
||||
factory: createApiFactory({
|
||||
api: dialogApiRef,
|
||||
deps: {},
|
||||
factory: () => new DefaultDialogApi(),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
ApiBlueprint.make({
|
||||
name: 'discovery',
|
||||
params: {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2025 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, { useEffect, useState } from 'react';
|
||||
import {
|
||||
AppRootElementBlueprint,
|
||||
DialogApi,
|
||||
DialogApiDialog,
|
||||
dialogApiRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createDeferred } from '@backstage/types';
|
||||
import { OnShowDialog } from '../apis/DefaultDialogApi';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
|
||||
let dialogId = 0;
|
||||
function getDialogId() {
|
||||
dialogId += 1;
|
||||
return dialogId.toString(36);
|
||||
}
|
||||
|
||||
type DialogState = DialogApiDialog & {
|
||||
id: string;
|
||||
modal: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The other half of the default implementation of the {@link DialogApi}.
|
||||
*
|
||||
* This component is responsible for rendering the dialogs in the React tree and managing a stack of dialogs.
|
||||
* It expects the implementation of the {@link DialogApi} to be the `DefaultDialogApi`. If one is replaced the other must be too.
|
||||
* @internal
|
||||
*/
|
||||
function DialogDisplay({
|
||||
dialogApi,
|
||||
}: {
|
||||
dialogApi: DialogApi & { connect(onShow: OnShowDialog): void };
|
||||
}) {
|
||||
const [dialogs, setDialogs] = useState<
|
||||
{ dialog: DialogState; element: React.JSX.Element }[]
|
||||
>([]);
|
||||
// const [state, dispatch] = React.useReducer(dialogReducer)
|
||||
useEffect(() => {
|
||||
dialogApi.connect(options => {
|
||||
const id = getDialogId();
|
||||
const deferred = createDeferred<unknown>();
|
||||
const dialog: DialogState = {
|
||||
id,
|
||||
modal: options.modal,
|
||||
close(result) {
|
||||
deferred.resolve(result);
|
||||
setDialogs(ds => ds.filter(d => d.dialog.id !== id));
|
||||
},
|
||||
update(elementOrComponent) {
|
||||
const element =
|
||||
typeof elementOrComponent === 'function'
|
||||
? elementOrComponent({ dialog })
|
||||
: elementOrComponent;
|
||||
setDialogs(ds =>
|
||||
ds.map(d => (d.dialog.id === id ? { dialog, element } : d)),
|
||||
);
|
||||
},
|
||||
async result() {
|
||||
return deferred;
|
||||
},
|
||||
};
|
||||
const element = options.component({ dialog });
|
||||
setDialogs(ds => [...ds, { dialog, element }]);
|
||||
return dialog;
|
||||
});
|
||||
}, [dialogApi]);
|
||||
|
||||
if (dialogs.length > 0) {
|
||||
const lastDialog = dialogs[dialogs.length - 1];
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
onClose={() => {
|
||||
if (!lastDialog.dialog.modal) {
|
||||
lastDialog.dialog.close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{lastDialog.element}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const dialogDisplayAppRootElement =
|
||||
AppRootElementBlueprint.makeWithOverrides({
|
||||
name: 'dialog-display',
|
||||
factory(originalFactory, { apis }) {
|
||||
const dialogApi = apis.get(dialogApiRef);
|
||||
if (!isInternalDialogApi(dialogApi)) {
|
||||
throw new Error(
|
||||
`Invalid dialog API implementation, dialog API has been overridden without also overriding the dialog-display element, got ${dialogApi}`,
|
||||
);
|
||||
}
|
||||
return originalFactory({
|
||||
element: <DialogDisplay dialogApi={dialogApi} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function isInternalDialogApi(
|
||||
dialogApi?: DialogApi,
|
||||
): dialogApi is DialogApi & { connect(onShow: OnShowDialog): void } {
|
||||
if (!dialogApi) {
|
||||
return false;
|
||||
}
|
||||
return 'connect' in dialogApi;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export { IconsApi } from './IconsApi';
|
||||
export { FeatureFlagsApi } from './FeatureFlagsApi';
|
||||
export { TranslationsApi } from './TranslationsApi';
|
||||
export { DefaultSignInPage } from './DefaultSignInPage';
|
||||
export { dialogDisplayAppRootElement } from './DialogDisplay';
|
||||
export {
|
||||
DefaultProgressComponent,
|
||||
DefaultErrorBoundaryComponent,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
oauthRequestDialogAppRootElement,
|
||||
alertDisplayAppRootElement,
|
||||
DefaultSignInPage,
|
||||
dialogDisplayAppRootElement,
|
||||
} from './extensions';
|
||||
import { apis } from './defaultApis';
|
||||
|
||||
@@ -62,5 +63,6 @@ export const appPlugin = createFrontendPlugin({
|
||||
DefaultSignInPage,
|
||||
oauthRequestDialogAppRootElement,
|
||||
alertDisplayAppRootElement,
|
||||
dialogDisplayAppRootElement,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -5099,6 +5099,7 @@ __metadata:
|
||||
"@backstage/integration-react": "workspace:^"
|
||||
"@backstage/plugin-permission-react": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@material-ui/core": ^4.9.13
|
||||
"@material-ui/icons": ^4.9.1
|
||||
"@material-ui/lab": ^4.0.0-alpha.61
|
||||
|
||||
Reference in New Issue
Block a user