Aasim/release1.23/resource filter (#12796)

* 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

* Adding tags to azure sql deployment
This commit is contained in:
Aasim Khan
2020-10-07 14:55:09 -07:00
committed by GitHub
parent 98ed0d5274
commit 93e806cca1
18 changed files with 511 additions and 32 deletions

View File

@@ -138,6 +138,7 @@
"light": "./images/data_controller.svg",
"dark": "./images/data_controller.svg"
},
"tags": ["Hybrid", "SQL Server", "PostgreSQL"],
"providers": [
{
"notebookWizard": {
@@ -556,6 +557,7 @@
"light": "./images/miaa.svg",
"dark": "./images/miaa.svg"
},
"tags": ["Hybrid", "SQL Server"],
"providers": [
{
"dialog": {
@@ -677,6 +679,7 @@
"light": "./images/postgres.svg",
"dark": "./images/postgres.svg"
},
"tags": ["Hybrid", "PostgreSQL"],
"providers": [
{
"dialog": {

View File

@@ -28,6 +28,7 @@
"light": "./images/sqldb_edge.svg",
"dark": "./images/sqldb_edge_inverse.svg"
},
"tags": ["Hybrid", "SQL Server"],
"options": [
{
"name": "type",

View File

@@ -278,6 +278,7 @@ export function createViewContext(): ViewTestContext {
validate: undefined!,
initializeModel: () => { return Promise.resolve(); },
modelBuilder: {
listView: undefined!,
radioCardGroup: undefined!,
navContainer: undefined!,
divContainer: () => divBuilder,

View File

@@ -227,6 +227,7 @@ export function createViewContext(): ViewTestContext {
validate: undefined!,
initializeModel: () => { return Promise.resolve(); },
modelBuilder: {
listView: undefined!,
radioCardGroup: undefined!,
navContainer: undefined!,
divContainer: () => divBuilder,

View File

@@ -265,6 +265,7 @@ describe('Manage Package Dialog', () => {
validate: undefined!,
initializeModel: () => { return Promise.resolve(); },
modelBuilder: {
listView: undefined!,
radioCardGroup: undefined!,
navContainer: undefined!,
divContainer: undefined!,

View File

@@ -74,6 +74,7 @@
"light": "./images/sql_server_container.svg",
"dark": "./images/sql_server_container_inverse.svg"
},
"tags": ["On-premises", "SQL Server"],
"options": [
{
"name": "version",
@@ -205,6 +206,7 @@
"light": "./images/sql_bdc.svg",
"dark": "./images/sql_bdc_inverse.svg"
},
"tags": ["On-premises", "SQL Server"],
"options": [
{
"name": "version",
@@ -363,6 +365,7 @@
"light": "./images/sql_server_on_windows.svg",
"dark": "./images/sql_server_on_windows_inverse.svg"
},
"tags": ["On-premises", "SQL Server"],
"options": [
{
"name": "version",
@@ -403,6 +406,7 @@
"light": "./images/azure-sql.svg",
"dark": "./images/azure-sql.svg"
},
"tags": ["Cloud"],
"options": [],
"providers": [
{

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;
getProvider(selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined;
okButtonText?: string;
tags?: string[];
}
export interface AgreementInfo {

View File

@@ -5,6 +5,7 @@
import * as nls from 'vscode-nls';
import { OptionsSourceType } from './helpers/optionSources';
import { ResourceTypeCategories } from './constants';
import { FieldType, OptionsType } from './interfaces';
const localize = nls.loadMessageBundle();
@@ -38,5 +39,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,6 +2,7 @@
* 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';
@@ -12,15 +13,19 @@ import { getErrorMessage } from '../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;
@@ -30,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(() => {
@@ -67,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);
}
@@ -149,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,
@@ -174,24 +192,90 @@ 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.
if (this._selectedResourceType.okButtonText) {
this._dialogObject.okButton.label = this._selectedResourceType.okButtonText;
}
else {
this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', "Select");
}
this._dialogObject.okButton.label = this._selectedResourceType.okButtonText || loc.select;
this._agreementCheckboxChecked = false;
this._agreementContainer.clearItems();
@@ -215,7 +299,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);
@@ -267,7 +351,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);
}));
@@ -366,7 +450,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>({
@@ -481,4 +565,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;
}
}

View File

@@ -248,6 +248,7 @@ declare module 'azdata' {
export interface ModelBuilder {
radioCardGroup(): ComponentBuilder<RadioCardGroupComponent, RadioCardGroupComponentProperties>;
listView(): ComponentBuilder<ListViewComponent, ListViewComponentProperties>;
tabbedPanel(): TabbedPanelComponentBuilder;
separator(): ComponentBuilder<SeparatorComponent, SeparatorComponentProperties>;
propertiesContainer(): ComponentBuilder<PropertiesContainerComponent, PropertiesContainerComponentProperties>;
@@ -301,6 +302,28 @@ declare module 'azdata' {
}
export interface ListViewComponentProperties extends ComponentProperties {
title?: ListViewTitle;
options: ListViewOption[];
selectedOptionId?: string;
}
export interface ListViewTitle {
text?: string;
style?: CssStyles;
}
export interface ListViewOption {
label: string;
id: string;
}
export type ListViewClickEvent = { id: string };
export interface ListViewComponent extends Component, ListViewComponentProperties {
onDidClick: vscode.Event<ListViewClickEvent>;
}
export interface SeparatorComponent extends Component {
}
export interface SeparatorComponentProperties extends ComponentProperties {

View File

@@ -135,6 +135,7 @@ export enum ModelComponentTypes {
Hyperlink,
Image,
RadioCardGroup,
ListView,
TabbedPanel,
Separator,
PropertiesContainer

View File

@@ -250,6 +250,13 @@ class ModelBuilderImpl implements azdata.ModelBuilder {
return builder;
}
listView(): azdata.ComponentBuilder<azdata.ListViewComponent, azdata.ListViewComponentProperties> {
let id = this.getNextComponentId();
let builder: ComponentBuilderImpl<azdata.ListViewComponent, azdata.ListViewComponentProperties> = this.getComponentBuilder(new ListViewComponentWrapper(this._proxy, this._handle, id), id);
this._componentBuilders.set(id, builder);
return builder;
}
tabbedPanel(): azdata.TabbedPanelComponentBuilder {
let id = this.getNextComponentId();
let builder = new TabbedPanelComponentBuilder(new TabbedPanelComponentWrapper(this._proxy, this._handle, id));
@@ -1820,6 +1827,43 @@ class RadioCardGroupComponentWrapper extends ComponentWrapper implements azdata.
}
}
class ListViewComponentWrapper extends ComponentWrapper implements azdata.ListViewComponent {
constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {
super(proxy, handle, ModelComponentTypes.ListView, id);
this.properties = {};
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<azdata.ListViewClickEvent>());
}
public get title(): azdata.ListViewTitle {
return this.properties['title'];
}
public set title(v: azdata.ListViewTitle) {
this.setProperty('title', v);
}
public get options(): azdata.ListViewOption[] {
return this.properties['options'];
}
public set options(v: azdata.ListViewOption[]) {
this.setProperty('options', v);
}
public get selectedOptionId(): string | undefined {
return this.properties['selectedOptionId'];
}
public set selectedOptionId(v: string | undefined) {
this.setProperty('selectedOptionId', v);
}
public get onDidClick(): vscode.Event<azdata.ListViewClickEvent> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
}
}
class TabbedPanelComponentWrapper extends ComponentWrapper implements azdata.TabbedPanelComponent {
constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {
super(proxy, handle, ModelComponentTypes.TabbedPanel, id);

View File

@@ -174,6 +174,7 @@ export enum ModelComponentTypes {
Hyperlink,
Image,
RadioCardGroup,
ListView,
TabbedPanel,
Separator,
PropertiesContainer

View File

@@ -0,0 +1,4 @@
<div role="listbox" class="modelview-listview-container" [ngStyle]="styles" [style.width]="width" [style.height]="height">
<div *ngIf="title" class="modelview-listview-title">{{title.text}}</div>
<div #vscodelist> </div>
</div>

View File

@@ -0,0 +1,192 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChangeDetectorRef, Component, ElementRef, forwardRef, Inject, Input, OnDestroy, ViewChild } from '@angular/core';
import * as azdata from 'azdata';
import { ComponentBase } from 'sql/workbench/browser/modelComponents/componentBase';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IListOptions, List } from 'vs/base/browser/ui/list/listWidget';
import 'vs/css!./media/listView';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/platform/dashboard/browser/interfaces';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
@Component({
templateUrl: decodeURI(require.toUrl('./listView.component.html'))
})
export default class ListViewComponent extends ComponentBase<azdata.ListViewComponentProperties> implements IComponent, OnDestroy {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
@ViewChild('vscodelist', { read: ElementRef }) private _vscodeList: ElementRef;
private _optionsList!: List<azdata.ListViewOption>;
private _selectedElementIdx!: number;
static ROW_HEIGHT = 26;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
) {
super(changeRef, el);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
const vscodelistOption: IListOptions<azdata.ListViewOption> = {
keyboardSupport: true,
mouseSupport: true,
smoothScrolling: true,
verticalScrollMode: ScrollbarVisibility.Auto,
};
this._optionsList = new List<azdata.ListViewOption>('ModelViewListView', this._vscodeList.nativeElement, new OptionListDelegate(ListViewComponent.ROW_HEIGHT), [new OptionsListRenderer()], vscodelistOption);
this._register(attachListStyler(this._optionsList, this.themeService));
this._register(this._optionsList.onDidChangeSelection((e) => {
if (e.indexes.length !== 0) {
this.selectOptionByIdx(e.indexes[0]);
}
}));
this._register(this._optionsList.onKeyDown((event: any) => {
if (!this.enabled || this.options.length === 0) {
return;
}
let e = new StandardKeyboardEvent(event);
if (e.keyCode === KeyCode.Space) {
this._optionsList.setSelection([this._optionsList.getFocus()[0]]);
DOM.EventHelper.stop(e, true);
}
}));
}
setLayout(layout: any): void {
this.layout();
}
ngOnDestroy(): void {
this.baseDestroy();
}
public get options(): azdata.ListViewOption[] {
return this.getProperties().options ?? [];
}
public get width(): string | number | undefined {
return this.getProperties().width ?? undefined;
}
public get height(): string | number | undefined {
return this.getProperties().height ?? undefined;
}
public get styles(): azdata.CssStyles | undefined {
return this.getProperties().CSSStyles ?? undefined;
}
public get title(): azdata.ListViewTitle {
return this.getProperties().title ?? undefined;
}
public get selectedOptionId(): string | undefined {
return this.getProperties().selectedOptionId ?? undefined;
}
public setProperties(properties: { [key: string]: any }) {
super.setProperties(properties);
if (this.options) {
this._optionsList!.splice(0, this._optionsList!.length, this.options);
let height = (<number>this.height) ?? (this.options.length * ListViewComponent.ROW_HEIGHT);
this._optionsList.layout(height);
}
// This is the entry point for the extension to set the selectedOptionId
if (this.selectedOptionId) {
this._optionsList.setSelection([this.options.map(v => v.id).indexOf(this.selectedOptionId)]);
}
}
public selectOptionByIdx(idx: number): void {
if (!this.enabled || this.options.length === 0) {
return;
}
this._selectedElementIdx = idx;
const selectedOption = this.options[idx];
this.setPropertyFromUI<string | undefined>((props, value) => props.selectedOptionId = value, selectedOption.id);
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: {
id: selectedOption.id
}
});
}
public focus(): void {
super.focus();
if (this._selectedElementIdx !== undefined) {
this._optionsList.domFocus();
const focusElement = (this._selectedElementIdx === undefined) ? 0 : this._selectedElementIdx;
this._optionsList.setFocus([focusElement]);
}
}
}
class OptionListDelegate implements IListVirtualDelegate<azdata.ListViewOption> {
constructor(
private _height: number
) {
}
public getHeight(element: azdata.ListViewOption): number {
return this._height;
}
public getTemplateId(element: azdata.ListViewOption): string {
return 'optionListRenderer';
}
}
interface ExtensionListTemplate {
root: HTMLElement;
}
class OptionsListRenderer implements IListRenderer<azdata.ListViewOption, ExtensionListTemplate> {
public static TEMPLATE_ID = 'optionListRenderer';
public get templateId(): string {
return OptionsListRenderer.TEMPLATE_ID;
}
public renderTemplate(container: HTMLElement): ExtensionListTemplate {
const tableTemplate: ExtensionListTemplate = Object.create(null);
tableTemplate.root = DOM.append(container, DOM.$('div.list-row.listview-option'));
return tableTemplate;
}
public renderElement(option: azdata.ListViewOption, index: number, templateData: ExtensionListTemplate): void {
templateData.root.innerText = option.label ?? '';
}
public disposeTemplate(template: ExtensionListTemplate): void {
// noop
}
public disposeElement(element: azdata.ListViewOption, index: number, templateData: ExtensionListTemplate): void {
// noop
}
}

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.modelview-listview-container {
display: flex;
flex-direction: column;
height: 100%;
max-width: 150px;
min-width: 120px;
font-size: 100%;
font-weight: inherit;
overflow: auto;
padding: 0 0 0 0;
font-size: 12px;
}
.modelview-listview-container .modelview-listview-title {
margin: 0 5px 5px 0;
font-weight: bold;
line-height: 16px;
padding: 5px 0px 5px 5px;
}
.modelview-listview-container .listview-option {
line-height: 16px;
width: 95%;
padding: 5px 0px 5px 5px;
}

View File

@@ -34,6 +34,8 @@ import TabbedPanelComponent from 'sql/workbench/browser/modelComponents/tabbedPa
import SeparatorComponent from 'sql/workbench/browser/modelComponents/separator.component';
import { ModelComponentTypes } from 'sql/platform/dashboard/browser/interfaces';
import PropertiesContainerComponent from 'sql/workbench/browser/modelComponents/propertiesContainer.component';
import ListViewComponent from 'sql/workbench/browser/modelComponents/listView.component';
export const DIV_CONTAINER = 'div-container';
registerComponentType(DIV_CONTAINER, ModelComponentTypes.DivContainer, DivContainer);
@@ -113,6 +115,9 @@ registerComponentType(HYPERLINK_COMPONENT, ModelComponentTypes.Hyperlink, Hyperl
export const RADIOCARDGROUP_COMPONENT = 'radiocardgroup-component';
registerComponentType(RADIOCARDGROUP_COMPONENT, ModelComponentTypes.RadioCardGroup, RadioCardGroup);
export const LISTVIEW_COMPONENT = 'listView-component';
registerComponentType(LISTVIEW_COMPONENT, ModelComponentTypes.ListView, ListViewComponent);
export const TABBEDPANEL_COMPONENT = 'tabbedpanel-component';
registerComponentType(TABBEDPANEL_COMPONENT, ModelComponentTypes.TabbedPanel, TabbedPanelComponent);