Skip to content

Admin UI redesign #2121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
db5b5ba
Boostrapped React-based admin panel.
alzaslon Feb 9, 2023
3eaab4a
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova Feb 12, 2023
0334e9d
Navigation top level
jsorohova Feb 13, 2023
cf371f0
Navigation - pages
jsorohova Feb 14, 2023
ee7cda7
Pages navigation item
jsorohova Feb 28, 2023
c1cdb69
Delete confirmation code improvement
jsorohova Feb 28, 2023
bf3c7bd
Navigation 'Navigation' item
jsorohova Mar 6, 2023
c0c1f39
Settings and Help navigation items, Top panel
jsorohova Mar 7, 2023
181e80c
Page/Layout modals
jsorohova Mar 14, 2023
b42d75a
Media modal
jsorohova Mar 29, 2023
8842df5
Media modals
jsorohova May 19, 2023
9f3d2a6
Merge conflict fixes
jsorohova May 19, 2023
ca13e11
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova May 23, 2023
9b94bde
Focused layout
jsorohova May 25, 2023
90b2aa7
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova May 28, 2023
cfa92de
Styles and Custom widgets items
jsorohova May 31, 2023
c0a2bca
Mobile styles
jsorohova Jun 3, 2023
522b60c
URLs navigation item
jsorohova Jun 10, 2023
62475bc
Toast notifications
jsorohova Jun 12, 2023
38b82fd
Site menu
jsorohova Jun 14, 2023
803daa1
Favicon upload
jsorohova Jun 14, 2023
7e4b65e
Popups item
jsorohova Jun 14, 2023
027dab9
Merged master
jsorohova Jun 19, 2023
39b0929
Onboarding modal
jsorohova Jun 27, 2023
6c6fcea
Components updates
jsorohova Jul 14, 2023
01e6114
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova Jul 17, 2023
2b11227
Merged master branch
jsorohova Aug 14, 2023
6a74d90
Anchors and bug fixes
jsorohova Sep 6, 2023
85b7fb7
Content changes
jsorohova Sep 9, 2023
c1ea0c7
Merged master branch
jsorohova Sep 20, 2023
42213f2
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova Sep 20, 2023
8f329d9
Validation implementation
jsorohova Sep 28, 2023
446a915
Reset content flow restyling
jsorohova Sep 28, 2023
06ec0f6
Pagination support
jsorohova Sep 28, 2023
9e21876
Loaders and content changes
jsorohova Oct 6, 2023
8b836fc
Permalink validation fixes
jsorohova Oct 6, 2023
403190d
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova Oct 6, 2023
2c87b7d
Feedback link change
jsorohova Oct 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,968 changes: 7,952 additions & 16 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@types/mime": "^3.0.1",
"@types/mocha": "10.0.1",
"@types/node": "^20.6.2",
"@types/react": "^18.0.27",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
"autoprefixer": "^10.4.15",
Expand Down Expand Up @@ -74,12 +75,15 @@
"@azure/api-management-custom-widgets-tools": "^1.0.0-beta.1",
"@azure/msal-browser": "^2.37.1",
"@braintree/sanitize-url": "6.0.4",
"@fluentui/font-icons-mdl2": "^8.5.9",
"@fluentui/react": "^8.105.11",
"@microsoft/applicationinsights-web": "^3.0.2",
"@monaco-editor/loader": "^1.3.3",
"@paperbits/azure": "0.1.592",
"@paperbits/common": "0.1.592",
"@paperbits/core": "0.1.592",
"@paperbits/forms": "0.1.592",
"@paperbits/forms": "0.1.592",
"@paperbits/react": "1.0.7",
"@paperbits/styles": "0.1.592",
"@webcomponents/custom-elements": "1.6.0",
"@webcomponents/shadydom": "^1.11.0",
Expand All @@ -103,8 +107,13 @@
"moment": "^2.29.4",
"monaco-editor": "^0.29.1",
"msal": "^1.4.18",
"nuka-carousel": "^6.0.3",
"prettier": "^2.8.8",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-cropper": "^2.3.2",
"react-dom": "^18.2.0",
"react-toastify": "^9.1.3",
"rehype-raw": "^6.1.1",
"rehype-sanitize": "^5.0.1",
"rehype-stringify": "^9.0.3",
Expand All @@ -116,4 +125,4 @@
"topojson-client": "^3.1.0",
"html-truncate": "1.2.2"
}
}
}
264 changes: 264 additions & 0 deletions src/admin/custom-widgets/customWidgetDetailsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import * as React from 'react';
import * as Utils from '@paperbits/common/utils';
import { Resolve } from '@paperbits/react/decorators';
import { IWidgetService } from '@paperbits/common/widgets';
import { EventManager } from '@paperbits/common/events';
import { KnockoutComponentBinder } from '@paperbits/core/ko/knockoutComponentBinder';
import { buildBlobConfigPath, buildBlobDataPath } from '@azure/api-management-custom-widgets-tools';
import { widgetFolderName, displayNameToName } from '@azure/api-management-custom-widgets-scaffolder';
import { CustomWidgetEditorViewModel, CustomWidgetViewModel, CustomWidgetViewModelBinder } from '../../components/custom-widget/ko';
import { CustomWidgetHandlers, CustomWidgetModelBinder, TCustomWidgetConfig, widgetCategory } from '../../components/custom-widget';
import { CustomWidgetModel } from '../../components/custom-widget/customWidgetModel';
import fallbackUi from '!!raw-loader!../../components/custom-widget-list/fallbackUi.html';
import { MapiBlobStorage } from '../../persistence';
import { ChoiceGroup, CommandBarButton, DefaultButton, IChoiceGroupOption, Icon, IIconProps, Link, Modal, PrimaryButton, Stack, Text, TextField } from '@fluentui/react';
import { DeleteConfirmationOverlay } from '../utils/components/deleteConfirmationOverlay';
import { CopyableTextField } from '../utils/components/copyableTextField';
import { UNIQUE_REQUIRED, validateField } from '../utils/validator';

