ReactViews are the recommended way to build all new experiences within the portal. This guide assumes you have already onboarded your extension to ReactViews, and are looking to build your first experience. We will slightly cover React, but for thorough documentation, it is recommended you visit their official documentation. We will focus on the specific differences of writing React within the ReactView framework versus writing React elsewhere.
Let's build a simple experience that queries ARM for a resource type across a set of subscriptions, and then displays the results to the user in a grid. After we build the base experience, you can switch to one of the various guides that improves the blade further.
ReactView development is designed to enable fast inner dev loops. Sideload your extension and add the feature flag in the query string:
&feature.reactreload=true
. Then, within your ReactView folder, run the command that triggers Webpack to run in watch mode. In our onboarding guide
we called this command npm run watch
.
Next create an empty file in your ReactView folder, and name it GettingStarted.ReactView.tsx
. All code below will go into this file.
Every ReactView needs a root component with a decorator on it. This results in boilerplate that looks like this:
import * as React from "react"; // this is needed for all react code, regardless of whether or not it's explicitly referenced
import * as ReactView from "@microsoft/azureportal-reactview/ReactView"; // this import gives us decorators for initializing and rendering the view
import { setTitle } from "@microsoft/azureportal-reactview/Az"; // allows us to set the title of the ReactView
setTitle("Getting Started"); // this can be called anywhere, in production scenarios this should be passed a localized string
@ReactView.ReduxFree.Decorator()
export class GettingStarted extends React.Component { // a completely empty React class component, needed
public render() {
return <span>Getting Started</span>;
}
}
Calling ARM in ReactViews should be done using batch, just like in Knockout blades. We'll also use the ArmResource
type from the ResourceManagement
module. A simple batch call to get all the resources looks like this:
import { ArmResource } from "@microsoft/azureportal-reactview/ResourceManagement";
import { batch } from "@microsoft/azureportal-reactview/Ajax";
function getAllResources() {
return batch<{ value: ArmResource[] }>({
uri: "/resources?api-version=2019-10-01",
type: "GET",
setTelemetryHeader: "GettingStarted.ReactView/AllResources",
}).then((armResponse) => {
return armResponse.content.value;
});
}
Now that we have a way to get the data, we need to make the data available within our component. React components typically store this information on their state. The state is a mutable store, local to an instance of a component. Mutating the store will trigger a re-render of the component. The full depth of state is beyond the scope of this guide, you can read more about it in the official React documentation.
For now, we'll define a simple state on the component where the resources we've retrieved from ARM can be accessed:
interface GettingStartedState { // typing for the state, you can also anonymously declare state within the generic below
resources: ArmResource[];
}
@ReactView.ReduxFree.Decorator({
viewReady: (state: GettingStartedState) => !!state.resources // now that we load data in our experience, we want to wait to assert ready until we've loaded it
})
class GettingStarted extends React.Component<{}, GettingStartedState> { // the first generic is for props, since we don't need any we leave it empty
constructor(props: {}) {
super(props); // you must call the super and pass the props from the constructor in
this.state = {}; // initialize the state. The synchronous part of the constructor is the only time you can directly set the state
getAllResources().then(resources => { // call our data loading function from earlier
this.setState({resources: resources}); // call this.setState to load our data into the state
});
}
}
Once the data is available in the component's state, we want to modify our render function to display the data to the user:
The next step is to render the data so the user can see it, we can do this by leveraging the @fluentui/react
List:
import { List } from "@fluentui/react/lib/List"; // Get the List component, note the direct import rather than importing from index
class GettingStarted extends React.Component<{}, GettingStartedState> {
public render() {
if (this.state.resources === undefined) {
return <></>; // if we don't have anything to render, we return a React Fragment
}
return <List
items={this.state.resources} // pass in the list of resource objects to render
onRenderCell={(resource) => { // this function gets called for each resource object, and should return a React.Node representation
return <span>{resource.name} is in {resource.location}</span>;
}}
>;
}
}
While the ArmResource
returned from our batch call gives us some data, to get the full metadata we want to parse it
further. We can do this with the ResourceMetadata
namespace within the ResourceManagement
module:
import { ArmResource, ResourceMetadata } from "@microsoft/azureportal-reactview/ResourceManagement";
import { getAllAssetTypes } from "@microsoft/azureportal-reactview/AssetTypes";
interface GettingStartedState {
resources: ResourceMetadata[]; // Update the state typing
}
class GettingStarted extends React.Component<{}, GettingStartedState> {
constructor(props: {}) {
Promise.all([getAllResources(), getAllAssetTypes()]) // parsing ArmResource requires AssetTypes
.then(results => {
const [armResources, assetTypes] = results; // destructure the returned array
const resources = armResources.map(resource => {
return ResourceMetadata.parse(resource, assetTypes); // map the resource array to a metadata array
});
this.setState({ resources: resources });
});
}
}
We now have to update the render function to accommodate the new shape of the resources, additionally we can now render the subscriptionId:
class GettingStarted extends React.Component<{}, GettingStartedState> {
public render() {
if (this.state.resources === undefined) {
return <></>; // If we don't have anything to render, we return a React Fragment
}
return <List
items={this.state.resources}
onRenderCell={(resource) => { // this function gets called for each resource object, and should return a React.Node representation
return <span>{resource.resourceName} is in {resource.resourceLocation} and {resource.armId.subscription}</span>;
}}
/>;
}
}
Let's add some icons for the resources to improve the look of the rows. To render the icons, we need to use
the FrameworkIcon
component. Additionally, we'll refactor our renderCell function outside of our class:
/* the FrameworkIcon component is designed to make rendering the icons in the ResourceMetadata easier */
import { FrameworkIcon } from "@microsoft/azureportal-reactview/FrameworkIcon";
const onRenderCell = (resource: ResourceMetadata) => {
return <>
<FrameworkIcon image={resource.icon} // FrameworkIcon can directly handle the icon value
style={{
width: "24px",
height: "24px",
display: "inline-block",
verticalAlign: "middle",
overflow: "hidden",
}}
/>
<span>{resource.resourceName} is in {resource.resourceLocation} and subscription {resource.armId.subscription}</span>
</>;
};
class GettingStarted extends React.Component<{}, GettingStartedState> {
public render() {
if (this.state.resources === undefined) {
return <></>;
}
return <List
items={this.state.resources}
/* This refactor cleans our code, and improves performance by preventing rerenders due to redeclaration */
onRenderCell={onRenderCell}
/>;
}
}
Let's also pull the custom styling on the icon into its own CSS class. To do that, we'll use a function called
mergeStyleSets
from @fluentui/react
:
import { mergeStyleSets } from '@fluentui/react/lib/Styling';
const classNames = mergeStyleSets({
icon: {
width: "24px",
height: "24px",
display: "inline-block",
verticalAlign: "middle",
},
});
const onRenderCell = (resource: ResourceMetadata) => {
return <>
<FrameworkIcon image={resource.icon} className={classNames.icon}/>
<span>{resource.resourceName} is in {resource.resourceLocation} and subscription {resource.armId.subscription}</span>
</>;
};
The next task is to improve the styling we have in our ReactView, the goal is to convert our items from rows into
cards. To do this, we'll leverage the Stack
component from @fluentui/react
as well as reach into our current
color pallette to help style the card:
import { Stack, StackItem } from "@fluentui/react/lib/Stack";
import { mergeStyleSets } from '@fluentui/react/lib/Styling';
import { useTheme } from "@fluentui/react-theme-provider/lib/useTheme";
const classNames = mergeStyleSets({
listRootDiv: {
width: "200px", // By restricting the width, we can create a card like layout
float: "left",
margin: "5px", // Put some padding around our cards
padding: "5px",
},
/* Below class is to force overflow text to end in ellipsis */
detailsStackItem: {
textOverflow: "ellipsis",
width: "140px", // 200px (width of parent) - 48px (width of icon) - 12px (width of left padding)
whiteSpace: "nowrap",
overflow: "hidden",
paddingLeft: "12px",
},
icon: {
width: "48px", // note we increased the size of the icon here
height: "48px",
display: "inline-block",
verticalAlign: "middle",
},
});
const onRenderCell = (resource: ResourceMetadata) => {
/*
Here we create a layout that has a left side, big icon and a right side with the three properties listed.
To do this, we create a Horizontal stack of 200 px, with the first item being the Icon, and the second
being a vertical stack of our three property values.
*/
// the useTheme hook allows us to grab the current correct color, and have it update as the portal theme changes
const backgroundColor = useTheme().semanticColors.bodyStandoutBackground;
return <div className={classNames.listRootDiv} styles={{ backgroundColor }}>
<Stack horizontal>
<StackItem >
<FrameworkIcon image={resource.icon} className={classNames.icon} />
</StackItem>
<StackItem>
<Stack>
<StackItem className={classNames.detailsStackItem}>
{resource.resourceName}
</StackItem>
<StackItem className={classNames.detailsStackItem}>
{resource.resourceLocation || "NoRegion"}
</StackItem>
<StackItem className={classNames.detailsStackItem}>
{resource.armId.subscription}
</StackItem>
</Stack>
</StackItem>
</Stack>
</div>;
};
export class GettingStarted extends React.Component<{}, GettingStartedState> {
public render() {
if (this.state.resources === undefined) {
return <></>;
}
return <List
items={this.state.resources}
onRenderCell={onRenderCell}
getItemCountForPage={() => this.state.resources?.length} // We add this to render all items
/>;
}
}
Let's add a bit of functionality to our cards, first a search box to filter by resourceName:
import { SearchBox } from "@fluentui/react/lib/SearchBox"; // use the built in searchbox
interface GettingStartedState {
resources?: ResourceMetadata[];
searchQuery?: string; // add our search string into the state
}
export class GettingStarted extends React.Component<{}, GettingStartedState> {
public render() {
if (this.state.resources === undefined) {
return <></>;
}
return <>
<SearchBox
onChange={(_, value) => {
/* When the search box changes, set the new value into our new searchQuery state */
this.setState({searchQuery: value.toLowerCase()});
}}
/>
<List
items={this.state.resources.filter(i => {
/* filter the items to only include those that include the searchQuery */
return i.armId.resourceName.toLowerCase().includes(this.state.searchQuery || "")
})}
onRenderCell={onRenderCell}
getItemCountForPage={() => this.state.resources?.length}
/>
</>;
}
Now we have a reasonably functioning searchbox. Let's add another filter for Subscription. This time, the component providing the filter will come from the ReactView package:
import { SubscriptionFilter } from "@microsoft/azureportal-reactview/SubscriptionFilter";
interface GettingStartedState {
resources?: ResourceMetadata[];
searchQuery?: string;
subscriptionIds?: string[];
}
export class GettingStarted extends React.Component<{}, GettingStartedState> {
render() {
if (this.state.resources === undefined) {
return <></>;
}
return <>
<SubscriptionFilter
onSubscriptionChange={(subs) => {
/* Update the state with the new set of subscriptions */
this.setState({
subscriptionIds: subs.map(i => i.subscriptionId)
});
}}
/>
<SearchBox
onChange={(_, value) => {
this.setState({searchQuery: value.toLowerCase()});
}}
/>
<List
items={this.state.resources
.filter(resource => {
return resource.armId.resourceName.toLowerCase().includes(this.state.searchQuery || "")
})
.filter((resource => {
/* Add another filter on the set of resources to render, accounting for the possible null ref */
return (this.state.subscriptionIds || []).includes(resource.armId.subscription);
}))
}
onRenderCell={onRenderCell}
getItemCountForPage={() => this.state.resources?.length}
/>
</>;
}
}
If you followed the above steps, we now have a view that has a grid of cards that we can search and filter within. Here's the full set of code that drives the experience:
import * as ReactView from "@microsoft/azureportal-reactview/ReactView";
import * as React from "react";
import { List } from "@fluentui/react/lib/List";
import { Stack, StackItem } from "@fluentui/react/lib/Stack";
import { mergeStyleSets } from "@fluentui/react/lib/Styling";
import { useTheme } from "@fluentui/react-theme-provider/lib/useTheme";
import { batch } from "@microsoft/azureportal-reactview/Ajax";
import { setTitle } from "@microsoft/azureportal-reactview/Az";
import { ResourceMetadata, ArmResource } from "@microsoft/azureportal-reactview/ResourceManagement";
import { getAllAssetTypes } from "@microsoft/azureportal-reactview/AssetTypes";
import { FrameworkIcon } from "@microsoft/azureportal-reactview/FrameworkIcon";
import { SearchBox } from "@fluentui/react/lib/SearchBox";
import { SubscriptionFilter } from "@microsoft/azureportal-reactview/SubscriptionFilter";
function getAllResources() {
return batch<{ value: ArmResource[] }>({
uri: "/resources?api-version=2019-10-01",
type: "GET",
setTelemetryHeader: "GettingStarted.ReactView/AllResources",
}).then((armResponse) => {
return armResponse.content.value;
});
}
interface GettingStartedState {
resources?: ResourceMetadata[];
searchQuery?: string;
subscriptionIds?: string[];
}
const classNames = mergeStyleSets({
listRootDiv: {
width: "200px",
float: "left",
margin: "5px",
padding: "5px",
},
detailsStackItem: {
textOverflow: "ellipsis",
width: "140px",
whiteSpace: "nowrap",
overflow: "hidden",
paddingLeft: "12px",
},
icon: {
width: "48px",
height: "48px",
display: "inline-block",
verticalAlign: "middle",
},
});
const onRenderCell = (resource: ResourceMetadata) => {
const backgroundColor = useTheme().semanticColors.bodyStandoutBackground;
return <div className={classNames.listRootDiv} style={{ backgroundColor }} >
<Stack horizontal>
<StackItem >
<FrameworkIcon image={resource.icon} className={classNames.icon} />
</StackItem>
<StackItem>
<Stack>
<StackItem className={classNames.detailsStackItem}>
{resource.resourceName}
</StackItem>
<StackItem className={classNames.detailsStackItem}>
{resource.resourceLocation || "NoRegion"}
</StackItem>
<StackItem className={classNames.detailsStackItem}>
{resource.armId.subscription}
</StackItem>
</Stack>
</StackItem>
</Stack>
</div>;
};
@ReactView.ReduxFree.Decorator({
viewReady: (state: GettingStartedState) => !!state.resources,
})
export class GettingStarted extends React.Component<{}, GettingStartedState> {
constructor(props: {}) {
super(props);
Promise.all([getAllResources(), getAllAssetTypes()])
.then(results => {
const [armResources, assetTypes] = results;
const resources = armResources.map(resource => {
return ResourceMetadata.parse(resource, assetTypes);
});
this.setState({ resources });
});
this.state = {};
setTitle("Getting Started");
}
render() {
if (this.state.resources === undefined) {
return <></>; // If we don't have anything to render, we return a React Fragment
}
return <>
<SubscriptionFilter
onSubscriptionChange={(subs) => {
this.setState({
subscriptionIds: subs.map(i => i.subscriptionId),
});
}}
/>
<SearchBox
onChange={(_, value) => {
this.setState({ searchQuery: value.toLowerCase() });
}}
/>
<List
items={this.state.resources
.filter(resource => {
return resource.armId.resourceName.toLowerCase().includes(this.state.searchQuery || "");
})
.filter((resource => {
return (this.state.subscriptionIds || []).includes(resource.armId.subscription);
}))
}
onRenderCell={onRenderCell}
getItemCountForPage={() => this.state.resources?.length}
/>
</>;
}
}
Congratulations, you should now be ready to begin building any pure ReactView experience you would like. For more advanced examples that leverage redux, or third party npm packages, see some of the other guides.