diff --git a/extensions/mssql/resources/dark/delete_inverse.svg b/extensions/mssql/resources/dark/delete_inverse.svg index 548f3729d0..7274a63148 100644 --- a/extensions/mssql/resources/dark/delete_inverse.svg +++ b/extensions/mssql/resources/dark/delete_inverse.svg @@ -1,3 +1,10 @@ - - - \ No newline at end of file + + + + + + + + + + diff --git a/extensions/mssql/resources/dark/group_inverse.svg b/extensions/mssql/resources/dark/group_inverse.svg new file mode 100644 index 0000000000..db3d7f4a10 --- /dev/null +++ b/extensions/mssql/resources/dark/group_inverse.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/mssql/resources/dark/user_inverse.svg b/extensions/mssql/resources/dark/user_inverse.svg new file mode 100644 index 0000000000..9a2aaa77e8 --- /dev/null +++ b/extensions/mssql/resources/dark/user_inverse.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/mssql/resources/light/group.svg b/extensions/mssql/resources/light/group.svg new file mode 100644 index 0000000000..1ecbcd5fbc --- /dev/null +++ b/extensions/mssql/resources/light/group.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/mssql/resources/light/user.svg b/extensions/mssql/resources/light/user.svg new file mode 100644 index 0000000000..68573d6688 --- /dev/null +++ b/extensions/mssql/resources/light/user.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/mssql/src/hdfs/aclEntry.ts b/extensions/mssql/src/hdfs/aclEntry.ts index 8284efa605..86afff03df 100644 --- a/extensions/mssql/src/hdfs/aclEntry.ts +++ b/extensions/mssql/src/hdfs/aclEntry.ts @@ -3,38 +3,44 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IconPathHelper, IconPath } from '../iconHelper'; + /** - * The parsed result from calling getAclStatus on the controller + * The permission status of an HDFS path - this consists of : + * - The sticky bit for that path + * - The permission bits for the owner, group and other + * - (Optional) Set of additional ACL entries on this path */ -export interface IAclStatus { +export class PermissionStatus { /** - * The ACL entries defined for the object - */ - entries: AclEntry[]; - /** - * The ACL entry object for the owner permissions - */ - owner: AclEntry; - /** - * The ACL entry object for the group permissions - */ - group: AclEntry; - /** - * The ACL entry object for the other permissions - */ - other: AclEntry; - /** - * The sticky bit status for the object. If true the owner/root are + * + * @param owner The ACL entry object for the owner permissions + * @param group The ACL entry object for the group permissions + * @param other The ACL entry object for the other permissions + * @param stickyBit The sticky bit status for the object. If true the owner/root are * the only ones who can delete the resource or its contents (if a folder) + * @param aclEntries The ACL entries defined for the object */ - stickyBit: boolean; + constructor(public owner: AclEntry, public group: AclEntry, public other: AclEntry, public stickyBit: boolean, public aclEntries: AclEntry[]) { } + + /** + * The permission octal for the path in the form [#]### with each # mapping to : + * 0 (optional) - The sticky bit (1 or 0) + * 1 - The owner permission digit + * 2 - The group permission digit + * 3 - The other permission digit + * @see AclEntryPermission for more information on the permission digits + */ + public get permissionOctal(): string { + return `${this.stickyBit ? '1' : ''}${this.owner.permissionDigit}${this.group.permissionDigit}${this.other.permissionDigit}`; + } } /** * The type of an ACL entry. Corresponds to the first (or second if a scope is present) field of * an ACL entry - e.g. user:bob:rwx (user) or default:group::r-- (group) */ -export enum AclEntryType { +export enum AclType { /** * An ACL entry applied to a specific user. */ @@ -58,7 +64,7 @@ export enum AclEntryType { * Typically this value is represented as a 3 digit octal - e.g. 740 - where the first digit is the owner, the second * the group and the third other. @see parseAclPermissionFromOctal */ -export enum AclPermissionType { +export enum PermissionType { owner = 'owner', group = 'group', other = 'other' @@ -92,6 +98,14 @@ export class AclEntryPermission { public toString() { return `${this.read ? 'r' : '-'}${this.write ? 'w' : '-'}${this.execute ? 'x' : '-'}`; } + + /** + * Gets the digit for a permission octal for this permission. This digit is a value + * between 0 and 7 inclusive, which is a bitwise OR the permission flags (r/w/x). + */ + public get permissionDigit(): number { + return (this.read ? 4 : 0) + (this.write ? 2 : 0) + (this.execute ? 1 : 0); + } } /** @@ -123,12 +137,19 @@ function parseAclPermission(permissionString: string): AclEntryPermission { export class AclEntry { constructor( public readonly scope: AclEntryScope, - public readonly type: AclEntryType | AclPermissionType, + public readonly type: AclType | PermissionType, public readonly name: string, public readonly displayName: string, public readonly permission: AclEntryPermission, ) { } + /** + * Gets the octal number representing the permission for this entry. This digit is a value + * between 0 and 7 inclusive, which is a bitwise OR the permission flags (r/w/x). + */ + public get permissionDigit(): number { + return this.permission.permissionDigit; + } /** * Returns the string representation of the ACL Entry in the form [SCOPE:]TYPE:NAME:PERMISSION. * Note that SCOPE is only displayed if it's default - access is implied if there is no scope @@ -163,22 +184,22 @@ export class AclEntry { * Maps the possible entry types into their corresponding values for using in an ACL string * @param type The type to convert */ -function getAclEntryType(type: AclEntryType | AclPermissionType): AclEntryType { +function getAclEntryType(type: AclType | PermissionType): AclType { // We only need to map AclPermissionType - AclEntryType is already the // correct values we're mapping to. - if (type in AclPermissionType) { + if (type in PermissionType) { switch (type) { - case AclPermissionType.owner: - return AclEntryType.user; - case AclPermissionType.group: - return AclEntryType.group; - case AclPermissionType.other: - return AclEntryType.other; + case PermissionType.owner: + return AclType.user; + case PermissionType.group: + return AclType.group; + case PermissionType.other: + return AclType.other; default: throw new Error(`Unknown AclPermissionType : ${type}`); } } - return type; + return type; } /** @@ -213,19 +234,19 @@ export function parseAclEntry(aclString: string): AclEntry { const parts: string[] = aclString.split(':'); let i = 0; const scope: AclEntryScope = parts.length === 4 && parts[i++] === 'default' ? AclEntryScope.default : AclEntryScope.access; - let type: AclEntryType; + let type: AclType; switch (parts[i++]) { case 'user': - type = AclEntryType.user; + type = AclType.user; break; case 'group': - type = AclEntryType.group; + type = AclType.group; break; case 'mask': - type = AclEntryType.mask; + type = AclType.mask; break; case 'other': - type = AclEntryType.other; + type = AclType.other; break; default: throw new Error(`Unknown ACL Entry type ${parts[i - 1]}`); @@ -269,3 +290,16 @@ export function parseAclPermissionFromOctal(octal: string): { sticky: boolean, o other: new AclEntryPermission((otherPermissionDigit & 4) === 4, (otherPermissionDigit & 2) === 2, (otherPermissionDigit & 1) === 1) }; } + +export function getImageForType(type: AclType | PermissionType): IconPath { + switch (type) { + case AclType.user: + case PermissionType.owner: + return IconPathHelper.user; + case AclType.group: + case PermissionType.group: + case PermissionType.other: + return IconPathHelper.group; + } + return { dark: '', light: '' }; +} diff --git a/extensions/mssql/src/hdfs/fileStatus.ts b/extensions/mssql/src/hdfs/fileStatus.ts new file mode 100644 index 0000000000..8af04bd4e9 --- /dev/null +++ b/extensions/mssql/src/hdfs/fileStatus.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export enum HdfsFileType { + File = 'File', + Directory = 'Directory', + Symlink = 'Symlink' +} + +export class FileStatus { + /** + * + * @param owner The ACL entry object for the owner permissions + * @param group The ACL entry object for the group permissions + * @param other The ACL entry object for the other permissions + * @param stickyBit The sticky bit status for the object. If true the owner/root are + * the only ones who can delete the resource or its contents (if a folder) + * @param aclEntries The ACL entries defined for the object + */ + constructor( + /** + * Access time for the file + */ + public readonly accessTime: string, + /** + * The block size of a file. + */ + public readonly blockSize: string, + /** + * The group owner. + */ + public readonly group: string, + /** + * The number of bytes in a file. (0 for directories) + */ + public readonly length: string, + /** + * The modification time. + */ + public readonly modificationTime: string, + /** + * The user who is the owner. + */ + public readonly owner: string, + /** + * The path suffix. + */ + public readonly pathSuffix: string, + /** + * The permission represented as a octal string. + */ + public readonly permission: string, + /** + * The number of replication of a file. + */ + public readonly replication: string, + /** + * Whether a directory is snapshot enabled or not + */ + public readonly snapshotEnabled: string, + /** + * The type of the path object. + */ + public readonly type: HdfsFileType + ) { } +} + +/** + * Parses a fileType string into the corresponding @see HdfsFileType + * @param fileType The fileType string to parse + */ +export function parseHdfsFileType(fileType: string): HdfsFileType { + switch (fileType.toLowerCase()) { + case 'file': + return HdfsFileType.File; + case 'directory': + return HdfsFileType.Directory; + case 'symlink': + return HdfsFileType.Symlink; + default: + throw new Error(`Unknown HdfsFileType '${fileType}'`); + } +} diff --git a/extensions/mssql/src/hdfs/hdfsModel.ts b/extensions/mssql/src/hdfs/hdfsModel.ts index ebc00a1467..dbac23e6ec 100644 --- a/extensions/mssql/src/hdfs/hdfsModel.ts +++ b/extensions/mssql/src/hdfs/hdfsModel.ts @@ -5,23 +5,29 @@ import * as vscode from 'vscode'; import { IFileSource } from '../objectExplorerNodeProvider/fileSources'; -import { IAclStatus, AclEntry, AclEntryScope, AclEntryType, AclEntryPermission } from './aclEntry'; +import { PermissionStatus, AclEntry, AclEntryScope, AclType, AclEntryPermission } from './aclEntry'; +import { FileStatus } from './fileStatus'; /** * Model for storing the state of a specified file/folder in HDFS */ export class HdfsModel { - private readonly _onAclStatusUpdated = new vscode.EventEmitter(); + private readonly _onPermissionStatusUpdated = new vscode.EventEmitter(); /** - * Event that's fired anytime changes are made by the model to the ACLStatus + * Event that's fired anytime changes are made by the model to the @see PermissionStatus */ - public onAclStatusUpdated = this._onAclStatusUpdated.event; + public onPermissionStatusUpdated = this._onPermissionStatusUpdated.event; /** - * The ACL status of the file/folder + * The @see PermissionStatus of the file/folder */ - public aclStatus: IAclStatus; + public permissionStatus: PermissionStatus; + + /** + * The @see FileStatus of the file/folder + */ + public fileStatus: FileStatus; constructor(private fileSource: IFileSource, private path: string) { this.refresh(); @@ -31,8 +37,10 @@ export class HdfsModel { * Refresh the ACL status with the current values on HDFS */ public async refresh(): Promise { - this.aclStatus = await this.fileSource.getAclStatus(this.path); - this._onAclStatusUpdated.fire(this.aclStatus); + [this.permissionStatus, this.fileStatus] = await Promise.all([ + this.fileSource.getAclStatus(this.path), + this.fileSource.getFileStatus(this.path)]); + this._onPermissionStatusUpdated.fire(this.permissionStatus); } /** @@ -41,18 +49,18 @@ export class HdfsModel { * @param name The name of the ACL Entry * @param type The type of ACL to create */ - public createAndAddAclEntry(name: string, type: AclEntryType): void { - if (!this.aclStatus) { + public createAndAddAclEntry(name: string, type: AclType): void { + if (!this.permissionStatus) { return; } const newEntry = new AclEntry(AclEntryScope.access, type, name, name, new AclEntryPermission(true, true, true)); // Don't add duplicates. This also checks the owner, group and other items - if ([this.aclStatus.owner, this.aclStatus.group, this.aclStatus.other].concat(this.aclStatus.entries).find(entry => entry.isEqual(newEntry))) { + if ([this.permissionStatus.owner, this.permissionStatus.group, this.permissionStatus.other].concat(this.permissionStatus.aclEntries).find(entry => entry.isEqual(newEntry))) { return; } - this.aclStatus.entries.push(newEntry); - this._onAclStatusUpdated.fire(this.aclStatus); + this.permissionStatus.aclEntries.push(newEntry); + this._onPermissionStatusUpdated.fire(this.permissionStatus); } /** @@ -60,8 +68,8 @@ export class HdfsModel { * @param entryToDelete The entry to delete */ public deleteAclEntry(entryToDelete: AclEntry): void { - this.aclStatus.entries = this.aclStatus.entries.filter(entry => !entry.isEqual(entryToDelete)); - this._onAclStatusUpdated.fire(this.aclStatus); + this.permissionStatus.aclEntries = this.permissionStatus.aclEntries.filter(entry => !entry.isEqual(entryToDelete)); + this._onPermissionStatusUpdated.fire(this.permissionStatus); } @@ -70,8 +78,10 @@ export class HdfsModel { * permissions that shouldn't change need to still exist and have the same values. * @param recursive Whether to apply the changes recursively (to all sub-folders and files) */ - public apply(recursive: boolean = false): Promise { + public apply(recursive: boolean = false): Promise { // TODO Apply recursive - return this.fileSource.setAcl(this.path, this.aclStatus.owner, this.aclStatus.group, this.aclStatus.other, this.aclStatus.entries); + return Promise.all([ + this.fileSource.setAcl(this.path, this.permissionStatus.owner, this.permissionStatus.group, this.permissionStatus.other, this.permissionStatus.aclEntries), + this.fileSource.setPermission(this.path, this.permissionStatus)]); } } diff --git a/extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts b/extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts index a4998657cb..e76d3e9319 100644 --- a/extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts +++ b/extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts @@ -6,13 +6,15 @@ import * as azdata from 'azdata'; import { HdfsModel } from '../hdfsModel'; import { IFileSource } from '../../objectExplorerNodeProvider/fileSources'; -import { IAclStatus, AclEntry, AclEntryType } from '../../hdfs/aclEntry'; +import { PermissionStatus, AclEntry, AclType, getImageForType } from '../../hdfs/aclEntry'; import { cssStyles } from './uiConstants'; import * as loc from '../../localizedConstants'; import { HdfsError } from '../webhdfs'; import { ApiWrapper } from '../../apiWrapper'; import { IconPathHelper } from '../../iconHelper'; +import { HdfsFileType } from '../fileStatus'; +const permissionsTypeIconColumnWidth = 35; const permissionsNameColumnWidth = 250; const permissionsStickyColumnWidth = 50; const permissionsReadColumnWidth = 50; @@ -36,11 +38,13 @@ export class ManageAccessDialog { private addUserOrGroupInput: azdata.InputBoxComponent; private dialog: azdata.window.Dialog; - private addUserOrGroupSelectedType: AclEntryType; + private defaultSectionComponents: azdata.Component[] = []; + + private addUserOrGroupSelectedType: AclType; constructor(private hdfsPath: string, private fileSource: IFileSource, private readonly apiWrapper: ApiWrapper) { this.hdfsModel = new HdfsModel(this.fileSource, this.hdfsPath); - this.hdfsModel.onAclStatusUpdated(aclStatus => this.handleAclStatusUpdated(aclStatus)); + this.hdfsModel.onPermissionStatusUpdated(permissionStatus => this.handlePermissionStatusUpdated(permissionStatus)); } public openDialog(): void { @@ -123,7 +127,7 @@ export class ManageAccessDialog { // = Owners permissions section = // ============================== - const ownersPermissionsHeaderRow = createPermissionsHeaderRow(modelView.modelBuilder, loc.ownersHeader, true); + const ownersPermissionsHeaderRow = this.createPermissionsHeaderRow(modelView.modelBuilder, loc.ownersHeader, true); contentContainer.addItem(ownersPermissionsHeaderRow, { CSSStyles: { ...cssStyles.tableHeaderLayoutCss } }); // Empty initially - this is going to eventually be populated with the owner/owning group permissions @@ -155,10 +159,10 @@ export class ManageAccessDialog { const typeContainer = modelView.modelBuilder.flexContainer().withProperties({ flexFlow: 'row' }).component(); const aclEntryTypeGroup = 'aclEntryType'; - const userTypeButton = this.createRadioButton(modelView.modelBuilder, loc.userLabel, aclEntryTypeGroup, AclEntryType.user); - const groupTypeButton = this.createRadioButton(modelView.modelBuilder, loc.groupLabel, aclEntryTypeGroup, AclEntryType.group); + const userTypeButton = this.createRadioButton(modelView.modelBuilder, loc.userLabel, aclEntryTypeGroup, AclType.user); + const groupTypeButton = this.createRadioButton(modelView.modelBuilder, loc.groupLabel, aclEntryTypeGroup, AclType.group); userTypeButton.checked = true; - this.addUserOrGroupSelectedType = AclEntryType.user; + this.addUserOrGroupSelectedType = AclType.user; typeContainer.addItems([userTypeButton, groupTypeButton], { flex: '0 0 auto' }); contentContainer.addItem(typeContainer, { flex: '0 0 auto' }); @@ -199,7 +203,7 @@ export class ManageAccessDialog { // = Named Users and Groups permissions header row = // ================================================= - const namedUsersAndGroupsPermissionsHeaderRow = createPermissionsHeaderRow(modelView.modelBuilder, loc.namedUsersAndGroupsHeader, false); + const namedUsersAndGroupsPermissionsHeaderRow = this.createPermissionsHeaderRow(modelView.modelBuilder, loc.namedUsersAndGroupsHeader, false); contentContainer.addItem(namedUsersAndGroupsPermissionsHeaderRow, { CSSStyles: { ...cssStyles.tableHeaderLayoutCss } }); // Empty initially - this is eventually going to be populated with the ACL entries set for this path @@ -209,7 +213,7 @@ export class ManageAccessDialog { contentContainer.addItem(this.namedUsersAndGroupsPermissionsContainer, { flex: '1', CSSStyles: { 'overflow': 'scroll', 'min-height': '200px' } }); this.viewInitialized = true; - this.handleAclStatusUpdated(this.hdfsModel.aclStatus); + this.handlePermissionStatusUpdated(this.hdfsModel.permissionStatus); await modelView.initializeModel(this.rootLoadingComponent); }); this.dialog.content = [tab]; @@ -218,25 +222,28 @@ export class ManageAccessDialog { azdata.window.openDialog(this.dialog); } - private handleAclStatusUpdated(aclStatus: IAclStatus): void { - if (!aclStatus || !this.viewInitialized) { + private handlePermissionStatusUpdated(permissionStatus: PermissionStatus): void { + if (!permissionStatus || !this.viewInitialized) { return; } + // Update display status for headers for the Default section - you can't set Default ACLs for non-directories so we just hide that column + this.defaultSectionComponents.forEach(component => component.display = this.hdfsModel.fileStatus.type === HdfsFileType.Directory ? '' : 'none'); + // Owners - const ownerPermissionsRow = this.createOwnerPermissionsRow(this.modelBuilder, aclStatus.stickyBit, aclStatus.owner); - const owningGroupPermissionsRow = this.createPermissionsRow(this.modelBuilder, aclStatus.group, false); + const ownerPermissionsRow = this.createOwnerPermissionsRow(this.modelBuilder, permissionStatus); + const owningGroupPermissionsRow = this.createPermissionsRow(this.modelBuilder, permissionStatus.group, /*includeDelete*/false); this.ownersPermissionsContainer.clearItems(); this.ownersPermissionsContainer.addItems([ownerPermissionsRow, owningGroupPermissionsRow], { CSSStyles: { 'border-bottom': cssStyles.tableBorderCss, 'border-top': cssStyles.tableBorderCss, 'margin-right': '14px' } }); // Others - const otherPermissionsRow = this.createPermissionsRow(this.modelBuilder, aclStatus.other, false); + const otherPermissionsRow = this.createPermissionsRow(this.modelBuilder, permissionStatus.other, false); this.othersPermissionsContainer.clearItems(); this.othersPermissionsContainer.addItem(otherPermissionsRow, { CSSStyles: { 'border-bottom': cssStyles.tableBorderCss, 'border-top': cssStyles.tableBorderCss, 'margin-right': '14px' } }); this.namedUsersAndGroupsPermissionsContainer.clearItems(); // Named users and groups - aclStatus.entries.forEach(entry => { + permissionStatus.aclEntries.forEach(entry => { const namedEntryRow = this.createPermissionsRow(this.modelBuilder, entry, true); this.namedUsersAndGroupsPermissionsContainer.addItem(namedEntryRow, { CSSStyles: { 'border-bottom': cssStyles.tableBorderCss, 'border-top': cssStyles.tableBorderCss } }); }); @@ -244,7 +251,7 @@ export class ManageAccessDialog { this.rootLoadingComponent.loading = false; } - private createRadioButton(modelBuilder: azdata.ModelBuilder, label: string, name: string, aclEntryType: AclEntryType): azdata.RadioButtonComponent { + private createRadioButton(modelBuilder: azdata.ModelBuilder, label: string, name: string, aclEntryType: AclType): azdata.RadioButtonComponent { const button = modelBuilder.radioButton().withProperties({ label: label, name: name }).component(); button.onDidClick(() => { this.addUserOrGroupSelectedType = aclEntryType; @@ -252,16 +259,33 @@ export class ManageAccessDialog { return button; } - private createOwnerPermissionsRow(builder: azdata.ModelBuilder, sticky: boolean, entry: AclEntry): azdata.FlexContainer { - const row = this.createPermissionsRow(builder, entry, false); - const stickyComponents = createCheckbox(builder, sticky, permissionsReadColumnWidth, permissionsRowHeight); + private createOwnerPermissionsRow(builder: azdata.ModelBuilder, permissionStatus: PermissionStatus): azdata.FlexContainer { + const row = this.createPermissionsRow(builder, permissionStatus.owner, false); + const stickyComponents = createCheckbox(builder, permissionStatus.stickyBit, permissionsReadColumnWidth, permissionsRowHeight); + stickyComponents.checkbox.onChanged(() => { + permissionStatus.stickyBit = stickyComponents.checkbox.checked; + }); // Insert after name item but before other checkboxes - row.insertItem(stickyComponents.container, 1, { flex: '0 0 auto' }); + row.insertItem(stickyComponents.container, 2, { flex: '0 0 auto' }); return row; } private createPermissionsRow(builder: azdata.ModelBuilder, entry: AclEntry, includeDelete: boolean): azdata.FlexContainer { const rowContainer = builder.flexContainer().withLayout({ flexFlow: 'row', height: permissionsRowHeight }).component(); + + // Icon + const iconCell = builder.image() + .withProperties({ + iconPath: getImageForType(entry.type), + width: permissionsTypeIconColumnWidth, + height: permissionsRowHeight, + iconWidth: 20, + iconHeight: 20 + }) + .component(); + rowContainer.addItem(iconCell, { flex: '0 0 auto' }); + + // Name const nameCell = builder.text().withProperties({ value: entry.displayName }).component(); rowContainer.addItem(nameCell); @@ -281,31 +305,35 @@ export class ManageAccessDialog { // Access - Execute const accessExecuteComponents = createCheckbox(builder, entry.permission.execute, permissionsExecuteColumnWidth, permissionsRowHeight); - rowContainer.addItem(accessExecuteComponents.container, { flex: '0 0 auto', CSSStyles: { 'border-right': cssStyles.tableBorderCss } }); + rowContainer.addItem(accessExecuteComponents.container, { flex: '0 0 auto', CSSStyles: { 'border-right': this.hdfsModel.fileStatus.type === HdfsFileType.Directory ? cssStyles.tableBorderCss : '' } }); accessExecuteComponents.checkbox.onChanged(() => { entry.permission.execute = accessExecuteComponents.checkbox.checked; }); - // Default - Read - const defaultReadComponents = createCheckbox(builder, false, permissionsReadColumnWidth, permissionsRowHeight); - rowContainer.addItem(defaultReadComponents.container, { flex: '0 0 auto' }); - defaultReadComponents.checkbox.onChanged(() => { - // entry.permission.read = defaultReadComponents.checkbox.checked; TODO hook up default logic - }); + // Only directories can set ACL defaults so we hide the column for non-directories + if (this.hdfsModel.fileStatus.type === HdfsFileType.Directory) { + // Default - Read + const defaultReadComponents = createCheckbox(builder, false, permissionsReadColumnWidth, permissionsRowHeight); + rowContainer.addItem(defaultReadComponents.container, { flex: '0 0 auto' }); + defaultReadComponents.checkbox.onChanged(() => { + // entry.permission.read = defaultReadComponents.checkbox.checked; TODO hook up default logic + }); - // Default - Write - const defaultWriteComponents = createCheckbox(builder, false, permissionsWriteColumnWidth, permissionsRowHeight); - rowContainer.addItem(defaultWriteComponents.container, { flex: '0 0 auto' }); - accessReadComponents.checkbox.onChanged(() => { - // entry.permission.write = accessReadComponents.checkbox.checked; TODO hook up default logic - }); + // Default - Write + const defaultWriteComponents = createCheckbox(builder, false, permissionsWriteColumnWidth, permissionsRowHeight); + rowContainer.addItem(defaultWriteComponents.container, { flex: '0 0 auto' }); + accessReadComponents.checkbox.onChanged(() => { + // entry.permission.write = accessReadComponents.checkbox.checked; TODO hook up default logic + }); + + // Default - Execute + const defaultExecuteComponents = createCheckbox(builder, false, permissionsExecuteColumnWidth, permissionsRowHeight); + rowContainer.addItem(defaultExecuteComponents.container, { flex: '0 0 auto' }); + accessReadComponents.checkbox.onChanged(() => { + // entry.permission.execute = accessReadComponents.checkbox.checked; TODO hook up default logic + }); + } - // Default - Execute - const defaultExecuteComponents = createCheckbox(builder, false, permissionsExecuteColumnWidth, permissionsRowHeight); - rowContainer.addItem(defaultExecuteComponents.container, { flex: '0 0 auto' }); - accessReadComponents.checkbox.onChanged(() => { - // entry.permission.execute = accessReadComponents.checkbox.checked; TODO hook up default logic - }); const deleteContainer = builder.flexContainer().withLayout({ width: permissionsDeleteColumnWidth, height: permissionsRowHeight }).component(); @@ -327,60 +355,90 @@ export class ManageAccessDialog { return rowContainer; } -} -/** - * Creates the header row for the permissions tables. This contains headers for the name, optional sticky and then read/write/execute for both - * access and default sections. - * @param modelBuilder The builder used to create the model components - * @param nameColumnText The text to display for the name column - * @param includeSticky Whether to include the sticky header - */ -function createPermissionsHeaderRow(modelBuilder: azdata.ModelBuilder, nameColumnText: string, includeSticky: boolean): azdata.FlexContainer { - const rowsContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + /** + * Creates the header row for the permissions tables. This contains headers for the name, optional sticky and then read/write/execute for both + * access and default sections. + * @param modelBuilder The builder used to create the model components + * @param nameColumnText The text to display for the name column + * @param includeSticky Whether to include the sticky header + */ + private createPermissionsHeaderRow(modelBuilder: azdata.ModelBuilder, nameColumnText: string, includeSticky: boolean): azdata.FlexContainer { + const rowsContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - // Section Headers - const sectionHeaderContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', justifyContent: 'flex-end' }).component(); - const accessSectionHeader = modelBuilder.text().withProperties({ value: loc.accessHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - sectionHeaderContainer.addItem(accessSectionHeader, { CSSStyles: { 'width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px` } }); - const defaultSectionHeader = modelBuilder.text().withProperties({ value: loc.defaultHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - sectionHeaderContainer.addItem(defaultSectionHeader, { CSSStyles: { 'width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px` } }); - // Delete - just used as a spacer - const deleteSectionHeader = modelBuilder.text().component(); - sectionHeaderContainer.addItem(deleteSectionHeader, { CSSStyles: { 'width': `${permissionsDeleteColumnWidth}px`, 'min-width': `${permissionsDeleteColumnWidth}px` } }); + // Section Headers + const sectionHeaderContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', justifyContent: 'flex-end' }).component(); + const accessSectionHeader = modelBuilder.text() + .withProperties({ + value: loc.accessHeader, + CSSStyles: { + 'width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, + 'min-width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, + ...cssStyles.permissionsTableHeaderCss + } + }) + .component(); - rowsContainer.addItem(sectionHeaderContainer); + sectionHeaderContainer.addItem(accessSectionHeader, { flex: '0 0 auto' }); + const defaultSectionHeader = modelBuilder.text() + .withProperties({ + value: loc.defaultHeader, + CSSStyles: { + 'width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, + 'min-width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, + ...cssStyles.permissionsTableHeaderCss + } + }) + .component(); + sectionHeaderContainer.addItem(defaultSectionHeader, { flex: '0 0 auto' }); + this.defaultSectionComponents.push(defaultSectionHeader); + // Delete - just used as a spacer + const deleteSectionHeader = modelBuilder.text().component(); + sectionHeaderContainer.addItem(deleteSectionHeader, { CSSStyles: { 'width': `${permissionsDeleteColumnWidth}px`, 'min-width': `${permissionsDeleteColumnWidth}px` } }); - // Table headers - const headerRowContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - const ownersCell = modelBuilder.text().withProperties({ value: nameColumnText }).component(); - headerRowContainer.addItem(ownersCell, { flex: '1 1 auto', CSSStyles: { 'width': `${permissionsNameColumnWidth}px`, 'min-width': `${permissionsNameColumnWidth}px`, ...cssStyles.tableHeaderCss } }); - if (includeSticky) { - const stickyCell = modelBuilder.text().withProperties({ value: loc.stickyHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - headerRowContainer.addItem(stickyCell, { CSSStyles: { 'width': `${permissionsStickyColumnWidth}px`, 'min-width': `${permissionsStickyColumnWidth}px` } }); + rowsContainer.addItem(sectionHeaderContainer); + + // Table headers + const headerRowContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); + + // Icon (spacer, no text) + const typeIconCell = modelBuilder.text().withProperties({ CSSStyles: { 'width': `${permissionsTypeIconColumnWidth}px`, 'min-width': `${permissionsTypeIconColumnWidth}px` } }).component(); + headerRowContainer.addItem(typeIconCell, { flex: '0 0 auto' }); + + // Name + const nameCell = modelBuilder.text().withProperties({ value: nameColumnText }).component(); + headerRowContainer.addItem(nameCell, { flex: '1 1 auto', CSSStyles: { 'width': `${permissionsNameColumnWidth}px`, 'min-width': `${permissionsNameColumnWidth}px`, ...cssStyles.tableHeaderCss } }); + if (includeSticky) { + const stickyCell = modelBuilder.text().withProperties({ value: loc.stickyHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); + headerRowContainer.addItem(stickyCell, { CSSStyles: { 'width': `${permissionsStickyColumnWidth}px`, 'min-width': `${permissionsStickyColumnWidth}px` } }); + } + + // Access Permissions Group + const accessReadCell = modelBuilder.text().withProperties({ value: loc.readHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); + headerRowContainer.addItem(accessReadCell, { CSSStyles: { 'width': `${permissionsReadColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth}px` } }); + const accessWriteCell = modelBuilder.text().withProperties({ value: loc.writeHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); + headerRowContainer.addItem(accessWriteCell, { CSSStyles: { 'width': `${permissionsWriteColumnWidth}px`, 'min-width': `${permissionsWriteColumnWidth}px` } }); + const accessExecuteCell = modelBuilder.text().withProperties({ value: loc.executeHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); + headerRowContainer.addItem(accessExecuteCell, { CSSStyles: { 'width': `${permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsExecuteColumnWidth}px`, 'margin-right': '5px' } }); + // Default Permissions Group + const defaultPermissionsHeadersContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); + const defaultReadCell = modelBuilder.text().withProperties({ value: loc.readHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); + defaultPermissionsHeadersContainer.addItem(defaultReadCell, { CSSStyles: { 'width': `${permissionsReadColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth}px` } }); + const defaultWriteCell = modelBuilder.text().withProperties({ value: loc.writeHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); + defaultPermissionsHeadersContainer.addItem(defaultWriteCell, { CSSStyles: { 'width': `${permissionsWriteColumnWidth}px`, 'min-width': `${permissionsWriteColumnWidth}px` } }); + const defaultExecuteCell = modelBuilder.text().withProperties({ value: loc.executeHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); + defaultPermissionsHeadersContainer.addItem(defaultExecuteCell, { CSSStyles: { 'width': `${permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsExecuteColumnWidth}px` } }); + headerRowContainer.addItem(defaultPermissionsHeadersContainer, { flex: '0 0 auto' }); + this.defaultSectionComponents.push(defaultPermissionsHeadersContainer); + + // Delete + const deleteCell = modelBuilder.text().component(); + headerRowContainer.addItem(deleteCell, { CSSStyles: { 'width': `${permissionsDeleteColumnWidth}px`, 'min-width': `${permissionsDeleteColumnWidth}px` } }); + + rowsContainer.addItem(headerRowContainer); + + return rowsContainer; } - - // Access Permissions Group - const accessReadCell = modelBuilder.text().withProperties({ value: loc.readHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - headerRowContainer.addItem(accessReadCell, { CSSStyles: { 'width': `${permissionsReadColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth}px` } }); - const accessWriteCell = modelBuilder.text().withProperties({ value: loc.writeHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - headerRowContainer.addItem(accessWriteCell, { CSSStyles: { 'width': `${permissionsWriteColumnWidth}px`, 'min-width': `${permissionsWriteColumnWidth}px` } }); - const accessExecuteCell = modelBuilder.text().withProperties({ value: loc.executeHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - headerRowContainer.addItem(accessExecuteCell, { CSSStyles: { 'width': `${permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsExecuteColumnWidth}px`, 'margin-right': '5px' } }); - // Default Permissions Group - const defaultReadCell = modelBuilder.text().withProperties({ value: loc.readHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - headerRowContainer.addItem(defaultReadCell, { CSSStyles: { 'width': `${permissionsReadColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth}px` } }); - const defaultWriteCell = modelBuilder.text().withProperties({ value: loc.writeHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - headerRowContainer.addItem(defaultWriteCell, { CSSStyles: { 'width': `${permissionsWriteColumnWidth}px`, 'min-width': `${permissionsWriteColumnWidth}px` } }); - const defaultExecuteCell = modelBuilder.text().withProperties({ value: loc.executeHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - headerRowContainer.addItem(defaultExecuteCell, { CSSStyles: { 'width': `${permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsExecuteColumnWidth}px` } }); - // Delete - const deleteCell = modelBuilder.text().component(); - headerRowContainer.addItem(deleteCell, { CSSStyles: { 'width': `${permissionsDeleteColumnWidth}px`, 'min-width': `${permissionsDeleteColumnWidth}px` } }); - - rowsContainer.addItem(headerRowContainer); - - return rowsContainer; } function createCheckbox(builder: azdata.ModelBuilder, checked: boolean, containerWidth: number, containerHeight: number): { container: azdata.FlexContainer, checkbox: azdata.CheckBoxComponent } { @@ -388,7 +446,6 @@ function createCheckbox(builder: azdata.ModelBuilder, checked: boolean, containe .withProperties({ checked: checked, height: 20, width: 20 }).component(); const container = builder.flexContainer() .withLayout({ width: containerWidth, height: containerHeight }) - //.withItems([checkbox], { CSSStyles: { ...cssStyles.permissionCheckboxCss }}) .component(); container.addItem(checkbox, { CSSStyles: { ...cssStyles.permissionCheckboxCss } }); return { diff --git a/extensions/mssql/src/hdfs/webhdfs.ts b/extensions/mssql/src/hdfs/webhdfs.ts index baf4a15514..c70e642b6a 100644 --- a/extensions/mssql/src/hdfs/webhdfs.ts +++ b/extensions/mssql/src/hdfs/webhdfs.ts @@ -11,9 +11,10 @@ import * as through from 'through2'; import * as nls from 'vscode-nls'; import * as auth from '../util/auth'; import { IHdfsOptions, IRequestParams } from '../objectExplorerNodeProvider/fileSources'; -import { IAclStatus, AclEntry, parseAcl, AclPermissionType, parseAclPermissionFromOctal, AclEntryScope } from './aclEntry'; +import { PermissionStatus, AclEntry, parseAcl, PermissionType, parseAclPermissionFromOctal, AclEntryScope } from './aclEntry'; import { Mount } from './mount'; import { everyoneName } from '../localizedConstants'; +import { FileStatus, parseHdfsFileType } from './fileStatus'; const localize = nls.loadMessageBundle(); const ErrorMessageInvalidDataStructure = localize('webhdfs.invalidDataStructure', "Invalid Data Structure"); @@ -75,7 +76,7 @@ export class WebHDFS { endpoint.pathname = this._opts.path + path; let searchOpts = Object.assign( { 'op': operation }, - this._opts.user ? { 'user.name': this._opts.user } : {}, + // this._opts.user ? { 'user.name': this._opts.user } : {}, params || {} ); endpoint.search = querystring.stringify(searchOpts); @@ -420,13 +421,42 @@ export class WebHDFS { }); } + public getFileStatus(path: string, callback: (error: HdfsError, fileStatus: FileStatus) => void): void { + this.checkArgDefined('path', path); + + let endpoint = this.getOperationEndpoint('getfilestatus', path); + this.sendRequest('GET', endpoint, undefined, (error, response) => { + if (!callback) { return; } + if (error) { + callback(error, undefined); + } else if (response.body.hasOwnProperty('FileStatus')) { + const fileStatus = new FileStatus( + response.body.FileStatus.accessTime || '', + response.body.FileStatus.blockSize || '', + response.body.FileStatus.group || '', + response.body.FileStatus.length || '', + response.body.FileStatus.modificationTime || '', + response.body.FileStatus.owner || '', + response.body.FileStatus.pathSuffix || '', + response.body.FileStatus.permission || '', + response.body.FileStatus.replication || '', + response.body.FileStatus.snapshotEnabled || '', + parseHdfsFileType(response.body.FileStatus.type || 'undefined') + ); + callback(undefined, fileStatus); + } else { + callback(new HdfsError(ErrorMessageInvalidDataStructure), undefined); + } + }); + } + /** * Get ACL status for given path * @param path The path to the file/folder to get the status of * @param callback Callback to handle the response * @returns void */ - public getAclStatus(path: string, callback: (error: HdfsError, aclStatus: IAclStatus) => void): void { + public getAclStatus(path: string, callback: (error: HdfsError, permissionStatus: PermissionStatus) => void): void { this.checkArgDefined('path', path); let endpoint = this.getOperationEndpoint('getaclstatus', path); @@ -436,14 +466,17 @@ export class WebHDFS { callback(error, undefined); } else if (response.body.hasOwnProperty('AclStatus')) { const permissions = parseAclPermissionFromOctal(response.body.AclStatus.permission); - const aclStatus: IAclStatus = { - owner: new AclEntry(AclEntryScope.access, AclPermissionType.owner, '', response.body.AclStatus.owner || '', permissions.owner), - group: new AclEntry(AclEntryScope.access, AclPermissionType.group, '', response.body.AclStatus.group || '', permissions.group), - other: new AclEntry(AclEntryScope.access, AclPermissionType.other, '', everyoneName, permissions.other), - stickyBit: !!response.body.AclStatus.stickyBit, - entries: (response.body.AclStatus.entries).map(entry => parseAcl(entry)).reduce((acc, parsedEntries) => acc.concat(parsedEntries), []) - }; - callback(undefined, aclStatus); + const permissionStatus = new PermissionStatus( + new AclEntry(AclEntryScope.access, PermissionType.owner, '', response.body.AclStatus.owner || '', permissions.owner), + new AclEntry(AclEntryScope.access, PermissionType.group, '', response.body.AclStatus.group || '', permissions.group), + new AclEntry(AclEntryScope.access, PermissionType.other, '', everyoneName, permissions.other), + !!response.body.AclStatus.stickyBit, + // We filter out empty names here since those are already added by the permission bits - WebHDFS creates an extra ACL + // entry for the owning group whenever another ACL is added. + (response.body.AclStatus.entries).map(entry => parseAcl(entry)) + .reduce((acc, parsedEntries) => acc.concat(parsedEntries), []) + .filter(e => e.name !== '')); + callback(undefined, permissionStatus); } else { callback(new HdfsError(ErrorMessageInvalidDataStructure), undefined); } @@ -473,6 +506,20 @@ export class WebHDFS { }); } + /** + * Sets the permission octal (sticky, owner, group & other) for a file/folder + * @param path The path to the file/folder to set the permission of + * @param permissionStatus The status containing the permission to set + */ + public setPermission(path: string, permissionStatus: PermissionStatus, callback: (error: HdfsError) => void): void { + this.checkArgDefined('path', path); + this.checkArgDefined('permissionStatus', permissionStatus); + let endpoint = this.getOperationEndpoint('setpermission', path, { permission: permissionStatus.permissionOctal }); + this.sendRequest('PUT', endpoint, undefined, (error) => { + return callback && callback(error); + }); + } + /** * Get all mounts for a HDFS connection * @param callback Callback to handle the response diff --git a/extensions/mssql/src/iconHelper.ts b/extensions/mssql/src/iconHelper.ts index b307fc1cae..36b896c5e6 100644 --- a/extensions/mssql/src/iconHelper.ts +++ b/extensions/mssql/src/iconHelper.ts @@ -14,6 +14,8 @@ export class IconPathHelper { private static extensionContext: vscode.ExtensionContext; public static delete: IconPath; + public static user: IconPath; + public static group: IconPath; public static setExtensionContext(extensionContext: vscode.ExtensionContext) { IconPathHelper.extensionContext = extensionContext; @@ -21,5 +23,13 @@ export class IconPathHelper { dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/delete_inverse.svg'), light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/delete.svg') }; + IconPathHelper.user = { + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/user_inverse.svg'), + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/user.svg') + }; + IconPathHelper.group = { + dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/group_inverse.svg'), + light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/group.svg') + }; } } diff --git a/extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts b/extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts index d5d0db447e..4ab12349d1 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts @@ -15,8 +15,9 @@ import * as nls from 'vscode-nls'; import * as constants from '../constants'; import { WebHDFS, HdfsError } from '../hdfs/webhdfs'; -import { AclEntry, IAclStatus } from '../hdfs/aclEntry'; +import { AclEntry, PermissionStatus } from '../hdfs/aclEntry'; import { Mount, MountStatus } from '../hdfs/mount'; +import { FileStatus } from '../hdfs/fileStatus'; const localize = nls.loadMessageBundle(); @@ -68,11 +69,15 @@ export interface IFileSource { readFileLines(path: string, maxLines: number): Promise; writeFile(localFile: IFile, remoteDir: string): Promise; delete(path: string, recursive?: boolean): Promise; + /** + * Retrieves the file status for the specified path (may be a file or directory) + */ + getFileStatus(path: string): Promise; /** * Get ACL status for given path * @param path The path to the file/folder to get the status of */ - getAclStatus(path: string): Promise; + getAclStatus(path: string): Promise; /** * Sets the ACL status for given path * @param path The path to the file/folder to set the ACL on @@ -82,6 +87,12 @@ export interface IFileSource { * @param aclEntries The ACL entries to set */ setAcl(path: string, ownerEntry: AclEntry, groupEntry: AclEntry, otherEntry: AclEntry, aclEntries: AclEntry[]): Promise; + /** + * Sets the permission octal (sticky, owner, group & other) for a file/folder + * @param path The path to the file/folder to set the permission of + * @param aclStatus The status containing the permission to set + */ + setPermission(path: string, aclStatus: PermissionStatus): Promise; exists(path: string): Promise; } @@ -344,17 +355,29 @@ export class HdfsFileSource implements IFileSource { }); } + public getFileStatus(path: string): Promise { + return new Promise((resolve, reject) => { + this.client.getFileStatus(path, (error: HdfsError, fileStatus: FileStatus) => { + if (error) { + reject(error); + } else { + resolve(fileStatus); + } + }); + }); + } + /** * Get ACL status for given path * @param path The path to the file/folder to get the status of */ - public getAclStatus(path: string): Promise { + public getAclStatus(path: string): Promise { return new Promise((resolve, reject) => { - this.client.getAclStatus(path, (error: HdfsError, aclStatus: IAclStatus) => { + this.client.getAclStatus(path, (error: HdfsError, permissionStatus: PermissionStatus) => { if (error) { reject(error); } else { - resolve(aclStatus); + resolve(permissionStatus); } }); }); @@ -379,4 +402,21 @@ export class HdfsFileSource implements IFileSource { }); }); } + + /** + * Sets the permission octal (sticky, owner, group & other) for a file/folder + * @param path The path to the file/folder to set the permission of + * @param aclStatus The status containing the permission to set + */ + public setPermission(path: string, aclStatus: PermissionStatus): Promise { + return new Promise((resolve, reject) => { + this.client.setPermission(path, aclStatus, (error: HdfsError) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } } diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index 95a0d1b07b..b9241a1ee9 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -3109,12 +3109,10 @@ declare module 'azdata' { requiredIndicator?: boolean; } - export interface ImageComponentProperties { - src: string; - alt?: string; - height?: number | string; - width?: number | string; + export interface ImageComponentProperties extends ComponentProperties, ComponentWithIcon { + } + export interface LinkArea { text: string; url: string; diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 312473171b..b0a768b59b 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -26,6 +26,7 @@ declare module 'sqlops' { webView(): ComponentBuilder; editor(): ComponentBuilder; text(): ComponentBuilder; + image(): ComponentBuilder; button(): ComponentBuilder; dropDown(): ComponentBuilder; tree(): ComponentBuilder>; @@ -595,6 +596,10 @@ declare module 'sqlops' { links?: LinkArea[]; } + export interface ImageComponentProperties extends ComponentProperties, ComponentWithIcon { + + } + export interface LinkArea { text: string; url: string; @@ -731,6 +736,10 @@ declare module 'sqlops' { onDidClick: vscode.Event; } + export interface ImageComponent extends Component, ImageComponentProperties { + + } + export interface HyperlinkComponent extends Component, HyperlinkComponentProperties { } diff --git a/src/sql/workbench/api/common/extHostModelView.ts b/src/sql/workbench/api/common/extHostModelView.ts index b062ccda1b..1730d730f0 100644 --- a/src/sql/workbench/api/common/extHostModelView.ts +++ b/src/sql/workbench/api/common/extHostModelView.ts @@ -693,6 +693,34 @@ class ComponentWrapper implements azdata.Component { } } +class ComponentWithIconWrapper extends ComponentWrapper { + + constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string) { + super(proxy, handle, type, id); + } + + public get iconPath(): string | URI | { light: string | URI; dark: string | URI } { + return this.properties['iconPath']; + } + public set iconPath(v: string | URI | { light: string | URI; dark: string | URI }) { + this.setProperty('iconPath', v); + } + + public get iconHeight(): string | number { + return this.properties['iconHeight']; + } + public set iconHeight(v: string | number) { + this.setProperty('iconHeight', v); + } + + public get iconWidth(): string | number { + return this.properties['iconWidth']; + } + public set iconWidth(v: string | number) { + this.setProperty('iconWidth', v); + } +} + class ContainerWrapper extends ComponentWrapper implements azdata.Container { constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string) { @@ -1152,40 +1180,12 @@ class TextComponentWrapper extends ComponentWrapper implements azdata.TextCompon } } -class ImageComponentWrapper extends ComponentWrapper implements azdata.ImageComponentProperties { +class ImageComponentWrapper extends ComponentWithIconWrapper implements azdata.ImageComponentProperties { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { super(proxy, handle, ModelComponentTypes.Image, id); this.properties = {}; } - - public get src(): string { - return this.properties['src']; - } - public set src(v: string) { - this.setProperty('src', v); - } - - public get alt(): string { - return this.properties['alt']; - } - public set alt(v: string) { - this.setProperty('alt', v); - } - - public get height(): number | string { - return this.properties['height']; - } - public set height(v: number | string) { - this.setProperty('height', v); - } - - public get width(): number | string { - return this.properties['width']; - } - public set width(v: number | string) { - this.setProperty('width', v); - } } class TableComponentWrapper extends ComponentWrapper implements azdata.TableComponent { @@ -1391,7 +1391,7 @@ class ListBoxWrapper extends ComponentWrapper implements azdata.ListBoxComponent } } -class ButtonWrapper extends ComponentWrapper implements azdata.ButtonComponent { +class ButtonWrapper extends ComponentWithIconWrapper implements azdata.ButtonComponent { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { super(proxy, handle, ModelComponentTypes.Button, id); @@ -1406,27 +1406,6 @@ class ButtonWrapper extends ComponentWrapper implements azdata.ButtonComponent { this.setProperty('label', v); } - public get iconPath(): string | URI | { light: string | URI; dark: string | URI } { - return this.properties['iconPath']; - } - public set iconPath(v: string | URI | { light: string | URI; dark: string | URI }) { - this.setProperty('iconPath', v); - } - - public get iconHeight(): string | number { - return this.properties['iconHeight']; - } - public set iconHeight(v: string | number) { - this.setProperty('iconHeight', v); - } - - public get iconWidth(): string | number { - return this.properties['iconWidth']; - } - public set iconWidth(v: string | number) { - this.setProperty('iconWidth', v); - } - public get title(): string { return this.properties['title']; } diff --git a/src/sql/workbench/browser/modelComponents/image.component.ts b/src/sql/workbench/browser/modelComponents/image.component.ts index e39b3740fc..b6877fd8a2 100644 --- a/src/sql/workbench/browser/modelComponents/image.component.ts +++ b/src/sql/workbench/browser/modelComponents/image.component.ts @@ -2,25 +2,25 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - +import 'vs/css!./media/image'; import { Component, Input, Inject, ChangeDetectorRef, forwardRef, - OnDestroy, AfterViewInit, ElementRef + OnDestroy, AfterViewInit, ElementRef, 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 { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/browser/modelComponents/interfaces'; +import { ComponentWithIconBase } from 'sql/workbench/browser/modelComponents/componentWithIconBase'; @Component({ selector: 'modelview-image', template: ` - ` +
` }) -export default class ImageComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit { +export default class ImageComponent extends ComponentWithIconBase implements IComponent, OnDestroy, AfterViewInit { @Input() descriptor: IComponentDescriptor; @Input() modelStore: IModelStore; + @ViewChild('imageContainer', { read: ElementRef }) imageContainer: ElementRef; constructor( @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, @@ -45,19 +45,27 @@ export default class ImageComponent extends ComponentBase implements IComponent, this.layout(); } - public set src(newValue: string) { - this.setPropertyFromUI((properties, value) => { properties.src = value; }, newValue); + public setProperties(properties: { [key: string]: any; }): void { + super.setProperties(properties); + this.updateIcon(); + this._changeRef.detectChanges(); } - public get src(): string { - return this.getPropertyOrDefault((props) => props.src, ''); + protected updateIcon() { + if (this.iconPath) { + if (!this._iconClass) { + super.updateIcon(); + DOM.addClasses(this.imageContainer.nativeElement, this._iconClass, 'icon'); + } else { + super.updateIcon(); + } + } } - public set alt(newValue: string) { - this.setPropertyFromUI((properties, value) => { properties.alt = value; }, newValue); - } - - public get alt(): string { - return this.getPropertyOrDefault((props) => props.alt, ''); + /** + * Helper to get the size string for the background-size CSS property + */ + private getImageSize(): string { + return `${this.getIconWidth()} ${this.getIconHeight()}`; } } diff --git a/src/sql/workbench/browser/modelComponents/media/image.css b/src/sql/workbench/browser/modelComponents/media/image.css new file mode 100644 index 0000000000..1aa4fb658b --- /dev/null +++ b/src/sql/workbench/browser/modelComponents/media/image.css @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +modelview-image div.icon { + background-repeat: no-repeat; + background-position: center; + background-size: contain; +}