Added categories and search based filtering to the resource dialog. (#12658)

* added filtering to the resource type along with a new component.

* -Added caching of cards
-Removed unused component props
-localized tags
-limited the scope of list items

* Made some changes in the PR

* - Added Iot Category to SQL edge
- Moved category names to constants
- Moved localization strings to localized constants
- Made filtering logic more concise
- Changed how category list is generated
--Category list can now be ordered
-Added back event generation for selectedCard

* Fixed bugs, and some additional changes
-Fixed radiogroup height to avoid the movement of options below it
-Restoring the focus back to the search and listview components
- Added focus behaviour for listview
- Fixed a typo in comment

* Made categories an Enum

* Added localized string

* localized category string
converted categories to enum.

* made the filtering logic more concise.

* returning string if no localized string formed
removed unnecessary returns

* fixed the filtering tag logic
resetting search when category is changed

* removing the iot tag from sql edge deployment

* made filtering logic more concise
made enum const

* added vscode list

* some cleanup

* Some PR changes
- Made PR camelcase
- added comments to SQL
- removed unnecessary export

* -Some PR related changes
-Removing unsupported style property
-scoping down css and removing unused ones.

* Fixed a comment text

* Fixed typings for listview event
This commit is contained in:
Aasim Khan
2020-10-07 13:38:12 -07:00
committed by GitHub
parent 830cef06db
commit 280a9d20f9
18 changed files with 512 additions and 28 deletions

View File

@@ -6,3 +6,13 @@
export const DeploymentConfigurationKey: string = 'deployment';
export const AzdataInstallLocationKey: string = 'azdataInstallLocation';
export const ToolsInstallPath = 'AZDATA_NB_VAR_TOOLS_INSTALLATION_PATH';
export const enum ResourceTypeCategories {
All = 'All',
OnPrem = 'On-premises',
SqlServer = 'SQL Server',
Hybrid = 'Hybrid',
PostgreSql = 'PostgreSQL',
Cloud = 'Cloud',
}

View File

@@ -21,6 +21,7 @@ export interface ResourceType {
displayIndex?: number;
okButtonText?: string;
getProvider(selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined;
tags?: string[];
}
export interface AgreementInfo {

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { ResourceTypeCategories } from './constants';
import { FieldType, OptionsType } from './interfaces';
const localize = nls.loadMessageBundle();
@@ -34,5 +35,29 @@ export const optionsTypeRadioOrDropdown = localize('optionsTypeRadioOrDropdown',
export const azdataEulaNotAccepted = localize('azdataEulaNotAccepted', "Deployment cannot continue. Azure Data CLI license terms have not yet been accepted. Please accept the EULA to enable the features that requires Azure Data CLI.");
export const azdataEulaDeclined = localize('azdataEulaDeclined', "Deployment cannot continue. Azure Data CLI license terms were declined.You can either Accept EULA to continue or Cancel this operation");
export const acceptEulaAndSelect = localize('deploymentDialog.RecheckEulaButton', "Accept EULA & Select");
export const resourceTypePickerDialogTitle = localize('resourceTypePickerDialog.title', "Select the deployment options");
export const resourceTypeSearchBoxDescription = localize('resourceTypePickerDialog.resourceSearchPlaceholder', "Filter resources...");
export const resoucrceTypeCategoryListViewTitle = localize('resourceTypePickerDialog.tagsListViewTitle', 'Categories');
export const scriptToNotebook = localize('ui.ScriptToNotebookButton', "Script");
export const deployNotebook = localize('ui.DeployButton', "Run");
export function getResourceTypeCategoryLocalizedString(resourceTypeCategory: string): string {
switch (resourceTypeCategory) {
case ResourceTypeCategories.All:
return localize('resourceTypePickerDialog.resourceTypeCategoryAll', "All");
case ResourceTypeCategories.OnPrem:
return localize('resourceTypePickerDialog.resourceTypeCategoryOnPrem', "On-premises");
case ResourceTypeCategories.SqlServer:
return localize('resourceTypePickerDialog.resourceTypeCategoriesSqlServer', "SQL Server");
case ResourceTypeCategories.Hybrid:
return localize('resourceTypePickerDialog.resourceTypeCategoryOnHybrid', "Hybrid");
case ResourceTypeCategories.PostgreSql:
return localize('resourceTypePickerDialog.resourceTypeCategoryOnPostgreSql', "PostgreSQL");
case ResourceTypeCategories.Cloud:
return localize('resourceTypePickerDialog.resourceTypeCategoryOnCloud', "Cloud");
default:
return resourceTypeCategory;
}
}

View File

@@ -2,26 +2,30 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { EOL } from 'os';
import * as nls from 'vscode-nls';
import { AgreementInfo, DeploymentProvider, ITool, ResourceType, ToolStatus } from '../interfaces';
import { select } from '../localizedConstants';
import { IResourceTypeService } from '../services/resourceTypeService';
import { IToolsService } from '../services/toolsService';
import { getErrorMessage } from '../common/utils';
import * as loc from './../localizedConstants';
import { DialogBase } from './dialogBase';
import { createFlexContainer } from './modelViewUtils';
import * as constants from '../constants';
const localize = nls.loadMessageBundle();
export class ResourceTypePickerDialog extends DialogBase {
private toolRefreshTimestamp: number = 0;
private _resourceTypes!: ResourceType[];
private _selectedResourceType: ResourceType;
private _view!: azdata.ModelView;
private _optionsContainer!: azdata.FlexContainer;
private _toolsTable!: azdata.TableComponent;
private _resourceTagsListView!: azdata.ListViewComponent;
private _resourceSearchBox!: azdata.InputBoxComponent;
private _cardGroup!: azdata.RadioCardGroupComponent;
private _optionDropDownMap: Map<string, azdata.DropDownComponent> = new Map();
private _toolsLoadingComponent!: azdata.LoadingComponent;
@@ -31,13 +35,16 @@ export class ResourceTypePickerDialog extends DialogBase {
private _installationInProgress: boolean = false;
private _tools: ITool[] = [];
private _eulaValidationSucceeded: boolean = false;
// array to store listners that are specific to the selected resource. To be cleared after change in selected resource.
private _currentResourceTypeDisposables: vscode.Disposable[] = [];
private _cardsCache: Map<string, azdata.RadioCard> = new Map();
constructor(
private toolsService: IToolsService,
private resourceTypeService: IResourceTypeService,
defaultResourceType: ResourceType,
private _resourceTypeNameFilters?: string[]) {
super(localize('resourceTypePickerDialog.title', "Select the deployment options"), 'ResourceTypePickerDialog', true);
super(loc.resourceTypePickerDialogTitle, 'ResourceTypePickerDialog', true);
this._selectedResourceType = defaultResourceType;
this._installToolButton = azdata.window.createButton(localize('deploymentDialog.InstallToolsButton', "Install tools"));
this._toDispose.push(this._installToolButton.onClick(() => {
@@ -68,44 +75,29 @@ export class ResourceTypePickerDialog extends DialogBase {
tab.registerContent((view: azdata.ModelView) => {
const tableWidth = 1126;
this._view = view;
const resourceTypes = this.resourceTypeService
this._resourceTypes = this.resourceTypeService
.getResourceTypes()
.filter(rt => !this._resourceTypeNameFilters || this._resourceTypeNameFilters.find(rtn => rt.name === rtn))
.sort((a: ResourceType, b: ResourceType) => {
return (a.displayIndex || Number.MAX_VALUE) - (b.displayIndex || Number.MAX_VALUE);
});
this._cardGroup = view.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
cards: resourceTypes.map((resourceType) => {
return <azdata.RadioCard>{
id: resourceType.name,
label: resourceType.displayName,
icon: resourceType.icon,
descriptions: [
{
textValue: resourceType.displayName,
textStyles: {
'font-size': '14px',
'font-weight': 'bold'
}
},
{
textValue: resourceType.description,
}
]
};
cards: this._resourceTypes.map((resourceType) => {
return this.createOrGetCard(resourceType);
}),
iconHeight: '35px',
iconWidth: '35px',
cardWidth: '300px',
cardHeight: '150px',
ariaLabel: localize('deploymentDialog.deploymentOptions', "Deployment options"),
width: '1100px',
width: '1000px',
height: '550px',
iconPosition: 'left'
}).component();
this._toDispose.push(this._cardGroup.onSelectionChanged(({ cardId }) => {
this._dialogObject.message = { text: '' };
this._dialogObject.okButton.label = loc.select;
const resourceType = resourceTypes.find(rt => { return rt.name === cardId; });
const resourceType = this._resourceTypes.find(rt => { return rt.name === cardId; });
if (resourceType) {
this.selectResourceType(resourceType);
}
@@ -150,10 +142,35 @@ export class ResourceTypePickerDialog extends DialogBase {
loadingText: localize('deploymentDialog.loadingRequiredTools', "Loading required tools information"),
showText: true
}).component();
const resourceComponents: azdata.Component[] = [];
if (this.getAllResourceTags().length !== 0) {
this._resourceTagsListView = this.createTagsListView();
resourceComponents.push(this._resourceTagsListView);
}
this._resourceSearchBox = view.modelBuilder.inputBox().withProperties({
placeHolder: loc.resourceTypeSearchBoxDescription,
ariaLabel: loc.resourceTypeSearchBoxDescription
}).component();
this._toDispose.push(this._resourceSearchBox.onTextChanged((value: string) => {
this.filterResources();
this._resourceSearchBox.focus();
}));
const searchContainer = view.modelBuilder.divContainer().withItems([this._resourceSearchBox]).withProps({
CSSStyles: {
'margin-left': '15px',
'width': '300px'
},
}).component();
const cardsContainer = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([searchContainer, this._cardGroup]).component();
resourceComponents.push(cardsContainer);
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[
{
component: this._cardGroup,
component: this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).withItems(resourceComponents).component(),
title: ''
}, {
component: this._agreementContainer,
@@ -175,18 +192,89 @@ export class ResourceTypePickerDialog extends DialogBase {
const form = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form).then(() => {
this.selectResourceType(this._resourceTypes[0]);
if (this._selectedResourceType) {
this._cardGroup.selectedCardId = this._selectedResourceType.name;
}
this._resourceTagsListView.focus();
});
});
this._dialogObject.content = [tab];
}
private createTagsListView(): azdata.ListViewComponent {
const tags = this.getAllResourceTags();
if (!tags.includes('All')) {
tags.splice(0, 0, 'All');
}
const items: azdata.ListViewOption[] = [];
tags.forEach((t: string, idx: number) => {
items.push({
label: loc.getResourceTypeCategoryLocalizedString(t),
id: t
});
});
const listView = this._view.modelBuilder.listView().withProps({
title: {
text: loc.resoucrceTypeCategoryListViewTitle
},
CSSStyles: {
'width': '140px',
'margin-top': '35px'
},
options: items,
selectedOptionId: items[0].id
}).component();
this._toDispose.push(listView.onDidClick((e) => {
this._resourceSearchBox.value = '';
this.filterResources();
listView.focus();
}));
return listView;
}
private filterResources(): void {
const tag = this._resourceTagsListView.selectedOptionId!;
const search = this._resourceSearchBox.value?.toLowerCase() ?? '';
// Getting resourceType based on the selected tag
let filteredResourceTypes = (tag !== 'All') ? this._resourceTypes.filter(element => element.tags?.includes(tag) ?? false) : this._resourceTypes;
// Filtering resourceTypes based on their names.
const filteredResourceTypesOnSearch: ResourceType[] = filteredResourceTypes.filter((element) => element.displayName.toLowerCase().includes(search!));
// Adding resourceTypes with descriptions matching the search text to the result at the end as they might be less relevant.
filteredResourceTypesOnSearch.push(...filteredResourceTypes.filter((element) => !element.displayName.toLowerCase().includes(search!) && element.description.toLowerCase().includes(search!)));
const cards = filteredResourceTypesOnSearch.map((resourceType) => this.createOrGetCard(resourceType));
if (filteredResourceTypesOnSearch.length > 0) {
this._cardGroup.updateProperties({
selectedCardId: cards[0].id,
cards: cards
});
this.selectResourceType(filteredResourceTypesOnSearch[0]);
}
else {
this._cardGroup.updateProperties({
selectedCardId: '',
cards: []
});
this._agreementCheckboxChecked = false;
this._agreementContainer.clearItems();
this._optionsContainer.clearItems();
this.updateToolsDisplayTable();
}
}
private selectResourceType(resourceType: ResourceType): void {
this._currentResourceTypeDisposables.forEach(disposable => disposable.dispose());
this._selectedResourceType = resourceType;
//handle special case when resource type has different OK button.
this._dialogObject.okButton.label = this._selectedResourceType.okButtonText || select;
this._dialogObject.okButton.label = this._selectedResourceType.okButtonText || loc.select;
this._agreementCheckboxChecked = false;
this._agreementContainer.clearItems();
@@ -210,7 +298,7 @@ export class ResourceTypePickerDialog extends DialogBase {
ariaLabel: option.displayName
}).component();
this._toDispose.push(optionSelectBox.onValueChanged(() => { this.updateToolsDisplayTable(); }));
this._currentResourceTypeDisposables.push(optionSelectBox.onValueChanged(() => { this.updateToolsDisplayTable(); }));
this._optionDropDownMap.set(option.name, optionSelectBox);
const row = this._view.modelBuilder.flexContainer().withItems([optionLabel, optionSelectBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
this._optionsContainer.addItem(row);
@@ -262,7 +350,7 @@ export class ResourceTypePickerDialog extends DialogBase {
this._toolsTable.data = this.toolRequirements.map(toolRequirement => {
const tool = this.toolsService.getToolByName(toolRequirement.name)!;
// subscribe to onUpdateData event of the tool.
this._toDispose.push(tool.onDidUpdateData((t: ITool) => {
this._currentResourceTypeDisposables.push(tool.onDidUpdateData((t: ITool) => {
this.updateToolsDisplayTableData(t);
}));
@@ -361,7 +449,7 @@ export class ResourceTypePickerDialog extends DialogBase {
required: true
}).component();
checkbox.checked = false;
this._toDispose.push(checkbox.onChanged(() => {
this._currentResourceTypeDisposables.push(checkbox.onChanged(() => {
this._agreementCheckboxChecked = !!checkbox.checked;
}));
const text = this._view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
@@ -476,4 +564,49 @@ export class ResourceTypePickerDialog extends DialogBase {
this._installationInProgress = false;
}
}
private getAllResourceTags(): string[] {
const supportedTags = [
constants.ResourceTypeCategories.All,
constants.ResourceTypeCategories.OnPrem,
constants.ResourceTypeCategories.Hybrid,
constants.ResourceTypeCategories.Cloud,
constants.ResourceTypeCategories.SqlServer,
constants.ResourceTypeCategories.PostgreSql
];
const tagsWithResourceTypes = supportedTags.filter(tag => {
return (tag === constants.ResourceTypeCategories.All) || this._resourceTypes.find(resourceType => resourceType.tags?.includes(tag)) !== undefined;
});
return tagsWithResourceTypes;
}
private createOrGetCard(resourceType: ResourceType): azdata.RadioCard {
if (this._cardsCache.has(resourceType.name)) {
return this._cardsCache.get(resourceType.name)!;
}
const newCard = <azdata.RadioCard>{
id: resourceType.name,
label: resourceType.displayName,
icon: resourceType.icon,
descriptions: [
{
textValue: resourceType.displayName,
textStyles: {
'font-size': '14px',
'font-weight': 'bold'
}
},
{
textValue: resourceType.description,
}
]
};
this._cardsCache.set(resourceType.name, newCard);
return newCard;
}
}