mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
@@ -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": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"light": "./images/sqldb_edge.svg",
|
||||
"dark": "./images/sqldb_edge_inverse.svg"
|
||||
},
|
||||
"tags": ["Hybrid", "SQL Server"],
|
||||
"options": [
|
||||
{
|
||||
"name": "type",
|
||||
|
||||
@@ -278,6 +278,7 @@ export function createViewContext(): ViewTestContext {
|
||||
validate: undefined!,
|
||||
initializeModel: () => { return Promise.resolve(); },
|
||||
modelBuilder: {
|
||||
listView: undefined!,
|
||||
radioCardGroup: undefined!,
|
||||
navContainer: undefined!,
|
||||
divContainer: () => divBuilder,
|
||||
|
||||
@@ -227,6 +227,7 @@ export function createViewContext(): ViewTestContext {
|
||||
validate: undefined!,
|
||||
initializeModel: () => { return Promise.resolve(); },
|
||||
modelBuilder: {
|
||||
listView: undefined!,
|
||||
radioCardGroup: undefined!,
|
||||
navContainer: undefined!,
|
||||
divContainer: () => divBuilder,
|
||||
|
||||
@@ -265,6 +265,7 @@ describe('Manage Package Dialog', () => {
|
||||
validate: undefined!,
|
||||
initializeModel: () => { return Promise.resolve(); },
|
||||
modelBuilder: {
|
||||
listView: undefined!,
|
||||
radioCardGroup: undefined!,
|
||||
navContainer: undefined!,
|
||||
divContainer: undefined!,
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface ResourceType {
|
||||
displayIndex?: number;
|
||||
getProvider(selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined;
|
||||
okButtonText?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface AgreementInfo {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
23
src/sql/azdata.proposed.d.ts
vendored
23
src/sql/azdata.proposed.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -135,6 +135,7 @@ export enum ModelComponentTypes {
|
||||
Hyperlink,
|
||||
Image,
|
||||
RadioCardGroup,
|
||||
ListView,
|
||||
TabbedPanel,
|
||||
Separator,
|
||||
PropertiesContainer
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -174,6 +174,7 @@ export enum ModelComponentTypes {
|
||||
Hyperlink,
|
||||
Image,
|
||||
RadioCardGroup,
|
||||
ListView,
|
||||
TabbedPanel,
|
||||
Separator,
|
||||
PropertiesContainer
|
||||
|
||||
@@ -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>
|
||||
192
src/sql/workbench/browser/modelComponents/listView.component.ts
Normal file
192
src/sql/workbench/browser/modelComponents/listView.component.ts
Normal 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
|
||||
}
|
||||
}
|
||||
33
src/sql/workbench/browser/modelComponents/media/listView.css
Normal file
33
src/sql/workbench/browser/modelComponents/media/listView.css
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user