-
Notifications
You must be signed in to change notification settings - Fork 343
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
Admin UI redesign #2121
Changes from 26 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
db5b5ba
Boostrapped React-based admin panel.
alzaslon 3eaab4a
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova 0334e9d
Navigation top level
jsorohova cf371f0
Navigation - pages
jsorohova ee7cda7
Pages navigation item
jsorohova c1cdb69
Delete confirmation code improvement
jsorohova bf3c7bd
Navigation 'Navigation' item
jsorohova c0c1f39
Settings and Help navigation items, Top panel
jsorohova 181e80c
Page/Layout modals
jsorohova b42d75a
Media modal
jsorohova 8842df5
Media modals
jsorohova 9f3d2a6
Merge conflict fixes
jsorohova ca13e11
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova 9b94bde
Focused layout
jsorohova 90b2aa7
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova cfa92de
Styles and Custom widgets items
jsorohova c0a2bca
Mobile styles
jsorohova 522b60c
URLs navigation item
jsorohova 62475bc
Toast notifications
jsorohova 38b82fd
Site menu
jsorohova 803daa1
Favicon upload
jsorohova 7e4b65e
Popups item
jsorohova 027dab9
Merged master
jsorohova 39b0929
Onboarding modal
jsorohova 6c6fcea
Components updates
jsorohova 01e6114
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova 2b11227
Merged master branch
jsorohova 6a74d90
Anchors and bug fixes
jsorohova 85b7fb7
Content changes
jsorohova c1ea0c7
Merged master branch
jsorohova 42213f2
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova 8f329d9
Validation implementation
jsorohova 446a915
Reset content flow restyling
jsorohova 06ec0f6
Pagination support
jsorohova 9e21876
Loaders and content changes
jsorohova 8b836fc
Permalink validation fixes
jsorohova 403190d
Merge branch 'master' of https://github.com/Azure/api-management-deve…
jsorohova 2c87b7d
Feedback link change
jsorohova File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
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) => { | ||
this.setState({ | ||
customWidget: { | ||
...this.state.customWidget, | ||
[field]: newValue | ||
} | ||
}); | ||
} | ||
|
||
deleteCustomWidget = async () => { | ||
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 = () => { | ||
this.setState({ showDeleteConfirmation: false }); | ||
} | ||
|
||
saveCustomWidget = async () => { | ||
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() { | ||
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} | ||
/> | ||
<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> | ||
</> | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.