interface CustomWidgetDetailsModalState {
isEdit: boolean,
customWidget: TCustomWidgetConfig,
saveButtonDisabled: boolean,
showInstructions: boolean,
showDeleteConfirmation: boolean
}

interface CustomWidgetDetailsModalProps {
customWidget: TCustomWidgetConfig,
customWidgets: TCustomWidgetConfig[],
onDismiss: () => void
}

const deleteIcon: IIconProps = { iconName: 'Delete' };
const textFieldStyles = { root: { paddingBottom: 15 } };
const listItemStyles = { root: { marginBottom: 10 } };

const technology: IChoiceGroupOption[] = [
{ key: 'typescript', text: 'TypeScript', styles: { field: { padding: 0 }} },
{ key: 'react', text: 'React', styles: { field: { padding: 0 }} },
{ key: 'vue', text: 'Vue', styles: { field: { padding: 0 }} }
];

export class CustomWidgetDetailsModal extends React.Component<CustomWidgetDetailsModalProps, CustomWidgetDetailsModalState> {
@Resolve('widgetService')
public widgetService: IWidgetService;

@Resolve('blobStorage')
public blobStorage: MapiBlobStorage;

@Resolve('eventManager')
public eventManager: EventManager;

constructor(props: CustomWidgetDetailsModalProps) {
super(props);

this.state = {
isEdit: !!this.props.customWidget,
customWidget: this.props.customWidget ?? { name: 'new-custom-widget', displayName: 'New custom widget', technology: 'typescript' },
saveButtonDisabled: false,
showInstructions: false,
showDeleteConfirmation: false
}
}

onInputChange = async (field: string, newValue: string): Promise<void> => {
this.setState({
customWidget: {
...this.state.customWidget,
[field]: newValue
}
});
}

deleteCustomWidget = async (): Promise<void> => {
const blobsToDelete = await this.blobStorage.listBlobs(buildBlobDataPath(this.state.customWidget.name));
blobsToDelete.push(buildBlobConfigPath(this.state.customWidget.name));
await Promise.all(blobsToDelete.map(blobKey => this.blobStorage.deleteBlob(blobKey)));

this.eventManager.dispatchEvent('onSaveChanges');
this.props.onDismiss();
}

closeDeleteConfirmation = (): void => {
this.setState({ showDeleteConfirmation: false });
}

saveCustomWidget = async (): Promise<void> => {
const name = displayNameToName(this.state.customWidget.displayName);
const config: TCustomWidgetConfig = {
name,
displayName: this.state.customWidget.displayName,
technology: this.state.customWidget.technology
};

const content = Utils.stringToUnit8Array(JSON.stringify(config));
await this.blobStorage.uploadBlob(buildBlobConfigPath(name), content);

const fallbackUiUnit8 = Utils.stringToUnit8Array(fallbackUi);
const dataPath = buildBlobDataPath(name);
await this.blobStorage.uploadBlob(`/${dataPath}index.html`, fallbackUiUnit8);
await this.blobStorage.uploadBlob(`/${dataPath}editor.html`, fallbackUiUnit8);

this.widgetService.registerWidget(name, {
modelDefinition: CustomWidgetModel,
componentBinder: KnockoutComponentBinder,
componentDefinition: CustomWidgetViewModel,
modelBinder: CustomWidgetModelBinder,
viewModelBinder: CustomWidgetViewModelBinder
});

this.widgetService.registerWidgetEditor(name, {
displayName: this.state.customWidget.displayName,
category: widgetCategory,
iconClass: "widget-icon widget-icon-component",
componentBinder: KnockoutComponentBinder,
componentDefinition: CustomWidgetEditorViewModel,
handlerComponent: new CustomWidgetHandlers(config)
});

this.eventManager.dispatchEvent('onSaveChanges');
this.setState({ isEdit: true, showInstructions: true });
}

validateCustomWidgetName = (displayName: string): string => {
if (this.props.customWidget) return '';

const name = displayNameToName(displayName);
const isValidName = !!!this.props.customWidgets.find((config) => config.name === name);
const errorMessage = validateField(UNIQUE_REQUIRED, displayName, isValidName);

this.setState({ saveButtonDisabled: errorMessage.length !== 0 });

return errorMessage;
}

render(): JSX.Element {
return <>
{this.state.showDeleteConfirmation &&
<DeleteConfirmationOverlay
deleteItemTitle={this.state.customWidget.displayName}
onConfirm={this.deleteCustomWidget.bind(this)}
onDismiss={this.closeDeleteConfirmation.bind(this)}
/>
}
<Modal
isOpen={true}
onDismiss={this.props.onDismiss}
containerClassName="admin-modal"
>
<Stack horizontal horizontalAlign="space-between" verticalAlign="center" className="admin-modal-header">
<Text block nowrap className="admin-modal-header-text">Custom widget / { this.state.customWidget.displayName }</Text>
<Stack horizontal tokens={{ childrenGap: 20 }}>
{!this.state.isEdit &&
<PrimaryButton
text="Save"
onClick={() => this.saveCustomWidget()}
disabled={this.state.saveButtonDisabled}
/>
}
<DefaultButton
text={this.state.isEdit ? 'Close' : 'Discard'}
onClick={this.props.onDismiss}
/>
</Stack>
</Stack>
<div className="admin-modal-content">
{this.state.isEdit &&
<CommandBarButton
iconProps={deleteIcon}
text="Delete"
onClick={() => this.setState({ showDeleteConfirmation: true })}
styles={{ root: { height: 44, marginBottom: 30 } }}
/>
}
<TextField
label="Name"
value={this.state.customWidget.displayName}
onChange={(event, newValue) => this.onInputChange('displayName', newValue)}
styles={textFieldStyles}
onGetErrorMessage={(value) => this.validateCustomWidgetName(value)}
disabled={this.state.isEdit}
required
/>
<ChoiceGroup
label="Technology"
options={technology}
selectedKey={this.state.customWidget.technology}
onChange={(event, option) => this.onInputChange('technology', option.key)}
disabled={this.state.isEdit}
styles={{ label: { padding: 0 } }}
/>
{this.state.isEdit &&
<Stack horizontal onClick={() => this.setState({ showInstructions: !this.state.showInstructions })} styles={{ root: { cursor: 'pointer', marginTop: 20 } }}>
<Icon
iconName="ChevronDown"
className={`collapsible-arrow ${this.state.showInstructions ? 'opened' : 'closed'}`}
/>
<Text styles={{ root: { fontWeight: 'bold' } }}>Get started with the development</Text>
</Stack>
}
<Stack className={`collapsible-section${!this.state.showInstructions ? ' hidden' : ''}`}>
<Text block styles={{ root: { paddingTop: 20 } }}>
Follow the steps below to create, implement, and deploy a custom widget.
<Link href="https://aka.ms/apimdocs/portal/customwidgets" target="_blank">Learn more</Link>.
</Text>
<ol>
<li>
<Text block styles={listItemStyles}>Open the terminal, navigate to the location where you want to save the widget, and execute the following command to download the code scaffold:</Text>
<CopyableTextField
fieldLabel="downloading the code scaffold command"
showLabel={false}
copyableValue={`npx @azure/api-management-custom-widgets-scaffolder --displayName="${this.state.customWidget.displayName}" --technology="${this.state.customWidget.technology}" --openUrl="${window.location.origin}"`}
/>
</li>
<li>
<Text block styles={listItemStyles}>Navigate to the newly created folder with the widget's code scaffold:</Text>
<CopyableTextField
fieldLabel="navigating to the new folder command"
showLabel={false}
copyableValue={`cd ${widgetFolderName(this.state.customWidget.name)}`}
/>
</li>
<li>
<Text block styles={listItemStyles}>Open the folder in the code editor of choice. For example:</Text>
<CopyableTextField
fieldLabel="opening the code command"
showLabel={false}
copyableValue="code ."
/>
</li>
<li>
<Text block styles={listItemStyles}>Install the dependencies:</Text>
<CopyableTextField
fieldLabel="dependencies installation command"
showLabel={false}
copyableValue="npm install"
/>
</li>
<li>
<Text block styles={listItemStyles}>Start the project:</Text>
<CopyableTextField
fieldLabel="project starting command"
showLabel={false}
copyableValue="npm start"
/>
</li>
<li>
<Text block styles={textFieldStyles}>Implement the code of the widget and test it locally.</Text>
</li>
<li>
<Text block styles={listItemStyles}>Deploy the custom widget to the developer portal in your API Management service:</Text>
<CopyableTextField
fieldLabel="deploying the custom widget command"
showLabel={false}
copyableValue="npm run deploy"
/>
</li>
</ol>
</Stack>
</div>
</Modal>
</>
}
}
Loading