- Browse - Table of Contents
- Browse
- Start with Asset Type Definition
- Browse with Azure Resource Graph
- Benefits to Tracking Resources with Azure Resource Graph
- Onboarding an asset to ARG
- KQL Query
- PDL Definition
- Adding a Query to PDL
- Adding Custom columns
- Column Formats
- Source Units for Number Columns
- Column Icons
QueryBladeLink
Column FormatDeepLink
Column Format- Possible Summary Visualizations
- Controlling column visibility via per-environment configuration
- Default columns
- Exclude columns
- Default filters
- Full Asset Browse definition
- Adding an informational info box with optional link to ARG browse
- Releasing the Azure Resource Graph experience
- Column Summaries for Extension-provided Columns
- Extensible commanding for ARG browse
- Merging Resource Types and Kinds
- Curating browse assets
- Providing a Custom Browse Hub
- Custom browse blade
- Customization of Browse for Resources not available in Azure Resource Graph
- Launching Browse Programmatically
The Favorites in the left nav and the 'All services' menu are the primary ways to launch tools and services within the portal. The default favorites are determined by C+E leadership based on the highest grossing services with the most engaged customers. New services will start in the 'All services' menu and, based on those metrics or the number of favorites surpasses other defaults, the list can be updated.
The most optimized and efficient method for browse is to on board tracked resources into the Azure Resource Graph (ARG) resources tables. The flexibility and features for filtering and summarizing resource in the resource group tables is far superior for those resources that can be onboarded. If the resources cannot be onboarded, there is a fallback to use Azure Resource Manager (ARM) instead.
The first step to having an entry in the 'All services' menu and in global search which can launch the browse experience is to define an asset type and set the options to use ARG browse. If ARG browse is not available, for ARM browse, the asset type is still required, so lets start there.
If you already have an asset type and wish to update to use ARG, you can skip ahead to this section: Azure Resource Graph
If you need to use a custom blade for complete control, once the asset type is ready, skip to this section: Custom browse blade
Lastly, if you need to use ARM browse for your resources, once the asset type is ready, skip to this section: Azure Resource Manager
Browse automatically queries the Azure Resource Graph (ARG) or Azure Resource Manager (ARM) for resources of a specific type and displays them in a grid. The performance and experience benefits of moving to utilize the Azure Resource Graph are outlined below and all new tracked resources should be implementing browse with ARG. Follow the instructions outlined here to set up browse for resources and then extend that to use browse using ARG in the section here Azure Resource Graph. If you already have an asset type using ARM browse, please follow the instructions in the Azure Resource Graph section to upgrade your browse experience today.
In this section we will explore the following:
- Define an asset type in PDL
- Specify the resource type
- Indicate that it should be visible in Browse
- Specify the API version that hubs extension should use to call ARM for the resource type
- Specify the browse type
That's it, you can see an example of that below
DX.json:
"assetType": {
"name": "Book",
//...
"browse": {
"type": "ResourceType",
//...
},
"resourceType": {
"name": "Microsoft.Press/books",
"apiVersion": "2016-01-01"
}
}
Legacy PDL
<AssetType Name="Book" ... >
<Browse Type="ResourceType" />
<ResourceType ResourceTypeName="Microsoft.Press/books" ApiVersion="2016-01-01" />
</AssetType>
All asset types have the following requirements:
- The asset type blade must have a single
id
parameter that is the asset id - The asset type part must be the same as the blade's pinned part
Asset types that represent Azure Resource Manager (ARM) resource types also have the following requirements:
- The asset id must be the string resource id
- The ARM RP manifest should include a RP, resource type, and resource kind metadata
To define your asset type, simply add the following snippet to PDL:
DX.json:
"assetType": {
"name": "MyAsset",
"service": { "property": "MyAsset.service", "module": "../../ClientResources" },
"displayNames": {
"singular": { "property": "MyAsset.singular", "module": "../../ClientResources" },
"plural": { "property": "MyAsset.plural", "module": "../../ClientResources" },
"lowerSingular": { "property": "MyAsset.lowerSingular", "module": "../../ClientResources" },
"lowerPlural": { "property": "MyAsset.lowerPlural", "module": "../../ClientResources" }
},
"keywords": { "property": "MyAsset.keywords", "module": "../..ClientResources" },
"description": { "property": "MyAsset.description", "module": "../..ClientResources" },
"icon": { "file": "../../Svg/MyAsset.svg" },
"blade": "MyAssetBlade",
"part": "MyAssetPart",
"preview": true,
// ...
}
Legacy PDL
<AssetType
Name="MyAsset"
ServiceDisplayName="{Resource MyAsset.service, Module=ClientResources}"
SingularDisplayName="{Resource MyAsset.singular, Module=ClientResources}"
PluralDisplayName="{Resource MyAsset.plural, Module=ClientResources}"
LowerSingularDisplayName="{Resource MyAsset.lowerSingular, Module=ClientResources}"
LowerPluralDisplayName="{Resource MyAsset.lowerPlural, Module=ClientResources}"
Keywords="{Resource MyAsset.keywords, Module=ClientResources}"
Description="{Resource MyAsset.description, Module=ClientResources}"
Icon="{Resource Content.MyExtension.Images.myAsset, Module=./../_generated/Svg}"
BladeName="MyAssetBlade"
PartName="MyAssetPart"
IsPreview="true">
...
</AssetType>
The name can be anything, since it's scoped to your extension. You'll be typing this a lot, so keep it succinct, yet clear -- it will be used to identify asset types in telemetry. It is advised that the name not contain any white space.
In order to provide a modern voice and tone within the portal, asset types have 4 different display names. The portal will use the most appropriate display name given the context. If your asset type display name includes an acronym or product name that is always capitalized, use the same values for upper and lower display name properties (e.g. displayNames.plural
and displayNames.lowerPlural
in JSON (or PluralDisplayName
and LowerPluralDisplayName
in PDL) may both use SQL databases
). Do not share strings between singular and plural display name properties. The display names should use sentence casing where acronyms, trademark/product names and the first letter of the uppercase display name should be capitalized but the rest should be lower case, ie use "Virtual machine" instead of "Virtual Machine" or use "Azure SQL database" instead of "Azure SQL Database".
- The 'All services' (browse) menu shows the
service
display name in JSON (orServiceDisplayName
in PDL) in the list of browseable asset types. Ifservice
in JSON (orServiceDisplayName
in PDL) is not available,displayNames.plural
in JSON (orPluralDisplayName
in PDL) will be shown instead. - The All Resources blade uses the
displayNames.singular
in JSON (orSingularDisplayName
in PDL) in the Type column, when visible. - Browse uses the
displayNames.lowerPlural
in JSON (orLowerPluralDisplayName
in PDL) when there are no resources (e.g. "No web apps to display"). - Browse uses the
displayNames.lowerPlural
in JSON (orLowerPluralDisplayName
in PDL) as the text filter placeholder.
Filtering functionality within the 'All services' (browse) menu searches over keywords
in JSON (or Keywords
in PDL). Keywords
is a comma-separated set of words or phrases which
allow users to search for your asset by identifiers other than than the set display names.
For the type display name, the asset type can define the display name values. The way that the type display name is derived works like this:
- If there is a ServiceDisplayName on the asset type, use that.
- Otherwise if there is a CompositeDisplayName (or individual display names) on the asset type, use the appropriate case and plurality of that value.
- If there are no display names available, use the raw resource type string.
Remember, your part and blade should both have a single id
input parameter, which is the resource id:
/**
* View model for the asset tile.
*/
@TemplatePart.Decorator({
supportedSizes: [
TemplatePart.Size.Normal,
TemplatePart.Size.Wide,
TemplatePart.Size.Large,
],
initialSize: TemplatePart.Size.Wide,
htmlTemplate: "Tile.html",
styleSheets: ["Tile.css"],
forAsset: {
assetIdParameter: "id",
assetType: "MyAsset",
},
})
@TemplatePart.InjectableModel.Decorator(DataContext)
export class MyAssetPart {
// Required. Must be the only input parameter.
public context: TemplatePart.Context<{ id: string }, DataContext>;
// ...
}
/**
* View model for the asset blade.
*/
@TemplateBlade.Decorator({
htmlTemplate: "Blade.html",
styleSheets: ["Blade.css"],
forAsset: {
assetIdParameter: "id",
assetType: "MyAsset",
},
})
@TemplateBlade.Pinnable.Decorator()
@TemplateBlade.InjectableModel.Decorator(DataContext)
export class MyAssetBlade {
// Required. Must be the only input parameter.
public context: TemplateBlade.Context<{ id: string }, DataContext>;
// ...
}
Legacy PDL
If still using older PDL part and tile definitions:
<Part Name="MyAssetPart" ViewModel="MyAssetPartViewModel" AssetType="MyAsset" AssetIdProperty="id" ...>
<Part.Properties>
<!-- Required. Must be the only input parameter. -->
<Property Name="id" Source="{DataInput Property=id}" />
</Part.Properties>
<BladeAction Blade="MyAssetBlade">
<BladeInput Source="id" Parameter="id" />
</BladeAction>
...
</Part>
<Blade Name="MyAssetBlade" ViewModel="MyAssetBladeViewModel" AssetType="MyAsset" AssetIdProperty="id">
<Blade.Parameters>
<!-- Required. Must be the only input parameter. -->
<Parameter Name="id" Type="Key" />
</Blade.Parameters>
<Blade.Properties>
<Property Name="id" Source="{BladeParameter Name=id}" />
</Blade.Properties>
...
</Blade>
If your asset type is in preview, set the preview=true
in JSON (or IsPreview="true"
in PDL) property. If the asset type is GA, simply remove the property (the default is false
).
You can hide or show your asset in different environments by setting the assettypeoptions extension feature flag in your config which is a rich object structure which allows changing not only asset types, but also hiding and showing asset instances in browse and global search as well as hiding and showing asset instances with a specific resource kind. This supersedes the legacy hideassettypes extension feature flag.
{
"assettypeoptions": {
"YOUR_ASSET_NAME": { "options": "HideAssetType" },
"YOUR_OTHER_ASSET_NAME": { "options": "HideAssetType,HideInstances" },
"YOUR_THIRD_ASSET_NAME": { "options": "" }
}
}
The "options" value is a comma-separated list of options which will be applied to the asset type:
Options | Result |
---|---|
HideAssetType | Hides the asset type from the All services left navigation |
HideInstances | Hides any instances of the asset type in browse all resources and global search |
HideAssetType,HideInstance | Hide the asset type from left navigation AND hides any instances in browse and global search |
empty string | This will show the asset type in left navigation AND shows instances in browse and global search |
Importantly, if HideInstances is used on an asset type, using resource type-specific browse will show those instances in that browse.
**IMPORTANT**
The case of empty string strips off any visibility options provided in PDL. The options are applied to the asset type, essentially replacing the options in PDL.
As mentioned above, visibility of instances with a specific resource kind can also be controlled if the kind is specified in the PDL:
{
"assettypeoptions": {
"YOUR_ASSET_NAME": { "options": "HideAssetType" },
"YOUR_OTHER_ASSET_NAME": { "options": "HideAssetType,HideInstances" },
"YOUR_THIRD_ASSET_NAME": { "options": "" },
"YOUR_ASSET_WITH_KINDS_NAME": { "kinds": { "KIND_NAME": { "options": "HideInstances" } } }
}
}
In this example, the instances of the asset type 'YOUR_ASSET_WITH_KINDS_NAME' which have the kind of 'KIND_NAME' will be hidden. Note that the 'HideAssetType' option does not apply to kind, only the 'HideInstances' option.
There is also no way to show hidden asset types or hide / show specific kinds using the old configuration flags, so please move to 'assettypeoptions'.
This now reads the config JSON file for the appropriate environment, so follow the same procedure as for the Hosting service next.
If you’re using the hosting service, you can do this by updating your domainname.json (e.g. portal.azure.cn.json file)
{
"assettypeoptions": {
"YOUR_ASSET_NAME": { "options": "HideAssetType" },
"YOUR_OTHER_ASSET_NAME": { "options": "HideAssetType,HideInstances" },
"YOUR_THIRD_ASSET_NAME": { "options": "" },
"YOUR_ASSET_WITH_KINDS_NAME": { "kinds": { "KIND_NAME": { "options": "HideInstances" } } }
}
}
IMPORTANT These flags cannot be mixed with the legacy 'hideassettypes' flag. If the config provides an 'assettypeoptions' flag, 'hideassettypes' flag will be ignored. This is one reason that using 'assettypeoptions' is preferred and use of 'hideassettypes' should be deprecated.
Testing your hidden asset
To test enable your hidden asset for testing purposes, you will need to update the hide asset feature flag to exclude the asset you want to show and ensure you have feature.canmodifyextensions set.
For the desired environment append the following feature flags.
If you want to test showing all hidden assets, you can specify all the assets as a comma separated list to the 'showassettypes' extension feature flag.
?microsoft_azure_mynewextension_assettypeoptions={"MyNewAsset":{"options":""},"MySecondNewAsset":{"options":""}}
or testing the hiding of an asset can be achieved with: https://rc.portal.azure.com/?microsoft_azure_compute_assettypeoptions={"VirtualMachine":{"options":"HideAssetType"}}µsoft_azure_compute=true&feature.canmodifyextensions=true
There is a definitive recipe for how visibility options are applied to asset types and kinds from the various sources of PDL, config JSON files and via overrides on the URL. The PDL has the lowest priority and should be considered the defaults. Whatever options you apply in PDL will be already applied to the asset type. Once the portal loads, the config JSON file for the appropriate domain is loaded and overrides are applied from there. The options flags are replaced by the flags from config. The 'assettypeoptions' are applied if present, otherwise 'hideassettypes' are applied if present. Note that if both are supplied, only the 'assettypeoptions' will be used. After that, the URL extension feature flag will have the highest priority and will be applied last. Again, if the 'MyExtensionName_assettypeoptions' feature flag is in the URL, it will be applied. Otherwise, if the 'MyExtensionName_showassettypes' feature flag is present, it will be applied. Otherwise, if the 'MyExtensionName_hideassettypes' feature flag is present, it will be applied. Note that the only asset types affected are those in the config or feature flag. Other asset types will not be affected:
- PDL flags are baked into the asset type definition at compile time.
- Config is applied:
- If the 'assettypeoptions' is present, apply any changes, jump to step 5
- If the 'hideassettypes' is present, apply any changes, jump to step 5 - this is considered legacy and should be replaced with assettypeoptions.
- URL overrides are applied:
- If the 'MyExtensionName_assettypeoptions' feature flag is present, apply any changes, jump to end
- If the 'MyExtensionName_showassettypes' feature flag is present, apply any changes, jump to end - this is considered legacy and should be replaced with assettypeoptions.
- If the 'MyExtensionName_hideassettypes' feature flag is present, apply any changes, jump to end - this is considered legacy and should be replaced with assettypeoptions.
As shown, if 'assettypeoptions' and 'hideassettypes' are all present in the config, the 'hideassettypes' will be ignored. The 'hideassettypes' flag is considered legacy and should be replaced with 'assettypeoptions'.
Also, if 'assettypeoptions', 'showassettypes' and 'hideassettypes' are all present in the URL, the 'showassettypes' and 'hideassettypes' will be ignored and if only 'showassettypes' and 'hideassettypes' are specified, 'hideassettypes' will be ignored. Both 'showassettypes' and 'hideassettypes' are both considered to be legacy and should be replaced with 'assettypeoptions'.
If the resource you wish to expose does not have kinds then please skip to the next topic.
ARM has the capability for a resource to define kinds, in some cases you may want to treat those kinds separately in the portal.
To define a kind for your asset, you need to declare the kind as a child of the ResourceType
within PDL. Firstly you will need to specify a default kind,
this kind inherits the blade/part defined in the Asset. The default kind is identified with "default"=true
in JSON (or IsDefault="true"
in PDL).
If your resource exposes multiple kinds you can declare them as siblings of the default kind.
Exposing your kind within the 'All services' menu will require your kind/asset to be curated within the Portal Framework. The framework also offers ways for grouping kinds together when browsing to those kinds. There are two options you can use group your kinds:
- Kind groups
- This will define a separate kind group within your extensions definition which can be used as a way to define a single view for multiple kinds while also keeping the individual kind view. In JSON, the kind object should include a
groupedKinds
property which is the array of references to other kinds. In PDL, useKindGroup
instead ofKind
for the group and include theKindReferences
.
- This will define a separate kind group within your extensions definition which can be used as a way to define a single view for multiple kinds while also keeping the individual kind view. In JSON, the kind object should include a
- Merged kinds
- Similar to kind groups merged kinds will group various kinds together and present them in a single view, except merged kind forces any instance of the individual kinds to be viewed as the merged view. In JSON, the kind object should include a
mergedKinds
property which is the array of kind objects to be merged together. In PDL useMergedKind
instead ofKind
for the merged kinds and includeKind
entries inside.
- Similar to kind groups merged kinds will group various kinds together and present them in a single view, except merged kind forces any instance of the individual kinds to be viewed as the merged view. In JSON, the kind object should include a
DX.json:
// This asset type represents a watch instance.
// An asset type represents an asset object in the system independent of other objects in the system.
// It represents a singular class of objects distinctively but without connection to other objects.
// This asset type includes a resource type which represents a watch instance in the resource map.
// A resource type represents an asset specifically in a resource map where the connections between
// objects is important. It represents a way to map resources in a resource map to the underlying
// assets in the system.
// It includes the resource map icons which are used in the resource map control.
// Watch is an "abstract" asset type, there is no such thing as a "watch", the default
// watch type is a "apple" watch. Other specializations are based on function.
"assetType": {
"name": "Watch",
"displayNames": "AssetTypeNames.Watch",
"viewModel": { "name": "WatchViewModel", "module": "AssetType/Watch" },
"icon": { "file": "../../Svg/Watches/generic.svg" },
"part": "AppleWatchTile",
"blade": "AppleWatchBlade",
"create": { "marketplacePackageId": "Microsoft/watch" },
"resourceMenu": { "resourceProvidedBy": "ProvidedByResourceMenu" },
"browse": {
"type": "ResourceType",
"query": { "file": "WatchQuery.kml" },
"defaultColumns": ["model", "status"],
"customConfig": { "useSupplementalData": true },
"columns": [
{
"name": "model",
"displayName": "Columns.Watch.model",
"description": "Columns.Watch.modelDescription",
"format": "String",
"width": "90fr"
},
{
"name": "status",
"displayName": "Columns.Watch.status",
"description": "Columns.Watch.statusDescription",
"format": "Status",
"width": "80fr",
"iconColumn": "statusIcon"
}
]
},
"resourceType": {
"name": "Microsoft.Test/watches",
"apiVersion": "2017-04-01",
"kinds": [
{
"name": "apple",
"default": true,
"displayNames": "AssetTypeNames.Watch.Apple",
"icon": { "file": "../../Svg/Watches/apple.svg" },
"create": { "marketplacePackageId": "Microsoft/applewatch" }
},
{
"name": "astro",
"displayNames": "AssetTypeNames.Watch.Astro",
"icon": { "file": "../../Svg/Watches/astro.svg" },
"part": "AstroWatchTile",
"blade": "AstroWatchBlade"
},
{
"name": "lg",
"displayNames": "AssetTypeNames.Watch.LG",
"icon": { "file": "../../Svg/Watches/lg.svg" },
"preview": true,
"part": "LgWatchTile",
"blade": "LgWatchBlade"
},
{
"name": "samsung",
"displayNames": "AssetTypeNames.Watch.Samsung",
"icon": { "file": "../../Svg/Watches/samsung.svg" },
"preview": true,
"part": "SamsungWatchTile",
"blade": "SamsungWatchBlade"
},
{
"name": "fitbit",
"displayNames": "AssetTypeNames.Watch.Fitbit",
"icon": { "file": "../../Svg/Watches/fitbit.svg" },
"part": "FitbitWatchTile",
"blade": "FitbitWatchBlade"
},
{
"name": "android",
"displayNames": "AssetTypeNames.Watch.Android",
"icon": { "file": "../../Svg/Watches/android.svg" },
"groupedKinds": ["lg", "samsung"]
},
{
"name": "garmin-merged",
"mergedKinds": [
{
"name": "garmin",
"displayNames": "AssetTypeNames.Watch.Garmin",
"icon": { "file": "../../Svg/Watches/garmin.svg" },
"part": "GarminWatchTile",
"blade": "GarminWatchBlade"
},
{
"name": "garmin2",
"displayNames": "AssetTypeNames.Watch.Garmin2",
"icon": { "file": "../../Svg/Watches/garmin2.svg" },
"part": "Garmin2WatchTile",
"blade": "Garmin2WatchBlade"
}
]
},
{
"name": "fitness",
"displayNames": "AssetTypeNames.Watch.Fitness",
"icon": { "file": "../../Svg/Watches/fitness.svg" },
"groupedKinds": ["apple", "lg", "fitbit", "garmin-merged"]
}
]
}
}
Legacy PDL
<!--
This asset type represents a watch instance.
An asset type represents an asset object in the system independent of other objects in the system.
It represents a singular class of objects distinctively but without connection to other objects.
This asset type includes a resource type which represents a watch instance in the resource map.
A resource type represents an asset specifically in a resource map where the connections between
objects is important. It represents a way to map resources in a resource map to the underlying
assets in the system.
It includes the resource map icons which are used in the resource map control.
Watch is an "abstract" asset type, there is no such thing as a "watch", the default
watch type is a "apple" watch. Other specializations are based on function.
-->
<AssetType Name="Watch"
ViewModel="{ViewModel Name=WatchViewModel, Module=./Watch/AssetType/Watch}"
CompositeDisplayName="{Resource AssetTypeNames.Watch, Module=ClientResources}"
Icon="{Svg File=../../Svg/Watches/generic.svg}"
BladeName="AppleWatchBlade"
PartName="AppleWatchTile"
MarketplaceItemId="Microsoft/watch">
<Browse Type="ResourceType"
UseCustomConfig="true"
UseSupplementalData="true" />
<ResourceMenu ResourceProvidedBy="NoResource" />
<ResourceType ResourceTypeName="Microsoft.Test/watches"
ApiVersion="2017-04-01">
<Kind Name="apple"
IsDefault="true"
CompositeDisplayName="{Resource AssetTypeNames.Watch.Apple, Module=ClientResources}"
Icon="{Svg File=../../Svg/Watches/apple.svg}" />
<Kind Name="lg"
CompositeDisplayName="{Resource AssetTypeNames.Watch.LG, Module=ClientResources}"
Icon="{Svg File=../../Svg/Watches/lg.svg}"
BladeName="LgWatchBlade"
PartName="LgWatchTile" />
<Kind Name="samsung"
CompositeDisplayName="{Resource AssetTypeNames.Watch.Samsung, Module=ClientResources}"
Icon="{Svg File=../../Svg/Watches/samsung.svg}"
BladeName="SamsungWatchBlade"
PartName="SamsungWatchTile" />
<Kind Name="fitbit"
CompositeDisplayName="{Resource AssetTypeNames.Watch.Fitbit, Module=ClientResources}"
Icon="{Svg File=../../Svg/Watches/fitbit.svg}"
BladeName="FitbitWatchBlade"
PartName="FitbitWatchTile" />
<!--
The 'android' kind group wraps the lg and samsung kinds into a single kind. The 'android' kind is an abstract
kind. There should never be a watch with the kind set to 'android'. Instead it's used to group kinds into
a single list. However, 'lg' watches and be seen separately, same with 'samsung' watches. The 'android' kind
will be emitted to the manifest as a kind.
-->
<KindGroup Name="android"
CompositeDisplayName="{Resource AssetTypeNames.Watch.Android, Module=ClientResources}"
Icon="{Svg File=../../Svg/Watches/android.svg}">
<KindReference KindName="lg" />
<KindReference KindName="samsung" />
</KindGroup>
<!--
The 'garmin-merged' kind has two merged kinds, 'garmin' and 'garmin2'. The 'garmin-merged' kind is not a real
kind and is not emitted to the manifest as a kind, it is organizational only.
-->
<MergedKind Name="garmin-merged">
<Kind Name="garmin"
CompositeDisplayName="{Resource AssetTypeNames.Watch.Garmin, Module=ClientResources}"
Icon="{Svg File=../../Svg/Watches/garmin.svg}"
BladeName="GarminWatchBlade"
PartName="GarminWatchTile" />
<Kind Name="garmin2"
CompositeDisplayName="{Resource AssetTypeNames.Watch.Garmin2, Module=ClientResources}"
Icon="{Svg File=../../Svg/Watches/garmin2.svg}"
BladeName="Garmin2WatchBlade"
PartName="Garmin2WatchTile" />
</MergedKind>
</ResourceType>
</AssetType>
For the type display name, the kind can override the values on the asset type. The way that the type display name is derived works like this:
- If there is a kind which matches the resource's kind, use the kind's ServiceDisplayName if present.
- Otherwise, if the kind's CompositeDisplayName is present (or individual display names), use the appropriate case and plurality of that value.
- If there kind has not display name or no kind is found or there is no kind, fall back to the asset type display name.
- If there is a ServiceDisplayName on the asset type, use that.
- Otherwise if there is a CompositeDisplayName (or individual display names) on the asset type, use the appropriate case and plurality of that value.
- If there are no display names available, use the raw resource type string.
Just as the type display name, blade, part and icon can be overridden per kind in the Kind entries, visibility of the kind and instances of resources of the kind can be overridden using the Options property on the Kind.
{
"assettypeoptions": {
"YOUR_ASSET_NAME": { "kinds": { "apple": { "options": "ShowInstances" }, "lg": { "options": "HideAssetType" } } }
}
}
The "options" value is a comma-separated list of options which will be applied to the asset type:
Options | Result |
---|---|
HideAssetType | Hides any curated kind-based asset type entry from the All services left navigation |
ShowAssetType | Shows any curated kind-based asset type entry in the All services left navigation - used to show a kind where the parent asset type is hiding the asset type (override) |
HideInstances | Hides any instances of the asset type with the kind in browse all resources and global search |
ShowInstances | Shows any instances of the asset type with the kind in browse all resources and global search - used to show instances of a resource with the kind where the parent asset type is hiding instances (override) |
HideAssetType,HideInstance | Hide the asset type from left navigation AND hides any instances in browse and global search |
HideAssetType,ShowInstance | Hide the asset type from left navigation BUT shows any instances in browse and global search |
ShowAssetType,HideInstance | Show the asset type in left navigation BUT hides any instances in browse and global search |
ShowAssetType,ShowInstance | Show the asset type in left navigation AND shows any instances in browse and global search |
empty string | This will show the asset type in left navigation AND shows instances in browse and global search |
Importantly, if HideInstances is used on a kind, using resource type-specific browse with a kind filter will show those instances in that browse.
**IMPORTANT**
Choosing the visibility options for the asset type and kinds is very powerful and configurable to ensure customers see what they should be seeing.
First, if your resource type has no kinds (or the kinds do not affect visibility), it is fairly straight-forward to choose the options.
Entry in All Services / Global Search | Show Resources in Browse All / Global Search | Options to use |
---|---|---|
Show entry | Show resources | no Options, empty string OR "ShowAssetType,ShowInstances" |
Show entry | Hide resources | "HideInstances" OR "ShowAssetType,HideInstances" |
Hide entry | Show resources | "HideAssetType" OR "HideAssetType,ShowInstances" |
Hide entry | Hide resources | "HideAssetType,HideInstances" |
However, if you have Kind entries with different visibility needs, it is a bit more complicated to choose the options for the asset type and the kinds. The visibility of the asset type entry and the instances is split into two charts, but the flags can be combined separated by a comma to achieve the end result.
Asset type entry in All Services / Global Search | Kind entry in All Services / Global Search | Asset Type Options | Kind Options |
---|---|---|---|
Show entry | Show entry | no Options OR empty | no Options OR empty |
Show entry | Hide entry | no Options OR empty | "HideInstances" |
Hide entry | Show entry | "HideInstances" | "ShowInstances" |
Hide entry | Hide entry | "HideInstances" | no Options OR empty |
Show Resources in Browse All / Global Search | Show Resources with Kind in Browse All / Global Search | Asset Type Options | Kind Options |
---|---|---|---|
Show resources | Show resources | no Options OR empty | no Options OR empty |
Show resources | Hide resources | no Options OR empty | "HideInstances" |
Hide resources | Show resources | "HideInstances" | "ShowInstances" |
Hide resources | Hide resources | "HideInstances" | no Options OR empty |
So, as an example, if you wanted to show the asset type in the all services menu but hide instances in Browse All AND has a kind which you wanted to hide in the all services menu but show instances of the kind in Browse All, the asset type would have the Options "HideInstances" (since ShowAssetType is implied) and the Kind would have the Options "HideAssetType,ShowInstances".
But utilizing the Options (which can be set per-environment in the config JSON files), there exists a powerful mechanism to show and hide resources with and without kinds to provide the appropriate experience for your customers.
To allow people to create new resources from Browse, you can associate your asset type with a Marketplace item or category:
DX.json:
"assetType": {
"name": "Book",
"create": {
"marketplacePackageId": "Microsoft.Book"
// or "marketplaceMenuItemId": "menu/book"
},
// ...
"browse": { "type": "ResourceType" },
"resourceType": { "name": "Microsoft.Test/book", "apiVersion": "2021-02-01" }
}
Legacy PDL
<AssetType
Name="Book"
MarketplaceItemId="Microsoft.Book"
<!-- or MarketplaceMenuItemId="menu/book" -->
...>
<Browse Type="ResourceType" />
<ResourceType ResourceTypeName="Microsoft.Press/books" ApiVersion="2016-01-01" />
</AssetType>
The Browse blade will launch the Marketplace item, if specified; otherwise, it will launch the Marketplace category blade for the specific
menu item id (e.g. gallery/virtualMachines/recommended
for Virtual machines > Recommended). To determine the right Marketplace category,
contact the Marketplace team. If neither is specified, the Add command won't
be available.
The framework offers the ability to display a description and links in the case that the users filters return no results.
NOTE: This will also display if the user visits the browse experience and they have not yet created the given resource.
To opt in to this experience you need to provide a description
and a link
, these are properties that you provide on your Asset.
DX.json:
"assetType": {
"name": "MyAsset",
// ...
"description": { "property": "MyAsset.description", "module": "../..ClientResources" },
// ...
"links": [
{
"title": { "property": "MyAsset.linkTitle1", "module": "../../ClientResources" },
"uri": "http://www.bing.com"
},
{
"title": { "property": "MyAsset.linkTitle2", "module": "../../ClientResources" },
"uri": "http://www.bing.com"
}
]
// ...
}
Legacy PDL
<AssetType
Name="MyAsset"
...
Description="{Resource MyAsset.description, Module=ClientResources}">
...
<Link Title="{Resource MyAsset.linkTitle1, Module=ClientResources}" Uri="https://www.bing.com"/>
<Link Title="{Resource MyAsset.linkTitle2, Module=ClientResources}" Uri="https://www.bing.com"/>
...
</AssetType>
If you aren’t familiar Azure Resource Graph, it’s a new service which provides a query-able caching layer over ARM. This gives us the capability to sort, filter, and search server side which is a vast improvement on what we have today.
There are compelling reasons to use the Azure Resource Graph for your tracked resources.
- Performance
- The general performance of using Azure Resource Graph is greatly improved as ARG is at heart an indexing service for your ARM resources. This includes paging and server-side filtering support.
- Query-able
- Allows for customized query options to provide rich data to customers and allows the browse blade to provide a richer experience to the customer.
- Summary Views
- The nature of ARG as an indexing and query-able layer allows the browse blade to provide rich summary views to give the customer rich visual representations of their resources.
- Future
- The ARG browse path will be receiving almost all the new features moving forward while the ARM browse path will only be receiving maintenance work moving forward.
What that does mean though is we won’t be loading extensions to gather the extra, supplemental, column data. Instead that will all be served via ARG.
Due to which there the following required from extension authors to onboard.
- Define the columns which you wish to expose
- Craft the query to power your data set
- To craft the query you can use the in portal advanced query editor Azure Resource Graph Explorer
- Ensure the query projects all the framework and extension expected columns
- Onboard your given asset via PDL
- If you haven't created an Asset follow the previous documentation on how to do that
- Choose how to expose the ARG experience
Note: the below contains the PDL, Columns definitions, and Query required to match to an existing AppServices browse experience.
Firstly you'll need to craft a KQL query which represents all possible data for your desired browse view, this includes the required framework columns.
Display name | Expected Column Name | PDL Reference | Default Width |
---|---|---|---|
Name | name | N/A - Injected as the first column | 200fr |
Resource Id | id | FxColumns.ResourceId | 100fr |
Subscription | N/A | FxColumns.Subscription | 100fr |
SubscriptionId | subscriptionId | FxColumns.SubscriptionId | 100fr |
Resource Group | resourceGroup | FxColumns.ResourceGroup | 100fr |
Resource Group Id | N/A | FxColumns.ResourceGroupId | 100fr |
Location | location | FxColumns.Location | 100fr |
Location Id | N/A | FxColumns.LocationId | 100fr |
Resource Type | N/A | FxColumns.ResourceType | 100fr |
Type | type | FxColumns.AssetType | 100fr |
Kind | kind | FxColumns.Kind | 100fr |
Tags | tags | FxColumns.Tags | 100fr |
NOTE: to avoid maintaining this list and to ensure your asset type will automatically get new framework columns, it is advisable to use the [FxColumns]
placeholder in your final project statement:
project [FxColumns], ...your columns here
For those who are not familiar with KQL you can use the public documentation as reference. https://docs.microsoft.com/en-us/azure/kusto/query/
Given the framework columns are required we can use the below as a starting point.
- Go to the Azure Resource Graph explorer
- Copy and paste the below query
- Update the
where
filter to your desire type
where type =~ 'microsoft.web/sites'
| project [FxColumns]
That query is the bare minimum required to populate ARG browse.
As you decide to expose more columns you can do so by using the logic available via the KQL language to extend
and then project
them in the query.
One common ask is to convert ARM property values to user friendly display strings, the best practice to do that is to use the case
statement in combination
with extending the resulting property to a given column name.
In the below example we're using a case
statement to rename the state
property to a user friendly display string under the column status
.
We're then including that column in our final project statement. We can then replace those display strings with client references once we migrate it over to
PDL in our extension providing localized display strings.
where type =~ 'microsoft.web/sites'
| extend state = tolower(properties.state)
| extend status = case(
state == 'stopped', 'Stopped',
state == 'running', 'Running',
'Other')
| project [FxColumns]
As an example the below query can be used to replicate the 'App Services' ARM based browse experience in ARG.
where type =~ 'microsoft.web/sites'
| extend appServicePlan = extract('serverfarms/([^/]+)', 1, tostring(properties.serverFarmId))
| extend appServicePlanId = properties.serverFarmId
| extend state = tolower(properties.state)
| extend sku = tolower(properties.sku)
| extend pricingTier = case(
sku == 'free', 'Free',
sku == 'shared', 'Shared',
sku == 'dynamic', 'Dynamic',
sku == 'isolated', 'Isolated',
sku == 'premiumv2', 'PremiumV2',
sku == 'premium', 'Premium',
'Standard')
| extend status = case(
state == 'stopped', 'Stopped',
state == 'running', 'Running',
'Other')
| extend appType = case(
kind contains 'botapp', 'Bot Service',
kind contains 'api', 'Api App',
kind contains 'functionapp', 'Function App',
'Web App')
| project [FxColumns],appServicePlanId,pricingTier,status,appType
In your extension you'll have a "assetType"
property declared in JSON (or tag declared in legacy PDL) which represents your ARM resource. In order to enable Azure Resource Graph (ARG) support for that asset we'll need to update the "browse"
property to include a reference to the query
, defaultColumns
, excludeColumns
, defaultFilters
, and custom column meta data - if you have any.
Create a new file, we'll use AppServiceQuery.kml
, and save your query in it.
You can update any display strings with references to resource files using following syntax '{{Resource name, Module=ClientResources}}'
. This will allow
for localization of the display strings in your .resx file.
The following is an example using the resource reference syntax.
where type == 'microsoft.web/sites'
| extend appServicePlanId = properties.serverFarmId
| extend state = tolower(properties.state)
| extend sku = tolower(properties.sku)
| extend pricingTier = case(
sku == 'free',
'{{Resource pricingTier.free, Module=BrowseResources}}',
sku == 'shared',
'{{Resource pricingTier.shared, Module=BrowseResources}}',
sku == 'dynamic',
'{{Resource pricingTier.dynamic, Module=BrowseResources}}',
sku == 'isolated',
'{{Resource pricingTier.isolated, Module=BrowseResources}}',
sku == 'premiumv2',
'{{Resource pricingTier.premiumv2, Module=BrowseResources}}',
sku == 'premium',
'{{Resource pricingTier.premium, Module=BrowseResources}}',
'{{Resource pricingTier.standard, Module=BrowseResources}}')
| extend status = case(
state == 'stopped',
'{{Resource status.stopped, Module=BrowseResources}}',
state == 'running',
'{{Resource status.running, Module=BrowseResources}}',
'{{Resource status.other, Module=BrowseResources}}')
| extend appType = case(
kind contains 'botapp',
'{{Resource appType.botapp, Module=BrowseResources}}',
kind contains 'api',
'{{Resource appType.api, Module=BrowseResources}}',
kind contains 'functionapp',
'{{Resource appType.functionapp, Module=BrowseResources}}',
'{{Resource appType.webapp, Module=BrowseResources}}')
| project [FxColumns], appServicePlanId, pricingTier, status, appType
To define a custom column you will need to create a columns
property within your browse
object in JSON (or add a Column
entry in PDL within your Browse
entry in PDL).
A column has 5 required properties.
DX.json:
"columns": [
{
"name": "status",
"displayName": { "property": "Columns.status", "module": "../../ClientResources" },
"description": { "property": "Columns.statusDescription", "module": "../../ClientResources" },
"format": "String",
"width": "90fr"
}
]
Legacy PDL
<Column Name="status"
DisplayName="{Resource Columns.status, Module=ClientResources}"
Description="{Resource Columns.statusDescription, Module=ClientResources}"
Format="String"
Width="90fr" />
Property | Description |
---|---|
name | The identifier which is used to uniquely refer to your column |
displayName | A display string, this has to be a reference to a resource |
lowerDisplayName | A lowercase display string, this has to be a reference to a resource |
description | A description string, this has to be a reference to a resource |
format | See below table for possible formats |
width | String, which represents the default width of the column (e.g. "120fr" - fractional units or "100px" - pixels) |
sortColumn | Optional name of a separate column returned by the query for sorting. If the column returned as Name is formatted, the SortColumn can be used to return a sortable format of the value (possibly original value) for sorting in the grid |
sourceUnits | Optional source units for a Number format column used to render the best appropriate units for the value (ie, bytes, KB, MB, etc.). See below table for possible units |
maximumFractionDigits | Optional precision for a Number format column if the column might show fraction digits - often useful when using SourceUnits |
blade | Optional blade reference for a BladeLink format column to specify the blade to launch when the link is clicked. Required for BladeLink format columns |
bladeParameterColumn | Optional parameter column for a BladeLink format column to specify the parameters for the blade. Required for BladeLink format columns. See note below * |
openBladeAsContextPane | Optional boolean to open a BladeLink or QueryBladeLink format column blade in the context pane. Default is to open as a blade, use true to open in context pane |
iconColumn | Optional name of separate column returned by the query for the icon for a Status format column. See notes about icons below . Required for Status format columns |
preventSummary | Optional flag when summary (visualization) of the column should be prevented |
columnQueryForSummary | Optional column query for the summarization for this column used for the summary query drilldown |
summaryQuery | Optional summarization query for this column if normal count() summarization is not appropriate. When the ColumnQueryForSummary property is set, that is prepended to the summary query |
summaryColumn | Optional column name to be used for the summary for this column. This is only valid with there is a summary query. The value column should have this name and the count column should have this name with the 'Column' suffix. For example, if the SummaryColumn is 'mySummary', the query value column should be 'mySummary' and the count column should be 'mySummaryCount' |
summaryVisualizations | Optional summary visualizations for the column. If not set, standard bar and donut charts along with grid (list) are used. See below table for possible summary visualizations . Comma-delimited list of possible values |
Note for BladeParameterColumn
:
- If this is set and the result is a string, the column name will be the parameter name with that value.
- If this is set and the result is an object, that object will be the entire parameters for the blade.
Format option | Description |
---|---|
String | String rendering of your column |
Resource | If the returned column is a ARM resource id, this column format will render the cell as the resources name and a link to the respective blade |
Date | Date rendering of your column |
Number | Number rendering of your column, can use the SourceUnits to help formatting (ie, bytes, kilobytes, megabytes) and MaximumFractionDigits to format a maximum precision for numbers with a fraction portion |
Location | String representation of an ARM location code localized for the user's locale (column should return location ID) |
BladeLink | A blade link column which allows the user to launch a blade represented by Blade using parameters returned by the BladeParameterColumn |
QueryBladeLink | A blade link column which allows the user to launch a blade where the text, blade/extension and parameters come from the query (see below) |
DeepLink | A deep link column which allows the user to launch a deep link where the text and link come from the query (see below) |
Tenant | String representation of an ARM tenant ID from the display name for the tenant (column should return tenant ID) |
Status | String rendering of your column with an icon which is return by IconColumn . Currently only StatusBadge icons are supported (see list below) |
The delineated sections below show possible appropriate units in groups (ie, 20,000 metric bytes will show as 20 KB and 1,363,148 SI bytes will show as 1.3 GB).
Unit | Description |
---|---|
None | No units - same as not including units |
- | - |
Percentage | Source is in percentage |
- | - |
Bytes | Source is in metric bytes (divisor of 1000) |
Kilobytes | Source is in metric kilobytes (divisor of 1000) |
Megabytes | Source is in metric megabytes (divisor of 1000) |
Gigabytes | Source is in metric gigabytes (divisor of 1000) |
Terabytes | Source is in metric terabytes (divisor of 1000) |
Petabytes | Source is in metric petabytes (divisor of 1000) |
- | - |
BytesPerDay | Source is in metric bytes per day (divisor of 1000) |
BytesPerHour | Source is in metric bytes per hour (divisor of 1000) |
BytesPerMinute | Source is in metric bytes per minute (divisor of 1000) |
BytesPerSecond | Source is in metric bytes per second (divisor of 1000) |
KilobytesPerSecond | Source is in metric kilobytes per second (divisor of 1000) |
MegabytesPerSecond | Source is in metric megabytes per second (divisor of 1000) |
GigabytesPerSecond | Source is in metric gigabytes per second (divisor of 1000) |
TerabytesPerSecond | Source is in metric terabytes per second (divisor of 1000) |
PetabytesPerSecond | Source is in metric petabytes per second (divisor of 1000) |
- | - |
Count | Source is a count |
Thousand | Source is a count in thousands |
Million | Source is a count in millions |
Billion | Source is a count in billions |
Trillion | Source is a count in trillions |
- | - |
MicroSeconds | Source is in microseconds |
MilliSeconds | Source is in milliseconds |
Seconds | Source is in seconds |
Minutes | Source is in minutes |
Hours | Source is in hours |
Days | Source is in days |
- | - |
CountPerDay | Source is a count per day |
CountPerHour | Source is a count per hour |
CountPerMinute | Source is a count per minute |
CountPerSecond | Source is a count per second |
ThousandPerSecond | Source is a count in thousands per second |
MillionPerSecond | Source is a count in millions per second |
BillionPerSecond | Source is a count in billions per second |
TrillionPerSecond | Source is a count in trillions per second |
- | - |
Bytes_SI | Source is in SI bytes (divisor of 1024) |
Kilobytes_SI | Source is in SI kilobytes (divisor of 1024) |
Megabytes_SI | Source is in SI megabytes (divisor of 1024) |
Gigabytes_SI | Source is in SI gigabytes (divisor of 1024) |
Terabytes_SI | Source is in SI terabytes (divisor of 1024) |
Petabytes_SI | Source is in SI petabytes (divisor of 1024) |
- | - |
BytesPerDay_SI | Source is in SI bytes per day (divisor of 1024) |
BytesPerHour_SI | Source is in SI bytes per hour (divisor of 1024) |
BytesPerMinute_SI | Source is in SI bytes per minute (divisor of 1024) |
BytesPerSecond_SI | Source is in SI bytes per second (divisor of 1024) |
KilobytesPerSecond_SI | Source is in SI kilobytes per second (divisor of 1024) |
MegabytesPerSecond_SI | Source is in SI megabytes per second (divisor of 1024) |
GigabytesPerSecond_SI | Source is in SI gigabytes per second (divisor of 1024) |
TerabytesPerSecond_SI | Source is in SI terabytes per second (divisor of 1024) |
PetabytesPerSecond_SI | Source is in SI petabytes per second (divisor of 1024) |
Some columns require icons to be returned by the IconColumn
column of the query. Possible values
are outlined below.
When an icon is returned by the query, the query should use the {{Icon icon-name-here}}
escape sequence with no wrapping quotes as shown here:
where type == 'microsoft.web/sites'
| extend status = case(
state == 'stopped', '{{Resource status.stopped, Module=BrowseResources}}',
state == 'running', '{{Resource status.running, Module=BrowseResources}}',
'{{Resource status.other, Module=BrowseResources}}')
| extend statusIcon = case(
state == 'stopped', {{Icon StatusBadge.Stopped}},
state == 'running', {{Icon StatusBadge.Success}},
{{Icon StatusBadge.None}})
| project [FxColumns],status,statusIcon
Icon | Description |
---|---|
StatusBadge.Canceled | The canceled icon |
StatusBadge.Critical | The critical icon |
StatusBadge.Disabled | The disabled icon |
StatusBadge.Error | The error icon |
StatusBadge.Failed | The failed error icon |
StatusBadge.Info | The info alert icon |
StatusBadge.None | No icon |
StatusBadge.Pending | The pending icon |
StatusBadge.Stopped | The stopped icon |
StatusBadge.Success | The success or running icon |
StatusBadge.Unknown | The icon for unknown state |
StatusBadge.Update | The icon to update |
StatusBadge.Upsell | The icon for upsell |
StatusBadge.Warning | The warning icon |
The QueryBladeLink
column format expects the column to be formatted in a specific way. The column must be returned as a dynamic
from the Kusto query (essentially an object) with the following properties:
Property | Type | Usage | Description |
---|---|---|---|
text | string | required | The text to be displayed for the link in the grid. |
blade | string | required | The blade the will be opened by the link when activated. |
extension | string | optional | The extension that owns the blade if the blade is not owed by the extension that owns the asset type. |
parameters | object | required | The parameters (or inputs) for the blade. |
An important difference between a QueryBladeLink
column and a BladeLink
column is that the query link column allows the blade (and extension) to be specified by the query (dynamically) as opposed to being fixed in the definition. The cost of this is that there is no validation of the blade / extension at compile time, so extra care is needed to ensure there are no spelling mistakes or other mistakes that will case a runtime failure. Extra testing and integration tests should be used for validation.
| extend bladeLinkText = case(
type =~ "microsoft.compute/virtualmachines", "{{Resource AssetType.launchVm, Module=ClientResources}}",
type =~ "microsoft.compute/virtualmachinescalesets", "{{Resource AssetType.launchVmss, Module=ClientResources}}",
"{{Resource AssetType.launchGeneric, Module=ClientResources}}")
| extend bladeLinkBlade = case(
type =~ "microsoft.compute/virtualmachines", pack("blade", "VmBlade", "extension", "HubsExtension"),
type =~ "microsoft.compute/virtualmachinescalesets", pack("blade", "VmssBlade", "extension", "HubsExtension"),
pack("blade", "PropertiesBlade", "extension", "HubsExtension"))
| extend bladeLinkParameters = case(
type =~ "microsoft.compute/virtualmachines", pack("id", id, "sku", tostring(properties.sku)),
type =~ "microsoft.compute/virtualmachinescalesets", pack("id", id),
pack("id", id))
| extend bladeLink = pack(
"text", bladeLinkText,
"blade", bladeLinkBlade.blade,
"extension", bladeLinkBlade.extension,
"parameters": bladeLinkParameters)
| project [FxColumns],bladeLink
The DeepLink
column format expects the column to be formatted in a specific way. The column must be returned as a dynamic
from the Kusto query (essentially an object) with the following properties:
Property | Type | Usage | Description |
---|---|---|---|
text | string | required | The text to be displayed for the link in the grid. |
link | string | required | The deep link to be opened when the link is activated (this should include the hash # prefix and be appropriately encoded). |
An important difference between a #blade
DeepLink
column and BladeLink
or QueryBladeLink
column is that the deep link will clear the current journey. For this reason, BladeLink
or QueryBladeLink
column is almost always more appropriate to use than a #blade
DeepLink
column.
| extend deepLinkText = case(
type =~ "microsoft.compute/virtualmachines", "{{Resource AssetType.launchVm, Module=ClientResources}}",
type =~ "microsoft.compute/virtualmachinescalesets", "{{Resource AssetType.launchVmss, Module=ClientResources}}",
"{{Resource AssetType.launchGeneric, Module=ClientResources}}")
| extend deepLinkFragment = case(
type =~ "microsoft.compute/virtualmachines", strcat("#resource/", url_encode_component(id)),
type =~ "microsoft.compute/virtualmachinescalesets", strcat("#blade/Extension/Blade/id/", url_encode_component(id)),
strcat("#resource/", url_encode_component(id)))
| extend deepLink = pack(
"text", deepLinkText,
"link", deepLinkFragment)
| project [FxColumns],deepLink
Visualization | Description |
---|---|
Map | Shows a map with pins for the counts of values. Source column must be a Location format column |
BarChart | Shows a bar chart for the counts of values |
DonutChart | Shows a donut chart for the counts of values |
Grid | Shows a grid (list) for the counts of values |
Default | All possible visualizations excluding Map |
DefaultWithMap | All possible visualizations including Map |
There can be a case where for a particular environment, a column will not be valid or have a useable value. A specific feature behind a column may not be available or a linked resource might not be implemented or featured for a particular environment (like national clouds). To handle this case, browse columns can be hidden using a per-environment configuration JSON file. When a column is hidden, it is completely removed from the asset type at runtime. It will be removed from the default columns, optional columns, default filters and the status column. It will also be removed from the resource hover card. It will appear to browse as if the column does not exist in the PDL. The configuration JSON files have a 'showargcolumns' property which is a dictionary of asset type name to an inner dictionary of column name to a boolean value. Setting a column to false
will hide the column effectively removing it.
For example, if the special
column is not available in hosted.portal.azure.com, the following can be added to the hosted.portal.azure.com.json configuration file to hide that column:
{
"showargcolumns": {
"YOUR_ASSET_NAME": { "special": false },
"YOUR_SECOND_ASSET_NAME": { "anothercolumn": false, "thirdcolumn": false }
}
}
As can be seen, columns where the visibility is not affected do not need to be specified in the showargcolumns
, only those being hidden should be specified with a value of false
. Shown for the second asset type, multiple columns can be hidden by specifying more than one for the asset type.
**IMPORTANT**
To test enable your column (or hide it) for testing purposes, you can use the URL to override the configuration behavior.
For the desired environment append the following feature flags.
?microsoft_azure_mynewextension_showargcolumns={"YOUR_ASSET_NAME":{"special":true},"YOUR_SECOND_ASSET_NAME":{"fourthcolumn":false}}
For example to turn a hidden column on: https://rc.portal.azure.com/?microsoft_azure_compute_showargcolumns={"VirtualMachine":{"osdisk":true}}&feature.canmodifyextensions=true
or to turn an existing column off: https://rc.portal.azure.com/?microsoft_azure_compute_showargcolumns={"VirtualMachine":{"osdisk":false}}&feature.canmodifyextensions=true
To specify default columns you need to declare a property defaultColumns
on your browse
JSON object.
Default columns is a comma separated list of column names, a mix of custom columns and framework defined columns from the earlier table. All framework columns are prefixed with FxColumns.
.
For example "defaultColumns": ["status", "appType", "appServicePlanId", "FxColumns.Location"]
(or in legacy PDL: DefaultColumns="status, appType, appServicePlanId, FxColumns.Location"
).
You can specify excludeColumns
property on your browse
JSON object to indicate which default columns should be excluded from the browse experience. It can be used for example for tenant-level resources.
Exclude columns is an array of column names, where values can come from the set ["FxColumns.SubscriptionId", "FxColumns.ResourceGroup", "FxColumns.Location", "FxColumns.Tags"]
.
Excluded columns won't appear in the grid, Kusto query, filter pills, groupBy dropdown, visualizations and column chooser.
For example "excludeColumns": ["FxColumns.SubscriptionId", "FxColumns.ResourceGroup", "FxColumns.Location"]
.
By default, 3 filter pills are displayed in your Browse experience: Subscription, Resource Group and Location (unless some of these columns are excluded by excludeColumns
property).
If you want to render additional filter pills by default, you can achieve that by specifying defaultFilters
property.
defaultFilters
is an array of column names, accepting only values that are specified in columns
property.
For example "defaultFilters": ["status"]
.
In the above query example there are 4 custom columns, the below Asset PDL
declares the custom column meta data which each map to a column in the query above.
It also declares the default columns and their ordering for what a new user of the browse experience should see.
DX.json:
"assetType": {
// other asset type properties
"browse": {
"type": "ResourceType",
"query": { "file": "./MyAssetQuery.kml" },
"defaultColumns": ["status", "appType", "FxColumns.Location"],
"columns": [
{
"name": "status",
"displayName": { "property": "Columns.status", "module": "../../ClientResources" },
"description": { "property": "Columns.statusDescription", "module": "../../ClientResources" },
"format": "String",
"width": "90fr"
},
{
"name": "appType",
"displayName": { "property": "Columns.appType", "module": "../../ClientResources" },
"description": { "property": "Columns.appTypeDescription", "module": "../../ClientResources" },
"format": "String",
"width": "90fr"
}
],
"excludeColumns": ["FxColumns.ResourceGroup"],
"defaultFilters": ["status"]
},
"resourceType": {
"name": "Microsoft.Test/myresources",
"apiVersion": "2016-01-01"
}
}
Legacy PDL
<AssetType>
<!-- other asset type properties -->
<Browse
Type="ResourceType"
Query="{Query File=./MyAssetQuery.kml}"
DefaultColumns="status, appType, FxColumns.Location">
<Column Name="status"
DisplayName="{Resource Columns.status, Module=ClientResources}"
Description="{Resource Columns.statusDescription, Module=ClientResources}"
Format="String"
Width="90fr" />
<Column Name="appType"
DisplayName="{Resource Columns.appType, Module=ClientResources}"
Description="{Resource Columns.appTypeDescription, Module=ClientResources}"
Format="String"
Width="90fr" />
</Browse>
<ResourceType
ResourceTypeName="Microsoft.Test/myresources"
ApiVersion="2016-01-01" />
</AssetType>
If you need to display an informational message and/or link above the list of resources, add a infoBox
to your browse
in JSON (or BrowseInfoBox
to your Browse in PDL):
DX.json:
"assetType": {
"name": "MyAsset",
// other asset type properties
"browse": {
"type": "ResourceType",
"infoBox": {
"display": "MyAsset.upsellInfoBox",
"style": "Upsell",
"link": { "uri": "https://azure.microsoft.com" } // external link
}
},
"resourceType": {
"name": "Microsoft.Test/myresources",
"apiVersion": "2016-01-01"
}
}
or
"assetType": {
"name": "MyAsset",
// other asset type properties
"browse": {
"type": "ResourceType",
"infoBox": {
"display": "MyAsset.upsellInfoBox",
"style": "Upsell",
"blade": { "name": "BrowseAll", "extension": "HubsExtension" } // blade
}
},
"resourceType": {
"name": "Microsoft.Test/myresources",
"apiVersion": "2016-01-01"
}
}
Legacy PDL
<AssetType Name="MyAsset"
...>
<Browse Type="ResourceType">
<BrowseInfoBox Display="{Resource MyAsset.upsellInfoBox, Module=ClientResources}"
Style="Upsell">
<LinkTarget Uri="https://azure.microsoft.com" /> <!-- external link -->
</BrowseInfoBox>
</Browse>
<ResourceType ResourceTypeName="Microsoft.Test/myresources"
ApiVersion="2019-08-09" />
</AssetType>
or
<AssetType Name="MyAsset"
...>
<Browse Type="ResourceType">
<BrowseInfoBox Display="{Resource MyAsset.upsellInfoBox, Module=ClientResources}"
Style="Upsell">
<BladeTarget BladeName="BrowseAll" ExtensionName="HubsExtension" /> <!-- blade -->
</BrowseInfoBox>
</Browse>
<ResourceType ResourceTypeName="Microsoft.Test/myresources"
ApiVersion="2019-08-09" />
</AssetType>
In JSON, the blade
object must have a name
and optional extension
property. In PDL, the BladeTarget
entry must have a BladeName
and optional ExtensionName
(if the blade is from another extension) and can have an optional openBladeAsContextPane
to open the blade as a context pane (the blade must be capable of opening as a context pane).
The styles for the info box match the styles for the InfoBox control:
Style | Description |
---|---|
Default | The default info box style |
Info | The standard informational info box style |
Upsell | The style used for up-sell information |
Success | A success style (included for parity) |
Warning | A warning style (included for parity) |
Error | An error style (included for parity) |
Per Asset you can configure extension side feature flags to control the release of your assets Azure Resource Graph browse experience.
Within your extension config, either hosting service or self hosted, you will need to specify config for your assets with one of the following:
{
"argbrowseoptions": {
"YOUR_ASSET_NAME": "OPTION_FROM_THE_TABLE_BELOW",
}
}
Option | Definition |
---|---|
AllowOptIn | Allows users to opt in/out of the new experience but will default to the old experience. This will show a 'Try preview' button on the old browse blade and an 'Opt out of preview' button on the ARG browse blade. |
ForceOptIn | Allows users to opt in/out of the new experience but will default to the new experience. This will show a 'Try preview' button on the old browse blade and an 'Opt out of preview' button on the ARG browse blade |
Force | This will force users to the new experience. There wil be no 'Opt out of preview' button on the ARG browse blade |
Disable | This will force users to the old experience. This is the default experience if not flags are set. There wil be no 'Try preview' button on the ARG browse blade |
The default for simple asset types without customized columns OR asset types with a browse query but no browse options defined in 'Force' meaning customers will see the new experience. If you need to change that behavior, set the argbrowseoptions value appropriately to allow opt in/out and whether the default is to use the old experience or use the new experience. It is recommended to not use an option or set the option to 'Force' to give customers the best experience possible.
It is important to note that if a query is added to the asset type's browse, then the default option will be different due to how we onboarded ARG browse and partner extension asset types. Once the query is added, the default becomes 'AllowOptIn', it is advised that you change this to 'Force' to ensure your asset type is using ARG.
**IMPORTANT**
To test each variation or to test when side loading you can use:
https://portal.azure.com/?ExtensionName_argbrowseoptions={"assetName":"OPTION"}
There are summary views available on the browse blade out of the box for certain FX built-in columns to show a map (location-based columns), bar chart, donut chart and list (grid) such a location, resource group, type and subscription. These summary visualizations give customers a quick break-down of their resources by these properties in an easy to understand chart with counts to quickly reason over their resources.
In addition, we are also adding extension-provided columns for browse for a specific resource type. These are based on the Column
definitions in the PDL. The code uses the Format
as a hint to provide an appropriate summarization based on a simple count per value in that column. This will work for the vast majority of cases where the column is a string, location, number, datetime, etc. but may not be appropriate for all columns and we have added some customization points to prevent a column from being used in the summary views or to provide custom queries. As well, the PDL allows to specification about which views should be available for the column (map, bar chart, donut chart and/or grid).
There are cases where a column is simply not useful as a summary. The Column
can be marked with the PreventSummary
property:
DX.json:
"columns": [
{
"name": "someColumn",
"displayName": "Columns.someColumn",
"description": "Columns.someColumnDescription",
"format": "String",
"width": "80fr",
"preventSummary": true
}
]
Legacy PDL
<Column Name="someColumn"
DisplayName="{Resource Columns.someColumn, Module=ClientResources}"
Description="{Resource Columns.someColumnDescription, Module=ClientResources}"
Format="String"
Width="80fr"
PreventSummary="true" />
In addition, all columns with the Format
of BladeLink
, QueryBladeLink
and DeepLink
are excluded from summaries.
There are cases where the default visualizations for location-based columns (map, bar chart, donut chart and grid) or non-location-based columns (bar chart, donut chart and grid) are not desirable. The Column
can be marked with the SummaryVisualizations
property:
DX.json:
"columns": [
{
"name": "someColumn",
"displayName": "Columns.someColumn",
"description": "Columns.someColumnDescription",
"format": "String",
"width": "80fr",
"summaryVisualizations": ["DonutChart", "Grid"]
}
]
Legacy PDL
<Column Name="someColumn"
DisplayName="{Resource Columns.someColumn, Module=ClientResources}"
Description="{Resource Columns.someColumnDescription, Module=ClientResources}"
Format="string"
Width="80fr"
SummaryVisualizations="DonutChart,Grid" />
These are the available values (can be combined using comma as shown above):
Visualization | Definition |
---|---|
Map | Shows a map representation with clickable pins - only valid for location-based columns |
BarChart | Shows a bar chart with clickable columns |
DonutChart | Shows a donut chart with clickable sections |
Grid | Shows a grid with clickable rows - should always be included |
Default | This is the default of every visualization except Map - useful for non-location-based columns |
DefaultWithMap | The is the default of every visualization including Map - useful for location-based columns |
The order of the visualizations does not matter and will not change the order of the items in the drop down in browse.
If you have a column which doesn't map well to a straight count() of column
summarization, you can provide queries to change the summarization for the columns. If the summarization is based on an existing column (has a Column
value), only the summaryQuery
property needs to be set on the Column
:
DX.json:
"columns": [
{
"name": "someColumn",
"displayName": "Columns.someColumn",
"description": "Columns.someColumnDescription",
"format": "String",
"width": "80fr",
"summaryQuery": { "file": "QueryForSomeColumnSummary.kml" }
}
]
Legacy PDL
<Column Name="someColumn"
DisplayName="{Resource Columns.someColumn, Module=ClientResources}"
Description="{Resource Columns.someColumnDescription, Module=ClientResources}"
Format="string"
Width="80fr"
SummaryQuery="{Query File=./QueryForSomeColumnSummary.kml" />
Then the query file would include only the summarize portion of the query:
summarize someColumnCount=count() by bin(someColumn, 100)
The result of the count summarization must be the column name with Count
appended (someColumn
column must have someColumnCount
count).
If a new column must be generated for the summarize, however, then the columnQueryForSummary
property must point to a query which will produce (extend) that new column separately from the summaryQuery
because the summary view drill down blade will use that portion of the query to provide the list of resources that match the clicked value:
DX.json:
"columns": [
{
"name": "someColumn",
"displayName": "Columns.someColumn",
"description": "Columns.someColumnDescription",
"format": "String",
"width": "80fr",
"columnQueryForSummary": { "file": "SomeColumnQueryForSummary.kml" },
"summaryQuery": { "file": "SomeColumnSummaryQuery.kml" },
"summaryColumn": "someColumnSummaryColumn",
}
]
Legacy PDL
<Column Name="someColumn"
DisplayName="{Resource Columns.someColumn, Module=ClientResources}"
Description="{Resource Columns.someColumnDescription, Module=ClientResources}"
Format="string"
Width="80fr"
ColumnQueryForSummary="{Query File=./SomeColumnQueryForSummary.kml}"
SummaryQuery="{Query File=./SomeColumnSummaryQuery.kml}"
SummaryColumn="someColumnSummaryColumn" />
Then the column query file would include only the extend portion of the query:
extend someColumnSummaryColumn = case(
(someColumn < 50), 'low',
(someColumn <= 100), 'normal',
'above')
Then the summary query file would include only the summarize portion of the query:
summarize someColumnSummaryColumnCount=count() by someColumnSummaryColumn
The summaryColumn
property needs to be set to the name of the produced (extended) column and again, the count summarization must be the summary column name with Count
appended (summaryColumn
is someColumnSummaryColumn
and the count must be someColumnSummaryColumnCount
).
The Kusto summarize
operator requires that the summarize by
clauses must be scalar and sometimes additional processing needs to be done (like having case-insensitive summary). The way this is handled for non-customized columns is by extending a temporary column casted to a scalar, used in by the summarize and then projected away.
The column query file is often not needed and the summary query file would handle the casting:
extend _someColumn = tolower(tostring(someColumn))
| summarize someColumn=any(someColumn),someColumnCount=count() by _someColumn
| project-away _someColumn
In this query, first a temporary column (_someColumn
) is produced with the conversion to scalar (tostring()
) and then converted to lower case for case-insensitive summarization (tolower()
). Once the column is projected, the original value must be projected (someColumn=any(someColumn)
) along with the count (someColumnCount=count()
) otherwise the value column will be missing from the results. The by
clause uses the temporary projected column _someColumn
. Lastly, to prevent the additional temporary column from being returned, the _someColumn is projected away (project-away _someColumn
).
Extensible commanding enables you to author and own your resource-specific commands that allow users to manage their resources at scale with minimal efforts. Once you have onboarded to ARG browse experience, you can start authoring commands for your browse experience using Typescript. Currently, extension authors can statically define commands associated to their . Extensible commanding is only supported for asset types which are onboarded to ARG browse. The typescript decorator for command takes metadata required for creation and execution of the commands.
Extension authors can specify two sets of commands for their asset type. The generic commands that do not require resource selection (these will be enabled by default) and selection based commands that require resource selection (These will only be enabled if user has selected resources in the browse grid). The generic commands will be placed between Add
and Edit columns
command in the toolbar. The selection based commands will be displayed after Assign tags
command in the toolbar.
Simply specify a command kind and intellisense will prompt you for all the required properties for that command type. These are the currently supported command types:
import { SvgType } from "Fx/Images";
import { CommandVisibility } from "Fx/Assets";
export = MsPortalFxForAsset;
module MsPortalFxForAsset {
export module ForAsset {
export module Commands {
/**
* Command kinds.
*/
export const enum CommandKind {
/**
* Kind for the open blade commands.
*/
OpenBladeCommand,
/**
* Kind for the menu command.
*/
MenuCommand,
}
/**
* Selection Command kinds.
*/
export const enum SelectionCommandKind {
/**
* Kind for the open blade commands that require selection.
*/
OpenBladeSelectionCommand,
/**
* Kind for the ARM commands.
*/
ArmCommand,
/**
* Kind for the selection based menu command.
*/
MenuSelectionCommand,
}
/**
* Visibility options for selection menu commands.
*/
export const enum SelectionMenuCommandVisibility {
/**
* Allows a command to be hidden by default.
*
* NOTE: This is useful if you are experimenting with command bar layout and wish to only show a command via experimentation.
*/
HiddenByDefault = CommandVisibility.HiddenByDefault,
/**
* Allows a command to appear on browse toolbar.
*/
BrowseToolbar = CommandVisibility.BrowseToolbar,
/**
* Allows a command to appear on resource hover card.
*/
ResourceHoverCard = CommandVisibility.ResourceHoverCard,
}
/**
* Visibility options for selection commands.
*/
export const enum SelectionCommandVisibility {
/**
* Allows a command to be hidden by default.
*
* NOTE: This is useful if you are experimenting with command bar layout and wish to only show a command via experimentation.
*/
HiddenByDefault = CommandVisibility.HiddenByDefault,
/**
* Allows a command to appear on browse toolbar.
*/
BrowseToolbar = CommandVisibility.BrowseToolbar,
/**
* Allows a command to appear in browse context menu.
*
* NOTE: Only selection based commands with minSelection === 1 support this option.
* Menu commands do not support this option.
*/
BrowseContextMenu = CommandVisibility.BrowseContextMenu,
/**
* Allows a command to appear on resource hover card.
*/
ResourceHoverCard = CommandVisibility.ResourceHoverCard,
}
/**
* Visibility options for non selection commands.
*/
export const enum NonSelectionCommandVisibility {
/**
* Allows a command to be hidden by default.
*
* NOTE: This is useful if you are experimenting with command bar layout and wish to only show a command via experimentation.
*/
HiddenByDefault = CommandVisibility.HiddenByDefault,
/**
* Allows a command to appear on browse toolbar.
*/
BrowseToolbar = CommandVisibility.BrowseToolbar,
/**
* Allows a command to appear on empty browse view.
*/
BrowseEmptyView = CommandVisibility.BrowseEmptyView,
/**
* Allows a command to replace default "create" button on a service hover card.
*
* NOTE: Only one command with this flag is supported per asset type.
*/
ServiceHoverCard = CommandVisibility.ServiceHoverCard,
}
/**
* Defines the options that are passed to the command decorator.
*/
export interface CommandOptions {
/**
* The asset type that the commands are associated with.
*/
readonly assetType: string;
/**
* The list of commands which do no require resource selection.
*/
readonly commands?: ReadonlyArray<Command>;
/**
* The list of commands which require selection.
*/
readonly selectionCommands?: ReadonlyArray<SelectionCommand>;
}
/**
* Constrains the @ForAsset.Commands decorator so that it can be applied only to classes implementing 'Contract'.
*/
export interface Contract {
}
/**
* Constrains the @Commands decorator so that it can be applied only to classes implementing 'Contract'.
*/
export interface CommandsClass {
new(...args: any[]): Contract;
}
/**
* Decorator for Asset commands
* @param options command options
*/
export function Decorator(options?: CommandOptions) {
return function (commandsClass: CommandsClass) {
};
}
/**
* The blade reference options for open blade command.
*/
export interface BladeReference {
/**
* The blade name.
*/
readonly blade: string;
/**
* The flag indicating whether blade supports provisioning or not.
* Defaults to false.
*/
readonly doesProvisioning?: boolean;
/**
* The extension name for the blade
*/
readonly extension?: string;
/**
* The flag indicating whether blade needs to be opened as a context pane.
* Defaults to false.
*/
readonly inContextPane?: boolean;
/**
* The blade parameters.
*
* NOTE: Blades that require list of resourceIds in the parameters, should specify {resourceIds} as the parameter value.
* Fx will replace the {resourceIds} value with currently selected resource Ids at runtime.
*/
readonly parameters?: ReadonlyStringMap<any>;
}
/**
* The marketplace blade reference
*/
export interface MarketplaceBladeReference {
/**
* The marketplaceItemId to open a create flow.
*/
readonly marketplaceItemId?: string;
}
/**
* Interface for Open blade commands.
*/
export interface OpenBladeCommand extends CommonCommandBase<CommandKind.OpenBladeCommand> {
/**
* The blade reference.
* Either a reference to the blade or the marketpkace item id which opens the create flow needs to be specified.
*/
readonly bladeReference: BladeReference | MarketplaceBladeReference;
}
/**
* The interface for resource selection for commands.
*/
export interface RequiresSelection {
/**
* The resource selection for commands.
* Default selection is max allowed selection supported by browse grid.
*/
readonly selection?: Selection;
}
/**
* The interface for command execution confirmation options.
*/
export interface ConfirmationOptions {
/**
* The confirmation dialog title to show before execution of the command.
*/
readonly title: string;
/**
* The confirmation dialog message to show before execution of the bulk command.
*/
readonly message: string;
/**
* The confirmation text input.
* User needs to enter this text in order to confirm command execution.
*/
readonly explicitConfirmationText?: string;
}
/**
* The interface for commands that require user confirmation.
*/
export interface ConfirmationCommandBase {
/**
* The command execution confirmation options.
*/
readonly confirmation: ConfirmationOptions;
}
/**
* The interface for ARM command definition.
*/
export interface ArmCommandDefinition {
/**
* Http method POST/DELETE/PATCH etc. By default POST will be used.
*/
readonly httpMethodType?: string;
/**
* ARM uri for the command operation.
* Uri should be a relative uri with the fixed format - {resourceid}/optionalOperationName?api-version.
* Example: "{resourceid}?api-version=2018-09-01-preview
*/
readonly uri: string;
/**
* ARM command operation can be long running operation. asyncOperation property specifies how to poll the status for completion of long running operation.
*/
readonly asyncOperation?: AsyncOperationOptions;
/**
* Optional list of resource-specific ARM error codes that should be retried for HttpStatusCode.BadRequest(400).
*
* By default, Fx retries below codes:
* Retry for transient errors with Http status codes: HttpStatusCode.InternalServerError(500), HttpStatusCode.BadGateway(502), HttpStatusCode.ServiceUnavailable(503), HttpStatusCode.GatewayTimeout(504)
* Retry for ARM conflict/throttle errors with status codes: HttpStatusCode.TooManyRequests(409), HttpStatusCode.Conflict(429)
* In addition to these, there could be resource-specific errors that need to be retried for HttpStatusCode.BadRequest(400).
* If this list is specified, Fx will parse ARM error codes for HttpStatusCode.BadRequest(400) requests and retry in addition to above retries.
*
* Example: ["PublicIpAddressCannotBeDeleted", "InuseNetworkSecurityGroupCannotBeDeleted"]
*/
readonly retryableArmCodes?: ReadonlyArray<string>;
/**
* Optional list of resource-specific ARM error codes that shouldn't be retried.
* This helps optimize network calls and improve bulk operation performance.
*
* By default, Fx won't issue retry for below code regardless of HTTP status code:
* "ScopeLocked"
* In addition to this Arm error code, there could be resource-specific error codes that shouldn't be retried.
* If this list is specified, Fx will ignore the above mentioned list and only honor this list of Arm codes that shouldn't be retried.
*
* Example: ["ScopeLocked"]
*/
readonly nonRetryableArmCodes?: ReadonlyArray<string>;
}
/**
* Optional Arm command configs to describe how long running ARM operations needs to be polled and results processed.
*/
export interface AsyncOperationOptions {
/**
* By default when http Accepted (202) status code is received, the Location header will be looked up for polling uri to get the status of long running operation.
* A different response header can be specified with the pollingHeaderOverride value.
*/
readonly pollingHeaderOverride?: string;
/**
* A property path to look for status in the response body.
* By default 'status' property will be looked up to see if it has "Succeeded", "Failed", "InProgress" or "Canceled".
*/
readonly statusPath?: string;
}
/**
* The interface for ARM commands.
* These commands honor default selection which is FullPage.
*/
export interface ArmCommand extends CommonCommandBase<SelectionCommandKind.ArmCommand>, ConfirmationCommandBase {
/**
* The map of ARM bulk command definitions per resource type.
*
* NOTE: A command may delete multiple types of resources e.g. browse for merged resource types.
* In such cases, ARM command definition can be specified for each resource type.
*/
readonly definitions: ReadonlyStringMap<ArmCommandDefinition>;
/**
* The flag indicating whether to launch Fx bulk delete confirmation blade for delete operations.
*/
readonly isDelete?: boolean;
}
/**
* The interface for open blade commands that require resource selection.
*/
export interface OpenBladeSelectionCommand extends CommonCommandBase<SelectionCommandKind.OpenBladeSelectionCommand>, RequiresSelection {
/**
* The blade reference.
*/
readonly bladeReference: BladeReference;
}
/**
* The interface for menu command item display customizations.
*/
export interface MenuItemCustomCommandOptions {
/**
* The command content.
*
* Customizes rendering of menu command item @see `Toolbar.ToolbarItems.MenuItemCustomButtonOptions` for more details.
*/
readonly content?: string;
}
/**
* The interface for commands to specify visibility options.
*
* NOTE: Only applies to top level commands. i.e. Individual items in menu commands can't specify visibility options.
*/
export interface SelectionCommandVisibilityOptions {
/**
* The command visibility options.
* Specify one or more options in the format: `SelectionCommandVisibility.BrowseToolbar | SelectionCommandVisibility.BrowseContextMenu`.
*/
readonly visibility?: SelectionCommandVisibility;
}
/**
* The interface for commands to specify visibility options.
*
* NOTE: Only applies to top level commands. i.e. Individual items in menu commands can't specify visibility options.
*/
export interface NonSelectionCommandVisibilityOptions {
/**
* The command visibility options.
* Specify one or more options in the format: `NonSelectionCommandVisibility.BrowseToolbar | NonSelectionCommandVisibility.BrowseEmptyView`.
*/
readonly visibility?: NonSelectionCommandVisibility;
}
/**
* The interface for commands to specify visibility options.
*
* NOTE: Only applies to top level commands. i.e. Individual items in menu commands can't specify visibility options.
*/
export interface SelectionMenuCommandVisibilityOptions {
/**
* The command visibility options.
*/
readonly visibility?: SelectionMenuCommandVisibility;
}
/**
* The interface for selection based menu command.
*/
export interface MenuSelectionCommand extends CommonCommandBase<SelectionCommandKind.MenuSelectionCommand>, RequiresSelection {
/**
* The list of commands.
*/
readonly commands: ReadonlyArray<OpenBladeSelectionCommand & MenuItemCustomCommandOptions | ArmCommand>; //If a new command type is supported in future, it'll be added to this list depending on whether it needs to be supported in the menu list.
}
/**
* The interface for menu command.
*/
export interface MenuCommand extends CommonCommandBase<CommandKind.MenuCommand> {
/**
* The list of commands.
*/
readonly commands: ReadonlyArray<OpenBladeCommand & MenuItemCustomCommandOptions>;
}
/**
* The interface for commands that require resource selection.
*/
export type SelectionCommand = OpenBladeSelectionCommand & SelectionCommandVisibilityOptions | ArmCommand & SelectionCommandVisibilityOptions | MenuSelectionCommand & SelectionMenuCommandVisibilityOptions;
/**
* The interface for command.
*/
export type Command = OpenBladeCommand & NonSelectionCommandVisibilityOptions | MenuCommand & NonSelectionCommandVisibilityOptions;
/**
* The interface for command selection.
*/
export interface Selection {
/**
* The maximum number of selected resources supported by the command operation.
*/
readonly maxSelectedItems?: number;
/**
* The minimum number of selected resources supported by the command operation.
*/
readonly minSelectedItems?: number;
/**
* The message shown when user tries to select more than supported items by the command operation.
*/
readonly disabledMessage: string;
}
/**
* The interface for common command properties.
*/
export interface CommonCommandBase<TKind extends CommandKind | SelectionCommandKind> {
/**
* The command kind.
*/
readonly kind: TKind;
/**
* The command Id.
*/
readonly id: string;
/**
* The command label.
*/
readonly label: string;
/**
* The command icon.
*/
readonly icon: (
{
/**
* URI to the image element.
*/
path: string;
} | {
/**
* References a built-in SVG element.
*/
image: SvgType;
});
/**
* The command tooltip.
*/
readonly tooltip?: string;
/**
* The command aria label.
*/
readonly ariaLabel?: string;
}
}
}
}
Here is a sample of defining various asset commands, represented by a single TypeScript file in your extension project.
import { ForAsset } from "Fx/Assets/Decorators";
import * as ClientResources from "ClientResources";
import { AssetTypeNames } from "_generated/ExtensionDefinition";
import { SvgType } from "Fx/Images";
@ForAsset.Commands.Decorator({
assetType: AssetTypeNames.virtualServer, // Asset type name associated with commands
commands: [ // Generic commands that do not require resource selection
{
kind: ForAsset.Commands.CommandKind.OpenBladeCommand,
id: "OpenBladeCommandId", // Unique identifier used for controlling visibility of commands
label: ClientResources.AssetCommands.openBlade,
ariaLabel: ClientResources.AssetCommands.openBlade,
icon: {
image: SvgType.AddTile,
},
bladeReference: {
blade: "SimpleTemplateBlade",
extension: "SamplesExtension", // An optional extension name, however, must be provided when opening a blade from a different extension
inContextPane: true, // An optional property to open the pane in context pane
},
},
{
kind: ForAsset.Commands.CommandKind.OpenBladeCommand,
id: "OpenCreateCommandId",
label: ClientResources.AssetCommands.openCreate,
icon: {
image: SvgType.Cubes,
},
bladeReference: {
marketplaceItemId: "Microsoft.EngineV3", // Opens marketplace create flow
},
},
{
kind: ForAsset.Commands.CommandKind.OpenBladeCommand,
id: "OpenProvisioningBladeCommandId",
label: ClientResources.AssetCommands.customCreate,
icon: {
image: SvgType.PowerUp,
},
bladeReference: {
blade: "CreateCustomRobot.ReactView",
extension: "SamplesExtension",
doesProvisioning: true,
},
},
{
kind: ForAsset.Commands.CommandKind.MenuCommand,
id: "MenuCommand",
label: ClientResources.AssetCommands.menuCommand,
icon: {
image: SvgType.PolyResourceLinked,
},
commands: [
{
kind: ForAsset.Commands.CommandKind.OpenBladeCommand,
id: "OpenBladeCommand1",
label: ClientResources.AssetCommands.openBlade1,
icon: {
image: SvgType.AzureQuickstart,
},
content: ClientResources.infoBalloonContent, // Adds the subtitle property for the menu command item.
bladeReference: {
blade: "SimpleTemplateBlade",
},
},
{
kind: ForAsset.Commands.CommandKind.OpenBladeCommand,
id: "OpenBladeCommand2",
label: ClientResources.AssetCommands.openBlade2,
icon: {
image: SvgType.QuickStartPoly,
},
bladeReference: {
blade: "DiTemplateBlade",
},
},
],
visibility:
ForAsset.Commands.NonSelectionCommandVisibility.BrowseToolbar |
ForAsset.Commands.NonSelectionCommandVisibility.BrowseEmptyView |
ForAsset.Commands.NonSelectionCommandVisibility.ServiceHoverCard,
},
{
kind: ForAsset.Commands.CommandKind.OpenBladeCommand,
id: "OpenBladeCommandIdV2", // Unique identifier used for controlling visibility of commands
label: ClientResources.AssetCommands.openBlade,
ariaLabel: ClientResources.AssetCommands.openBlade,
icon: {
image: SvgType.AddTile,
},
bladeReference: {
blade: "SimpleTemplateBlade",
extension: "SamplesExtension", // An optional extension name, however, must be provided when opening a blade from a different extension
inContextPane: true, // An optional property to open the pane in context pane
},
visibility: ForAsset.Commands.NonSelectionCommandVisibility.HiddenByDefault, // Hide this command by defualt in all environments. Can be enabled via experiementation config for certain users.
},
],
selectionCommands: [ // Commands that require resource selection
{
kind: ForAsset.Commands.SelectionCommandKind.ArmCommand, // Executes ARM bulk operations
id: "BulkDelete",
label: ClientResources.AssetCommands.delete,
icon: {
image: SvgType.Delete,
},
definitions: {
"microsoft.test/virtualservers": {
httpMethodType: "DELETE",
uri: "{resourceid}?api-version=2018-09-01-preview", // The fixed format that starts with {resourceid}
asyncOperation: {
pollingHeaderOverride: "Azure-AsyncOperation",
},
},
},
isDelete: true, // Launches the default bulk delete confirmation blade provided by Fx on user click
confirmation: {
title: ClientResources.AssetCommands.confirmDeleteTitle,
message: ClientResources.AssetCommands.confirmDeleteMessage,
},
visibility: ForAsset.Commands.SelectionCommandVisibility.BrowseToolbar | ForAsset.Commands.SelectionCommandVisibility.BrowseContextMenu | ForAsset.Commands.SelectionCommandVisibility.ResourceHoverCard, // Show this command on browse toolbar and browse context menu.
},
{
kind: ForAsset.Commands.SelectionCommandKind.ArmCommand, // Executes ARM bulk operations
id: "BulkStart",
label: ClientResources.AssetCommands.start,
icon: {
image: SvgType.Start,
},
definitions: {
"microsoft.test/virtualservers": {
uri: "{resourceid}?api-version=2018-09-01-preview", // The fixed format that starts with {resourceid}
},
},
confirmation: {
title: ClientResources.AssetCommands.confirmStartTitle,
message: ClientResources.AssetCommands.confirmStartMessage,
},
},
{
kind: ForAsset.Commands.SelectionCommandKind.OpenBladeSelectionCommand,
id: "SelectionBasedOpenBladeCommand1",
label: ClientResources.AssetCommands.openBlade2,
icon: {
image: SvgType.Attachment,
},
bladeReference: {
blade: "SimpleTemplateBlade",
},
selection: {
maxSelectedItems: 3,
minSelectedItems: 3,
disabledMessage: ClientResources.AssetCommands.disabledMessage,
},
},
{
kind: ForAsset.Commands.SelectionCommandKind.MenuSelectionCommand,
id: "SelectionBasedMenuCommand",
label: ClientResources.AssetCommands.openBlade,
icon: {
image: SvgType.PolyResourceLinked,
},
selection: {
maxSelectedItems: 3,
minSelectedItems: 2,
disabledMessage: ClientResources.AssetCommands.disabledMessage,
},
commands: [
{
kind: ForAsset.Commands.SelectionCommandKind.OpenBladeSelectionCommand,
id: "SelectionBasedOpenBladeCommand",
label: ClientResources.AssetCommands.openBlade1,
icon: {
image: SvgType.AzureQuickstart,
},
content: ClientResources.infoBalloonContent, // Adds the subtitle property for the menu command item.
bladeReference: {
blade: "SimpleTemplateBlade",
},
},
{
kind: ForAsset.Commands.SelectionCommandKind.OpenBladeSelectionCommand,
id: "SelectionBasedOpenBladeCommand",
label: ClientResources.AssetCommands.openBlade2,
icon: {
image: SvgType.QuickStartPoly,
},
bladeReference: {
blade: "DiTemplateBlade",
},
},
],
},
],
})
export class VirtualServerCommands {
}
You can control visibility of individual or all your commands in different environments by setting the hideAssetTypeCommands extension feature flag in your config. You can specify a comma separated list of asset command ids or "*" to hide all the extensible commands on your browse blade
If you’re using the hosting service, you can do this by updating the relevant environment configuration file (e.g. portal.azure.cn.json file)
{
"hideAssetTypeCommands": {
"YOUR_ASSETTYPE_NAME_DEFINED_IN_PDL": ["YOUR_COMMAND_ID_TO_HIDE"],
"YOUR_ASSETTYPE_NAME_DEFINED_IN_PDL": ["YOUR_COMMAND_ID1_TO_HIDE", "YOUR_COMMAND_ID2_TO_HIDE"],
"YOUR_THIRD_ASSETTYPE_NAME_DEFINED_IN_PDL": ["*"]
}
}
For the desired environment append the following feature flags.
?microsoft_azure_myextension_hideassettypecommands={"MyAsset":["MyCommandId1", "MyCommandId2"]}
If you want to test hiding all your commands, you can specify ["*"].
?microsoft_azure_myextension_hideassettypecommands={"MyAsset":["*"]}
If you want to test commands by showing them locally that are hidden via config file changes, specify empty string [""]. This will ignore hidden commands and show all commands for a given asset type.
?microsoft_azure_myextension_hideassettypecommands={"MyAsset":[""]}
Portal now allows extension authors to integrate their extensible commands across various areas in portal such as empty browse view, context menu, service hover card and resource hover card.
You can use visibility
property on the command to specify areas in portal where the given command needs to be shown.
Here's a sample of a command that uses visibility
property which states that the command should appear on browse toolbar, context menu and resource hover card:
{
kind: ForAsset.Commands.SelectionCommandKind.ArmCommand, // Executes ARM bulk operations
id: "BulkDelete",
label: ClientResources.AssetCommands.delete,
icon: {
image: SvgType.Delete,
},
definitions: {
"microsoft.test/virtualservers": {
httpMethodType: "DELETE",
uri: "{resourceid}?api-version=2018-09-01-preview", // The fixed format that starts with {resourceid}
asyncOperation: {
pollingHeaderOverride: "Azure-AsyncOperation",
},
},
},
isDelete: true, // Launches the default bulk delete confirmation blade provided by Fx on user click
confirmation: {
title: ClientResources.AssetCommands.confirmDeleteTitle,
message: ClientResources.AssetCommands.confirmDeleteMessage,
},
visibility: ForAsset.Commands.SelectionCommandVisibility.BrowseToolbar | ForAsset.Commands.SelectionCommandVisibility.BrowseContextMenu | ForAsset.Commands.SelectionCommandVisibility.ResourceHoverCard, // Show this command on browse toolbar, browse context menu and resource hover card.
},
Notice that not all commands can support all the visibility options. e.g. you can not specify BrowseContextMenu as the visibility option for non selection commands as they are not resource specific.
Command type | BrowseContextMenu | BrowseToolbar | BrowseEmptyView | ResourceHoverCard | ServiceHoverCard |
---|---|---|---|---|---|
Non selection commands | N/A | Yes | Yes | N/A | Yes |
Non selection menu commands | N/A | Yes | Yes | N/A | Yes |
Selection commands | Yes | Yes | N/A | Yes | N/A |
Selection menu commands | N/A | Yes | N/A | Yes | N/A |
- All commands appear on BrowseToolbar by default unless explicitly hidden via config OR a command has visibility property specified which doesn't include
BrowseToolbar
- All selection (non menu) commands with minSelectedItems === 1 appear in context menu by default unless a command has visibility property specified which doesn't include
BrowseContextMenu
- All selection commands with minSelectedItems === 1 appear in resource hover cards by default unless a command has visibility property specified which doesn't include
ResourceHoverCard
Portal now supports experimenting with asset type commands in browse command bar by using Ibiza experimentation platform.
-
Extension authors can create an Experiment in Control Tower with a value that overrides their default browse commands.
- The variable name has to be a well-known string that uniquely identifies the asset type. The format should be of the form described below:
BrowseCommands-ExtensionNameAssetTypeName The variable name should start with
BrowseCommands-
followed by extension name and asset type name without any underscores. e.g. this would translate toBrowseCommands-MicrosoftAzureComputeVirtualMachines
for Virtual Machine resource type. - The variable must be created under
AzurePortal
namespace. - In the Control Tower, the value for above variable must be set to one of the keys of the map defined in step 2. which will determine the flight/progression user will see in the current session. i.e."commandBarLayout1" or "commandBarLayout2" or "commandBarLayout3"
- Extension authors must choose HubsExtension as the value for Extension filter while setting up the experiment.Configuring your experiment in Control Tower.
- Extension authors must specify the environment filter in Control Tower. Experimentation changes will only affect the environment based on this filter (e.g MPAC, RC).
- The variable name has to be a well-known string that uniquely identifies the asset type. The format should be of the form described below:
BrowseCommands-ExtensionNameAssetTypeName The variable name should start with
-
Extension authors define the map of different browse command bar layouts that are part of given experiment in their environmental config files. i.e. default.json
{
"assetTypeBrowseCommandsExperiments": {
"VirtualMachines": {
"commandBarLayout1": {
"commands": ["cmdId1", "cmdId2", "cmdId3"],
"selectionCommands": ["cmdId5", "cmdId6"]
},
"commandBarLayout2": {
"selectionCommands": ["cmdId5", "cmdId6"]
},
"commandBarLayout3": {
"commands": ["cmdId3", "cmdId1", "cmdId4"]
}
}
}
}
commands
array defines the layout for non selection based commands by specifying command ids. selectionCommands
array defines the selection based commands by specifying command ids. Extensions can decide to experiement with only one section of the toolbar i.e. either selection commands or non selection commands. Rest of the commands would be read from the default set of commands supplied by extension.
If you want to verify the command bar layout for a specific treatment variable value, it can be tested with query strings:
?exp.AzurePortal.BrowseCommands-MicrosoftAzureComputeVirtualMachines=commandBarLayout1
More info can be found here: (https://microsoft.sharepoint.com/teams/Ibizaexperimentation/SitePages/Code-integration-for-A-B-testing.aspx)
If you are looking to enable a new command in browse command bar only for certain users and want to hide it by default for rest of the users in all environments, use HiddenByDefault
visibility option when you define the command in your decorator. This visibility option will hide a given command across all areas where extensible commands are integrated such as browse context menu, hover cards and empty browse view.
{
kind: ForAsset.Commands.CommandKind.OpenBladeCommand,
id: "OpenBladeCommandIdV2", // Unique identifier used for controlling visibility of commands
label: ClientResources.AssetCommands.openBlade,
ariaLabel: ClientResources.AssetCommands.openBlade,
icon: {
image: SvgType.AddTile,
},
bladeReference: {
blade: "SimpleTemplateBlade",
extension: "SamplesExtension", // An optional extension name, however, must be provided when opening a blade from a different extension
inContextPane: true, // An optional property to open the pane in context pane
},
visibility: ForAsset.Commands.NonSelectionCommandVisibility.HiddenByDefault // Hide this command by default in all environments. Can be enabled via experimentation config for certain users.
},
In the environment config, you can specify this command id for one of your layouts and users hitting the flight with that experiment will only see the new command in browse command bar. e.g:
{
"assetTypeBrowseCommandsExperiments": {
"VirtualMachines": {
"commandBarLayout1": {
"commands": ["OpenBladeCommandIdV2", "cmdId2", "cmdId3"],
"selectionCommands": ["cmdId5", "cmdId6"]
},
"commandBarLayout2": {
"selectionCommands": ["cmdId5", "cmdId6"]
},
}
}
}
Often there are multiple resource types which may have a different namespace (RP) or type but from the customer's perspective are the same or closely related. To enforce that mental map of being similar, it is often advantageous to keep those resources together and show more than one resource type or a combination of resource type and kind in a singular browse list. The merged resource types feature allows this by allowing multiple resource types and/or kinds in a single browse list. Normally a browse list using the Azure Resource Graph is a list of a single resource type (or all resource types). By specifying additional merged resource types however, more than one resource type will appear in browse merged into a single list.
The simple case is where a new resource type should appear with an existing set of resources. In this case, simply add a new asset type for the new resource type and then add the resource type as a merged resource type to the browse of the original asset type. The new asset type is required for the type display name, the icon, the blade and any other associated properties for the new resource type.
DX.json:
"assetType": {
"name": "OriginalAsset",
// ...
"browse": {
"type": "ResourceType",
"query": { "file": "./OriginalAssetQuery.kml" },
"defaultColumns": ["status", "FxColumns.AssetType", "technology"],
"columns": [
// ...
]
},
"resourceType": {
"name": "Microsoft.Test/originalresources",
"apiVersion": "2020-01-31"
},
// ...
}
Legacy PDL
<AssetType Name="OriginalAsset"
...>
<Browse Type="ResourceType"
Query="{Query File=./OriginalAssetQuery.kml}"
DefaultColumns="status, FxColumns.AssetType, technology">
...
</Browse>
<ResourceType ResourceTypeName="Microsoft.Test/originalresources"
ApiVersion="2017-04-01" />
...
</AssetType>
Query:
resources
| where type =~ 'microsoft.test/originalresources'
// ...
| project [FxColumns], status, statusIcon, technology
Add the mergedResourceTypes
array to the browse
object in JSON (or add a MergedResourceType
entry to the Browse
entry in PDL). The name
property should be the full name like the name
property on the resourceType
object in JSON (or ResourceTypeName
property on the ResourceType
entry in PDL).
DX.json:
"assetType": {
"name": "OriginalAsset",
// ...
"browse": {
"type": "ResourceType",
"query": { "file": "./OriginalAssetQuery.kml" },
"defaultColumns": ["status", "FxColumns.AssetType", "technology"],
"columns": [
// ...
],
"mergedResourceTypes": [{ "name": "microsoft.test/newresources" }]
},
"resourceType": {
"name": "Microsoft.Test/originalresources",
"apiVersion": "2020-01-31"
},
// ...
}
Legacy PDL
<AssetType Name="OriginalAsset"
...>
<Browse Type="ResourceType"
Query="{Query File=./OriginalAssetQuery.kml}"
DefaultColumns="status, FxColumns.AssetType, technology">
...
<MergedResourceType ResourceTypeName="Microsoft.Test/newresources" />
</Browse>
<ResourceType ResourceTypeName="Microsoft.Test/originalresources"
ApiVersion="2020-01-31" />
...
</AssetType>
Updated Query:
resources
| where type =~ ('microsoft.test/originalresources', 'microsoft.test/newresources')
// ...
| project [FxColumns], status, statusIcon, technology
Using the 'in' operator is an efficient way to check for multiple resource types. If the columns need to be generated from different properties, be sure to use the extend
operator in the query to ensure valid values are used from both or if properties are only available from one resource type, it is advisable to extend
null into the column when appropriate.
Provide the new asset type for the new resource type. It is a good idea to merge in the original resource type from the new resource type (that way the merge will go both ways). Here we reuse the query from the original asset type since the query should be the same. Provide the new icon, localizable display names, blades and optional parts for the new asset type.
DX.json:
"assetType": {
"name": "NewAsset",
// ...
"browse": {
"type": "ResourceType",
"query": { "file": "./OriginalAssetQuery.kml" }, // reuse query
"defaultColumns": ["status", "FxColumns.AssetType", "technology"],
"columns": [
// ...
],
"mergedResourceTypes": [{ "name": "microsoft.test/originalresources" }]
},
"resourceType": {
"name": "Microsoft.Test/newresources",
"apiVersion": "2021-01-31"
},
// ...
}
Legacy PDL
<AssetType Name="NewAsset"
...>
<Browse Type="ResourceType"
Query="{Query File=./OriginalAssetQuery.kml}" // reuse query
DefaultColumns="status, FxColumns.AssetType, technology">
...
<MergedResourceType ResourceTypeName="Microsoft.Test/originalresources" />
</Browse>
<ResourceType ResourceTypeName="Microsoft.Test/newresources"
ApiVersion="2021-01-31" />
...
</AssetType>
If the new asset type should not appear in the all services menu as a separate entry, you can mark the new asset type as 'HideAssetType' using the asset type 'options' property in JSON (or 'Options' property in PDL).
It is possible to merge resources from another resource type which have a specific kind.
DX.json:
"assetType": {
"name": "OriginalAsset",
// ...
"browse": {
// ...
"mergedResourceTypes": [
{ "name": "microsoft.test/newresources", "kind": "bluekind" },
{ "name": "microsoft.Test/newresources2", "kind": "roundkind",
"additionalKinds": ["squarekind", "pentagonkind"] }
]
},
"resourceType": {
"name": "Microsoft.Test/originalresources",
"apiVersion": "2020-01-31"
},
// ...
}
Legacy PDL
<AssetType Name="OriginalAsset"
...>
<Browse ...>
...
<MergedResourceType ResourceTypeName="Microsoft.Test/newresources" ResourceKindName="bluekind" />
<MergedResourceType ResourceTypeName="Microsoft.Test/newresources2" ResourceKindName="roundkind">
<AdditionalKind ResourceKindName="squarekind" />
<AdditionalKind ResourceKindName="pentagonkind" />
</MergedResourceType>
</Browse>
<ResourceType ResourceTypeName="Microsoft.Test/originalresources"
ApiVersion="2020-01-31" />
...
</AssetType>
Also, as shown, it is possible to include multiple kinds by using the additionalKinds
property in JSON (or AdditionalKind
entries on the MergedResourceKind
entry in PDL).
By default, when browse sees any merged resource types, those types will be "filtered in" (shown) by default when browse first loads. However, it is possible to control the default visibility of the merged resource types. This is often useful when provide different virtual asset types where one asset type will show all resource types by default and another will selectively show resources. The user can alway use the type filter to alter visilibility from the initial view, but this allows the initial view to be controlled. The mergedResourceType
has a selected
property in JSON (or the MergedResourceType
entry has a Selected
property in PDL) which will control the initial visibility. In the following example, the original resources and the new resources will be shown by design, but the new resources 2 will not be shown until the user changes the type filter.
DX.json:
"assetType": {
"name": "OriginalAsset",
// ...
"browse": {
// ...
"mergedResourceTypes": [
{ "name": "microsoft.test/newresources", "selected": "true" },
{ "name": "microsoft.Test/newresources2", "selected": "false" }
]
},
"resourceType": {
"name": "Microsoft.Test/originalresources",
"apiVersion": "2020-01-31"
},
// ...
}
Legacy PDL
<AssetType Name="OriginalAsset"
...>
<Browse ...>
...
<MergedResourceType ResourceTypeName="Microsoft.Test/newresources" Selected="true" />
<MergedResourceType ResourceTypeName="Microsoft.Test/newresources2" Selected="false" />
</Browse>
<ResourceType ResourceTypeName="Microsoft.Test/originalresources"
ApiVersion="2020-01-31" />
...
</AssetType>
We have been asked a few times how to provide browse entries for different combinations of merged resource types, sometimes with different initial visibility, or providing different browse hub options. The best way of handling this situations to by using the concept of "virtual asset types". Virtual asset types are asset types with a "fake" or "virtual" resource type. That is, a resource type which does not actually exist in the given namespace (RP). These are useful for providing an all services (and global search) entry with specific merged resources.
An example of this would be a collection of related resources where there is a resource which is a child of a server where the server and the resource are closely related and there is a desire to show both resources together. However, the partner team would like to show the combined view (the "uber" view) from one all services entry and individual resources from separate entries (there may be existing entries and favorites which are important to maintain). Here is an example:
Asset Type / All Services Entry | Resources to Show |
---|---|
Operating Systems | Operating System Servers + Operating System Instances |
Operating System Servers | Operating System Servers |
Operating System Instances | Operating System Instances |
In this case, there should be three asset types - one virtual asset type for the Operating Systems entry and one real asset type for the Operating System Servers and Operating System Instances respectively. The Operating Systems virtual asset type needs to have a virtual resource type and our recommendation is that the resource type use the same (or the common) namespace and use a resource type that will not be added in the future. The other asset types would then use the concrete (or actual) resource types.
DX.json:
"assetType": {
"name": "OperatingSystem",
// ...
"browse": {
// ...
"mergedResourceTypes": [
{ "name": "microsoft.os/servers" }, // real resource type
{ "name": "microsoft.os/instances" } // real resource type
]
},
"resourceType": {
"name": "microsoft.os/virtual_operatingsystems", // virtual resource type (same namespace)
"apiVersion": "2021-01-31"
},
// ...
},
"assetType": {
"name": "OperatingSystemServer",
// ...
"browse": {
// ...
},
"resourceType": {
"name": "microsoft.os/servers", // real resource type
"apiVersion": "2020-01-31"
},
// ...
},
"assetType": {
"name": "OperatingSystemInstance",
// ...
"browse": {
// ...
},
"resourceType": {
"name": "microsoft.os/instances", // real resource type
"apiVersion": "2020-05-25"
},
// ...
}
Legacy PDL
<AssetType Name="OperatingSystem"
...>
<Browse ...>
...
<MergedResourceType ResourceTypeName="microsoft.os/servers" />
<MergedResourceType ResourceTypeName="microsoft.os/instances" />
</Browse>
<ResourceType ResourceTypeName="microsoft.os/virtual_operatingsystems"
ApiVersion="2021-01-31" />
...
</AssetType>
<AssetType Name="OperatingSystemServer"
...>
<Browse ...>
...
</Browse>
<ResourceType ResourceTypeName="microsoft.os/servers"
ApiVersion="2020-01-31" />
...
</AssetType>
<AssetType Name="OperatingSystemInstance"
...>
<Browse ...>
...
</Browse>
<ResourceType ResourceTypeName="microsoft.os/instances"
ApiVersion="2020-05-25" />
...
</AssetType>
The query for the virtual asset type should include resources with the two resource types but not the virtual resource type (though it should not matter given the resource type is virtual and should not have any isntances).
OperatingSystem.kml:
resources
| where type in~ ('microsoft.os/servers','microsoft.os/instances')
| project [FxColumns]
OperationSystemServer.kml:
resources
| where type =~ 'microsoft.os/servers'
| project [FxColumns]
OperationSystemInstance.kml:
resources
| where type =~ 'microsoft.os/instances'
| project [FxColumns]
When adding a new 'Asset' to your extension and exposing it through the 'All services' menu, by default it will be grouped in the 'Other' category. In order to get your 'Asset' correctly categorized you will need to make a request to the Portal Framework to curate your 'Asset'.
For the portal to correct curate your 'Asset' we will need the 'ExtensionName' and 'AssetName' as defined in your extension.
Please contact [email protected] with the following template:
Subject: 'Browse curation request - YourAssetName' Body:
- 'ExtensionName - YourExtensionName'
- 'AssetName - YourAssetName'
- 'KindName - YourKindName' (If applicable)
- 'Category - DesiredCategory'
- 'Portal environment - portal.azure.com (etc...)'
If you have a collection of related resources or browse items which are similar from the user's perspective, you can create a hub for the assets. Multiple asset types can point to the same hub and by using a menu blade, you can provide an entry point / overview for the resources, have each asset type point to a menu item with the browse experience for that asset type or provide an introduction page.
NOTE: One caveat to providing a hub page, your extension will need to be loaded to display the hub, so there is a slight performance hit. The default browse experience in the Hubs extension is preloaded and prewarmed to provide the fastest possible access, but this is not possible for each extension providing a custom browse hub. You need to weigh the performance hit versus the convenience to the user.
To provide a custom hub blade, simply add the DeepLink
property to the Browse
entry:
DX.json:
"assetType": {
"name": "Book",
// ...
"browse": {
"type": "ResourceType",
"deepLink": "#blade/MyExtension/BookMenuBlade"
},
Legacy PDL
<AssetType Name="Book"
...>
<Browse Type="ResourceType"
DeepLink="#blade/MyExtension/BookMenuBlade" />
</AssetType>
The DeepLink
must be a standard #blade
deep link to your blade and if your blade is a menu blade, the deep link can provide a menu item (which must be handled by your menu blade code):
DX.json:
"assetType": {
"name": "Manual",
// ...
"browse": {
"type": "ResourceType",
"deepLink": "#blade/MyExtension/BookMenuBlade/menuid/manuals"
},
Legacy PDL
<AssetType Name="Manual"
...>
<Browse Type="ResourceType"
DeepLink="#blade/MyExtension/BookMenuBlade/menuid/manuals" />
</AssetType>
The code for our sample menu blade can be seen here:
/**
* Menu blade for the books browse deep link.
*/
@MenuBlade.Decorator()
export class BookMenuBlade {
/**
* Menu blade view model properties.
*/
public title = ClientResources.AssetTypeNames.Book.plural;
public subtitle = "";
public context: MenuBlade.Context<Parameters>;
public viewModel: MenuBlade.ViewModel2;
/**
* Blade view model constructor.
*/
public onInitialize() {
const { container, parameters } = this.context;
this.viewModel = MenuBlade.ViewModel2.create(container, {
groups: [
{
id: "types",
displayText: ClientResources.AssetTypeNames.Book.plural,
items: [
{
id: "overview",
displayText: ClientResources.AssetTypeNames.Book.plural,
icon: Images.book,
supplyBladeReference: () => BladeReferences
.forBlade("BookOverview")
.createReference({
parameters: null,
}),
},
{
id: "manuals",
displayText: ClientResources.AssetTypeNames.Manual.plural,
icon: Images.manual,
supplyBladeReference: () => BladeReferences
.forExtension("HubsExtension")
.forBlade("ARGBrowseResourcesInMenu")
.createReference({
parameters: {
resourceType: "Microsoft.test/manuals",
},
}),
},
{
id: "novels",
displayText: ClientResources.AssetTypeNames.Novel.plural,
icon: Images.novel,
supplyBladeReference: () => BladeReferences
.forExtension("HubsExtension")
.forBlade("ARGBrowseResourcesInMenu")
.createReference({
parameters: {
resourceType: "Microsoft.test/novels",
},
}),
},
],
},
],
// The default menu item is the overview unless the deeplink had a 'menuid'.
defaultId: parameters.menuid || "overrview",
});
return Q(); // This sample loads no data.
}
}
If you don't have a list of resources and simply need to add a custom blade to Browse, you can define an asset type with a Browse
type of AssetTypeBlade
. This tells Browse to launch the blade associated with the asset type. Note that the asset type doesn't actually refer to an instance of a resource in this case. This is most common for services that are only provisioned once per directory or horizontal services (Cost Management, Monitoring, Azure Advisor etc...). In this case, the PluralDisplayName
is used in the 'All services' menu, but the other display names are ignored. Feel free to set them to the same value.
DX.json:
"assetType": {
"name": "CompanyLibrary",
"blade": "CompanyLibraryBlade",
// ...
"browse": { "type": "AssetTypeBlade" }
}
Legacy PDL
<AssetType
Name="CompanyLibrary"
BladeName="CompanyLibraryBlade"
... >
<Browse Type="AssetTypeBlade" />
</AssetType>
If your resource type is not available in Azure Resource Graph, you can still customize the browse experience. While there are compelling reasons to move your resource type to the Azure Resource Graph, if your resources are untracked, currently you cannot use the Azure Resource Group. Using ARM browse as a fallback is a possibility until more support for untracked resources is available.
By default, ARM Browse only shows the resource name, group, location, and subscription. To customize the columns, add a view-model to the AssetType
and indicate that you have custom Browse config:
<AssetType Name="Book" ViewModel="BookViewModel" ... >
<Browse Type="ResourceType" UseCustomConfig="true" />
<ResourceType ResourceTypeName="Microsoft.Press/books" ApiVersion="2016-01-01" />
</AssetType>
Now, create the asset view-model class that implements the getBrowseConfig()
function:
class BookViewModel implements ExtensionDefinition.ViewModels.ResourceTypes.BookViewModel.Contract {
public getBrowseConfig(): Promise<MsPortalFx.Assets.BrowseConfig> {
...
}
}
The getBrowseConfig()
function provides the following configuration options for your Browse blade:
columns
- List of custom columns the user will be able to choose to displaydefaultColumns
- List of column ids that will be used by defaultproperties
- Additional properties used by formatted columns (e.g. HTML formatting)
Start by specifying all possible custom columns you want to make available to customers using BrowseConfig.columns
. Browse will share the list of standard ARM columns and any custom columns you define with users and let them choose which columns they want to see.
To specify which columns to show by default, save the column ids to BrowseConfig.defaultColumns
. If any columns require additional data, like HTML-formatted columns that include 2 or more properties, save the additional property names (not the itemKey
) to BrowseConfig.properties
. Browse needs to initialize the grid with all the properties you'll use for supplemental data to ensure the grid will be updated properly.
class BookViewModel implements ExtensionDefinition.ViewModels.ResourceTypes.BookViewModel.Contract {
public getBrowseConfig(): Promise<MsPortalFx.Assets.BrowseConfig> {
return Q.resolve({
// columns the user will be able to choose to display
columns: [
{
id: "author",
name: ko.observable<string>(ClientResources.author),
itemKey: "author"
},
{
id: "genre",
name: ko.observable<string>(ClientResources.genre),
itemKey: "genre",
format: MsPortalFx.ViewModels.Controls.Lists.Grid.Format.HtmlBindings,
formatOptions: {
htmlBindingsTemplate: "<div data-bind='text: genre'></div> (<div data-bind='text: subgenre'></div>)"
}
}
],
// default columns to show -- name is always first
defaultColumns: [
ResourceColumns.resourceGroup,
"author",
"genre"
],
// additional properties used to support the available columns
properties: [
"subgenre"
]
});
}
}
Notice that the genre column actually renders 2 properties: genre and subgenre. Because of this, we need to add "subgenre" to the array of additional properties to ensure it gets rendered properly to the grid.
At this point, you should be able to compile and see your columns show up in your Browse blade. Of course, you still need to populate your supplemental data. Let's do that now...
In order to specify supplemental data to display on top of the standard resource columns, you'll need to opt in to specifying supplemental data in PDL:
<AssetType Name="Book" ViewModel="BookViewModel" ... >
<Browse Type="ResourceType" UseSupplementalData="true" />
<ResourceType ResourceTypeName="Microsoft.Press/books" ApiVersion="2016-01-01" />
</AssetType>
You'll also need to implement the supplementalDataStream
property and getSupplementalData()
function on your asset view-model:
class BookViewModel implements ExtensionDefinition.ViewModels.ResourceTypes.BookViewModel.Contract {
public supplementalDataStream = ko.observableArray<MsPortalFx.Assets.SupplementalData>([]);
public getBrowseConfig(): Promise<MsPortalFx.Assets.BrowseConfig> {
...
}
public getSupplementalData(resourceIds: string[], columns: string[]): Promise<any> {
...
}
}
After the Browse blade retrieves the first page of resources from ARM, it will call getSupplementalData()
with the batch of resource ids retrieved from ARM as well as the column ids currently being displayed in the grid. You'll then retrieve only the properties required to populate those columns for only the specified resource ids. Do not query all properties for all resources!
class BookViewModel implements ExtensionDefinition.ViewModels.ResourceTypes.BookViewModel.Contract {
private _container: MsPortalFx.ViewModels.ContainerContract;
private _dataContext: any;
private _view: any;
constructor(container: MsPortalFx.ViewModels.ContainerContract, initialState: any, dataContext: ResourceTypesArea.DataContext) {
this._container = container;
this._dataContext = dataContext;
}
...
public getSupplementalData(resourceIds: string[], columns: string[]): Promise<any> {
// NOTE: Do not create the view in the constructor. Initialize here to create only when needed.
this._view = this._view || this._dataContext.bookQuery.createView(this._container);
// connect the view to the supplemental data stream
MsPortalFx.Assets.SupplementalDataStreamHelper.ConnectView(
this._container,
view,
this.supplementalDataStream,
(book: Book) => {
return resourceIds.some((resourceId) => {
return ResourceTypesService.compareResourceId(resourceId, book.id());
});
},
(book: Book) => {
// save the resource id so Browse knows which row to update
var data = <MsPortalFx.Assets.SupplementalData>{ resourceId: book.id() };
// only save author if column is visible
if (columns.indexOf("author") !== -1) {
data.author = robot.author();
}
// if the genre column is visible, also add the subgenre property
if (columns.indexOf("genre") !== -1) {
data.genre = robot.genre;
data.subgenre = robot.subgenre;
}
return data;
});
// send resource ids to a controller and aggregate data into one client request
return view.fetch({ resourceIds: resourceIds });
}
}
NOTE: If you notice that some of the supplemental properties aren't being saved to the grid, double-check that the property names are either listed as the itemKey
for a column or have been specified in BrowseConfig.properties
. Unknown properties won't be saved to the grid.
NOTE: Do not pre-initialize data. Browse will show a loading indicator based on whether or not it's received data. If you initialize any supplemental data, this will inform the grid that loading has completed. Instead, leave cells empty when first displaying them.
Now, you should have supplemental data getting populated. Great! Let's add context menu commands...
If you need to display an informational message and/or link above the list of resources, add an infoBox
to your Browse config:
class BookViewModel implements ExtensionDefinition.ViewModels.ResourceTypes.BookViewModel.Contract {
public getBrowseConfig(): Promise<MsPortalFx.Assets.BrowseConfig> {
return Q.resolve({
infoBox: {
image: MsPortalFx.Base.Images.Info(),
text: resx.browseBookInfoBoxText,
// optionally specify a blade to launch when the infobox is clicked
blade: <MsPortalFx.ViewModels.DynamicBladeSelection>{
detailBlade: "BookInfoBlade",
detailBladeInputs: null
},
// ...or link to an external web page
uri: "https://microsoftpress.com"
// NOTE: Blade is preferred over link, if both are specified.
},
...
});
}
...
}
Context menu commands in Browse must take a single id
input parameter that is the resource id of the specific resource. To specify commands, add the name of the command group defined in PDL to Browse config:
<CommandGroup Name="BrowseBookCommands">
...
</CommandGroup>
class BookViewModel implements ExtensionDefinition.ViewModels.ResourceTypes.BookViewModel.Contract {
public getBrowseConfig(): Promise<MsPortalFx.Assets.BrowseConfig> {
return Q.resolve({
// NOTE: Extension (commandGroupOwner) only required if from another extension
contextMenu: {
commandGroup: "BrowseBookCommands",
commandGroupOwner: "<extension name>"
},
...
});
}
...
}
If you need to expose different commands based on some other metadata, you can also specify the the command group in SupplementalData.contextMenu
in the same way.
There are a few cases where code needs to programmatically open a browse blade. This can be done from a link or button on a blade or as a menu item in a menu blade (TOC). Given the complexity of reasoning over the feature flags, the asset type support and changing blade names, it is not advisable to try to guess or hard code the browse blade name. Instead, a new API available to both FX code and react views code has been added to call and let the shell determine that best appropriate blade using the same logic as the all services menu code.
const bladeReference = await getBrowseBladeReference({
resourceType: "some resource type here",
kind: "some resource type kind here", // kind is optional
inMenu: true, // inMenu is optional
});
// bladeReference.blade is the name of the blade
// bladeReference.extension is the name of the extension owning the blade
// bladeReference.parameters is the default parameters which should be passed to the blade as inputs
The bladeReference
parameters can be expanded for additional options like an initial view with the filters set appropriately.
The following resource types can be used to launch browse:
Resource type | Description |
---|---|
Microsoft.resources/resources |
Opens browse all resources |
Microsoft.resources/subscriptions/resourcegroups |
Open browse resource groups |
To use the getBrowseBladeReference()
API in FX code, first import the function:
import { getBrowseBladeReference } from "Fx/ResourceManagement";
Then simply call the API and await the result. If the resource type provided does not have a browse blade, the result will be null:
const browseBlade = await getBrowseBladeReference({
resourceType: "Microsoft.compute/virtualmachines",
});
if (browseBlade) {
bladeLink.bladeReference = new PdlBladeReference(
bladeReference.blade,
bladeReference.extension,
{
parameters: bladeReference.parameters,
});
}
To use the getBrowseBladeReference()
API in FX code, first import the function:
import { getBrowseBladeReference } from "Fx/ResourceManagement";
The when the supplyBladeReference is called for the menu item, return the blade reference.
**IMPORTANT**
const browseBlade = await getBrowseBladeReference({
resourceType: "Microsoft.compute/virtualmachines",
inMenu: true,
});
//... later in menu creation:
items: [
{
id: "browsevms",
displayText: ClientResources.Browse.launchBrowseVmsText,
icon: null,
supplyBladeReference: () => new PdlBladeReference(
bladeReference.blade,
bladeReference.extension,
{
parameters: bladeReference.parameters,
}),
},
To use the getBrowseBladeReference()
API in react view code, first import the function:
import { getBrowseBladeReference } from "@microsoft/azureportal-reactview/ResourceManagement";
Then simple call the API when a browse blade reference is need to open a browse blade:
const browseBlade = await getBrowseBladeReference({
resourceType: "Microsoft.compute/virtualmachines",
});
//... later to open the blade:
return (
<>
<BladeLink
bladeReference={browseBlade}
>{ClientResources.Browse.launchBrowseBladeText}</BladeLink>
</>
);