mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Initial work for Manage Access dialog (#7483)
* Add width and height properties to checkbox component * Initial work on manage access dialog * Add missed change * Add comments and clean up * Initial work on manage access dialog * Add missed change * Add comments and clean up * Add return type * Address comments and use apiWrapper * Fix compile error
This commit is contained in:
@@ -30,8 +30,8 @@ export class ApiWrapper {
|
||||
return azdata.dataprotocol.registerFileBrowserProvider(provider);
|
||||
}
|
||||
|
||||
public createDialog(title: string): azdata.window.Dialog {
|
||||
return azdata.window.createModelViewDialog(title);
|
||||
public createDialog(title: string, dialogName?: string, isWide?: boolean): azdata.window.Dialog {
|
||||
return azdata.window.createModelViewDialog(title, dialogName, isWide);
|
||||
}
|
||||
|
||||
public openDialog(dialog: azdata.window.Dialog): void {
|
||||
|
||||
@@ -101,7 +101,7 @@ export class AclEntryPermission {
|
||||
* e.g. The following are all valid strings
|
||||
* rwx
|
||||
* ---
|
||||
* -w-
|
||||
* -w-
|
||||
* @param permissionString The string representation of the permission
|
||||
*/
|
||||
function parseAclPermission(permissionString: string): AclEntryPermission {
|
||||
@@ -113,10 +113,11 @@ function parseAclPermission(permissionString: string): AclEntryPermission {
|
||||
}
|
||||
|
||||
/**
|
||||
* A single ACL Entry. This consists of up to 4 values
|
||||
* scope - The scope of the entry @see AclEntryScope
|
||||
* type - The type of the entry @see AclEntryType
|
||||
* name - The name of the user/group. Optional.
|
||||
* A single ACL Permission entry
|
||||
* scope - The scope of the entry @see AclEntryScope
|
||||
* type - The type of the entry @see AclEntryType
|
||||
* name - The name of the user/group used to set ACLs Optional.
|
||||
* displayName - The name to display in the UI
|
||||
* permission - The permission set for this ACL. @see AclPermission
|
||||
*/
|
||||
export class AclEntry {
|
||||
@@ -124,6 +125,7 @@ export class AclEntry {
|
||||
public readonly scope: AclEntryScope,
|
||||
public readonly type: AclEntryType | AclPermissionType,
|
||||
public readonly name: string,
|
||||
public readonly displayName: string,
|
||||
public readonly permission: AclEntryPermission,
|
||||
) { }
|
||||
|
||||
@@ -135,12 +137,48 @@ export class AclEntry {
|
||||
* Example strings :
|
||||
* user:bob:rwx
|
||||
* default:user:bob:rwx
|
||||
* user::r-x
|
||||
* default:group::r--
|
||||
* user::r-x
|
||||
* default:group::r--
|
||||
*/
|
||||
toString(): string {
|
||||
return `${this.scope === AclEntryScope.default ? 'default:' : ''}${this.type}:${this.name}:${this.permission.toString()}`;
|
||||
toAclString(): string {
|
||||
return `${this.scope === AclEntryScope.default ? 'default:' : ''}${getAclEntryType(this.type)}:${this.name}:${this.permission.toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this and the specified AclEntry are equal. Two entries are considered equal
|
||||
* if their scope, type and name are equal.
|
||||
* @param other The other entry to compare against
|
||||
*/
|
||||
public isEqual(other: AclEntry): boolean {
|
||||
if (!other) {
|
||||
return false;
|
||||
}
|
||||
return this.scope === other.scope &&
|
||||
this.type === other.type &&
|
||||
this.name === other.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// We only need to map AclPermissionType - AclEntryType is already the
|
||||
// correct values we're mapping to.
|
||||
if (type in AclPermissionType) {
|
||||
switch (type) {
|
||||
case AclPermissionType.owner:
|
||||
return AclEntryType.user;
|
||||
case AclPermissionType.group:
|
||||
return AclEntryType.group;
|
||||
case AclPermissionType.other:
|
||||
return AclEntryType.other;
|
||||
default:
|
||||
throw new Error(`Unknown AclPermissionType : ${type}`);
|
||||
}
|
||||
}
|
||||
return <AclEntryType>type;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +194,7 @@ export class AclEntry {
|
||||
* default:other:r--
|
||||
*
|
||||
* So a valid ACL string might look like this
|
||||
* user:bob:rwx,user::rwx,default::bob:rwx,group::r-x,default:other:r--
|
||||
* user:bob:rwx,user::rwx,default::bob:rwx,group::r-x,default:other:r--
|
||||
* @param aclString The string representation of the ACL
|
||||
*/
|
||||
export function parseAcl(aclString: string): AclEntry[] {
|
||||
@@ -194,7 +232,7 @@ export function parseAclEntry(aclString: string): AclEntry {
|
||||
}
|
||||
const name = parts[i++];
|
||||
const permission = parseAclPermission(parts[i++]);
|
||||
return new AclEntry(scope, type, name, permission);
|
||||
return new AclEntry(scope, type, name, name, permission);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +246,7 @@ export function parseAclEntry(aclString: string): AclEntry {
|
||||
* So an octal of 730 would map to :
|
||||
* - The owner with rwx permissions
|
||||
* - The group with -wx permissions
|
||||
* - All others with --- permissions
|
||||
* - All others with --- permissions
|
||||
* @param octal The octal string to parse
|
||||
*/
|
||||
export function parseAclPermissionFromOctal(octal: string): { owner: AclEntryPermission, group: AclEntryPermission, other: AclEntryPermission } {
|
||||
|
||||
77
extensions/mssql/src/hdfs/hdfsModel.ts
Normal file
77
extensions/mssql/src/hdfs/hdfsModel.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IFileSource } from '../objectExplorerNodeProvider/fileSources';
|
||||
import { IAclStatus, AclEntry, AclEntryScope, AclEntryType, AclEntryPermission } from './aclEntry';
|
||||
|
||||
/**
|
||||
* Model for storing the state of a specified file/folder in HDFS
|
||||
*/
|
||||
export class HdfsModel {
|
||||
|
||||
private readonly _onAclStatusUpdated = new vscode.EventEmitter<IAclStatus>();
|
||||
/**
|
||||
* Event that's fired anytime changes are made by the model to the ACLStatus
|
||||
*/
|
||||
public onAclStatusUpdated = this._onAclStatusUpdated.event;
|
||||
|
||||
/**
|
||||
* The ACL status of the file/folder
|
||||
*/
|
||||
public aclStatus: IAclStatus;
|
||||
|
||||
constructor(private fileSource: IFileSource, private path: string) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the ACL status with the current values on HDFS
|
||||
*/
|
||||
public async refresh(): Promise<void> {
|
||||
this.aclStatus = await this.fileSource.getAclStatus(this.path);
|
||||
this._onAclStatusUpdated.fire(this.aclStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ACL Entry and adds it to the list of current entries. Will do nothing
|
||||
* if a duplicate entry (@see AclEntry.isEqual) exists
|
||||
* @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) {
|
||||
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))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.aclStatus.entries.push(newEntry);
|
||||
this._onAclStatusUpdated.fire(this.aclStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified entry from the list of registered
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies the changes made to this model to HDFS. Note that this will overwrite ALL permissions so any
|
||||
* 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<void> {
|
||||
// TODO Apply recursive
|
||||
return this.fileSource.setAcl(this.path, this.aclStatus.owner, this.aclStatus.group, this.aclStatus.other, this.aclStatus.entries);
|
||||
}
|
||||
}
|
||||
337
extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts
Normal file
337
extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { HdfsModel } from '../hdfsModel';
|
||||
import { IFileSource } from '../../objectExplorerNodeProvider/fileSources';
|
||||
import { IAclStatus, AclEntry, AclEntryType } from '../../hdfs/aclEntry';
|
||||
import { cssStyles } from './uiConstants';
|
||||
import { ownersHeader, permissionsHeader, stickyHeader, readHeader, writeHeader, executeHeader, manageAccessTitle, allOthersHeader, addUserOrGroupHeader, enterNamePlaceholder, addLabel, accessHeader, defaultHeader, applyText, applyRecursivelyText, userLabel, groupLabel, errorApplyingAclChanges } from '../../localizedConstants';
|
||||
import { HdfsError } from '../webhdfs';
|
||||
import { ApiWrapper } from '../../apiWrapper';
|
||||
|
||||
const permissionsNameColumnWidth = 250;
|
||||
const permissionsStickyColumnWidth = 50;
|
||||
const permissionsReadColumnWidth = 50;
|
||||
const permissionsWriteColumnWidth = 50;
|
||||
const permissionsExecuteColumnWidth = 50;
|
||||
const permissionsDeleteColumnWidth = 50;
|
||||
|
||||
const permissionsRowHeight = 75;
|
||||
|
||||
export class ManageAccessDialog {
|
||||
|
||||
private hdfsModel: HdfsModel;
|
||||
private viewInitialized: boolean = false;
|
||||
|
||||
private modelBuilder: azdata.ModelBuilder;
|
||||
private rootLoadingComponent: azdata.LoadingComponent;
|
||||
private ownersPermissionsContainer: azdata.FlexContainer;
|
||||
private othersPermissionsContainer: azdata.FlexContainer;
|
||||
private namedUsersAndGroupsPermissionsContainer: azdata.FlexContainer;
|
||||
private addUserOrGroupInput: azdata.InputBoxComponent;
|
||||
private dialog: azdata.window.Dialog;
|
||||
|
||||
private addUserOrGroupSelectedType: AclEntryType;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
public openDialog(): void {
|
||||
if (!this.dialog) {
|
||||
this.dialog = this.apiWrapper.createDialog(manageAccessTitle, 'HdfsManageAccess', true);
|
||||
this.dialog.okButton.label = applyText;
|
||||
|
||||
const applyRecursivelyButton = azdata.window.createButton(applyRecursivelyText);
|
||||
applyRecursivelyButton.onClick(async () => {
|
||||
try {
|
||||
await this.hdfsModel.apply(true);
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(errorApplyingAclChanges(err instanceof HdfsError ? err.message : err));
|
||||
}
|
||||
});
|
||||
this.dialog.customButtons = [applyRecursivelyButton];
|
||||
this.dialog.registerCloseValidator(async (): Promise<boolean> => {
|
||||
try {
|
||||
await this.hdfsModel.apply();
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(errorApplyingAclChanges(err instanceof HdfsError ? err.message : err));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const tab = azdata.window.createTab(manageAccessTitle);
|
||||
tab.registerContent(async (modelView: azdata.ModelView) => {
|
||||
this.modelBuilder = modelView.modelBuilder;
|
||||
|
||||
const rootContainer = modelView.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column', width: '100%', height: '100%' })
|
||||
.component();
|
||||
|
||||
this.rootLoadingComponent = modelView.modelBuilder.loadingComponent().withItem(rootContainer).component();
|
||||
|
||||
const pathLabel = modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: this.hdfsPath }).component();
|
||||
rootContainer.addItem(pathLabel, { flex: '0 0 auto' });
|
||||
|
||||
// =====================
|
||||
// = Permissions Title =
|
||||
// =====================
|
||||
const permissionsTitle = modelView.modelBuilder.text()
|
||||
.withProperties<azdata.TextComponentProperties>({ value: permissionsHeader, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } })
|
||||
.component();
|
||||
rootContainer.addItem(permissionsTitle, { CSSStyles: { 'margin-top': '15px', 'padding-left': '10px', ...cssStyles.titleCss } });
|
||||
|
||||
// ==============================
|
||||
// = Owners permissions section =
|
||||
// ==============================
|
||||
|
||||
const ownersPermissionsHeaderRow = createPermissionsHeaderRow(modelView.modelBuilder, ownersHeader, true);
|
||||
rootContainer.addItem(ownersPermissionsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
|
||||
|
||||
// Empty initially - this is going to eventually be populated with the owner/owning group permissions
|
||||
this.ownersPermissionsContainer = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
rootContainer.addItem(this.ownersPermissionsContainer);
|
||||
|
||||
// ==============================
|
||||
// = Others permissions section =
|
||||
// ==============================
|
||||
|
||||
const othersPermissionsHeaderRow = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
|
||||
const ownersHeaderCell = modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: allOthersHeader }).component();
|
||||
othersPermissionsHeaderRow.addItem(ownersHeaderCell, { flex: '1 1 auto', CSSStyles: { 'width': `${permissionsNameColumnWidth}px`, 'min-width': `${permissionsNameColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
|
||||
rootContainer.addItem(othersPermissionsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
|
||||
|
||||
// Empty initially - this is eventually going to be populated with the "Everyone" permissions
|
||||
this.othersPermissionsContainer = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
rootContainer.addItem(this.othersPermissionsContainer);
|
||||
|
||||
// ===========================
|
||||
// = Add User Or Group Input =
|
||||
// ===========================
|
||||
|
||||
const addUserOrGroupTitle = modelView.modelBuilder.text()
|
||||
.withProperties<azdata.TextComponentProperties>({ value: addUserOrGroupHeader, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } })
|
||||
.component();
|
||||
rootContainer.addItem(addUserOrGroupTitle, { CSSStyles: { 'margin-top': '15px', 'padding-left': '10px', ...cssStyles.titleCss } });
|
||||
|
||||
const typeContainer = modelView.modelBuilder.flexContainer().withProperties({ flexFlow: 'row' }).component();
|
||||
const aclEntryTypeGroup = 'aclEntryType';
|
||||
const userTypeButton = this.createRadioButton(modelView.modelBuilder, userLabel, aclEntryTypeGroup, AclEntryType.user);
|
||||
const groupTypeButton = this.createRadioButton(modelView.modelBuilder, groupLabel, aclEntryTypeGroup, AclEntryType.group);
|
||||
userTypeButton.checked = true;
|
||||
this.addUserOrGroupSelectedType = AclEntryType.user;
|
||||
|
||||
typeContainer.addItems([userTypeButton, groupTypeButton], { flex: '0 0 auto' });
|
||||
rootContainer.addItem(typeContainer, { flex: '0 0 auto' });
|
||||
const addUserOrGroupInputRow = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
|
||||
|
||||
this.addUserOrGroupInput = modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({ inputType: 'text', placeHolder: enterNamePlaceholder, width: 250 }).component();
|
||||
const addUserOrGroupButton = modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({ label: addLabel, width: 75 }).component();
|
||||
addUserOrGroupButton.onDidClick(() => {
|
||||
this.hdfsModel.createAndAddAclEntry(this.addUserOrGroupInput.value, this.addUserOrGroupSelectedType);
|
||||
this.addUserOrGroupInput.value = '';
|
||||
});
|
||||
addUserOrGroupButton.enabled = false; // Init to disabled since we don't have any name entered in yet
|
||||
this.addUserOrGroupInput.onTextChanged(() => {
|
||||
if (this.addUserOrGroupInput.value === '') {
|
||||
addUserOrGroupButton.enabled = false;
|
||||
} else {
|
||||
addUserOrGroupButton.enabled = true;
|
||||
}
|
||||
});
|
||||
addUserOrGroupInputRow.addItem(this.addUserOrGroupInput, { flex: '0 0 auto' });
|
||||
addUserOrGroupInputRow.addItem(addUserOrGroupButton, { flex: '0 0 auto', CSSStyles: { 'margin-left': '20px' } });
|
||||
|
||||
rootContainer.addItem(addUserOrGroupInputRow);
|
||||
|
||||
// =================================================
|
||||
// = Named Users and Groups permissions header row =
|
||||
// =================================================
|
||||
|
||||
const namedUsersAndGroupsPermissionsHeaderRow = createPermissionsHeaderRow(modelView.modelBuilder, ownersHeader, false);
|
||||
rootContainer.addItem(namedUsersAndGroupsPermissionsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
|
||||
|
||||
// Empty initially - this is eventually going to be populated with the ACL entries set for this path
|
||||
this.namedUsersAndGroupsPermissionsContainer = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
rootContainer.addItem(this.namedUsersAndGroupsPermissionsContainer);
|
||||
|
||||
this.viewInitialized = true;
|
||||
this.handleAclStatusUpdated(this.hdfsModel.aclStatus);
|
||||
await modelView.initializeModel(this.rootLoadingComponent);
|
||||
});
|
||||
this.dialog.content = [tab];
|
||||
}
|
||||
|
||||
azdata.window.openDialog(this.dialog);
|
||||
}
|
||||
|
||||
private handleAclStatusUpdated(aclStatus: IAclStatus): void {
|
||||
if (!aclStatus || !this.viewInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Owners
|
||||
const ownerPermissionsRow = this.createOwnerPermissionsRow(this.modelBuilder, aclStatus.stickyBit, aclStatus.owner);
|
||||
const owningGroupPermissionsRow = this.createPermissionsRow(this.modelBuilder, aclStatus.group, false);
|
||||
this.ownersPermissionsContainer.clearItems();
|
||||
this.ownersPermissionsContainer.addItems([ownerPermissionsRow, owningGroupPermissionsRow], { CSSStyles: { 'border-bottom': cssStyles.tableBorder, 'border-top': cssStyles.tableBorder } });
|
||||
|
||||
// Others
|
||||
const otherPermissionsRow = this.createPermissionsRow(this.modelBuilder, aclStatus.other, false);
|
||||
this.othersPermissionsContainer.clearItems();
|
||||
this.othersPermissionsContainer.addItem(otherPermissionsRow, { CSSStyles: { 'border-bottom': cssStyles.tableBorder, 'border-top': cssStyles.tableBorder } });
|
||||
|
||||
this.namedUsersAndGroupsPermissionsContainer.clearItems();
|
||||
// Named users and groups
|
||||
aclStatus.entries.forEach(entry => {
|
||||
const namedEntryRow = this.createPermissionsRow(this.modelBuilder, entry, true);
|
||||
this.namedUsersAndGroupsPermissionsContainer.addItem(namedEntryRow, { CSSStyles: { 'border-bottom': cssStyles.tableBorder, 'border-top': cssStyles.tableBorder } });
|
||||
});
|
||||
|
||||
this.rootLoadingComponent.loading = false;
|
||||
}
|
||||
|
||||
private createRadioButton(modelBuilder: azdata.ModelBuilder, label: string, name: string, aclEntryType: AclEntryType): azdata.RadioButtonComponent {
|
||||
const button = modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({ label: label, name: name }).component();
|
||||
button.onDidClick(() => {
|
||||
this.addUserOrGroupSelectedType = aclEntryType;
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
private createOwnerPermissionsRow(builder: azdata.ModelBuilder, sticky: boolean, entry: AclEntry): azdata.FlexContainer {
|
||||
const row = this.createPermissionsRow(builder, entry, false);
|
||||
const stickyCheckbox = builder.checkBox().withProperties({ checked: sticky, height: 25, width: 25 }).component();
|
||||
const stickyContainer = builder.flexContainer().withLayout({ width: permissionsReadColumnWidth }).withItems([stickyCheckbox]).component();
|
||||
// Insert after name item but before other checkboxes
|
||||
row.insertItem(stickyContainer, 1, { 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();
|
||||
const nameCell = builder.text().withProperties({ value: entry.displayName }).component();
|
||||
rowContainer.addItem(nameCell);
|
||||
|
||||
// Access - Read
|
||||
const accessReadComponents = createCheckbox(builder, entry.permission.read, permissionsReadColumnWidth, permissionsRowHeight);
|
||||
rowContainer.addItem(accessReadComponents.container, { flex: '0 0 auto' });
|
||||
accessReadComponents.checkbox.onChanged(() => {
|
||||
entry.permission.read = accessReadComponents.checkbox.checked;
|
||||
});
|
||||
|
||||
// Access - Write
|
||||
const accessWriteComponents = createCheckbox(builder, entry.permission.write, permissionsWriteColumnWidth, permissionsRowHeight);
|
||||
rowContainer.addItem(accessWriteComponents.container, { flex: '0 0 auto' });
|
||||
accessWriteComponents.checkbox.onChanged(() => {
|
||||
entry.permission.write = accessWriteComponents.checkbox.checked;
|
||||
});
|
||||
|
||||
// Access - Execute
|
||||
const accessExecuteComponents = createCheckbox(builder, entry.permission.execute, permissionsExecuteColumnWidth, permissionsRowHeight);
|
||||
rowContainer.addItem(accessExecuteComponents.container, { flex: '0 0 auto', CSSStyles: { 'border-right': cssStyles.tableBorder } });
|
||||
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
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
const deleteContainer = builder.flexContainer().withLayout({ width: permissionsDeleteColumnWidth }).component();
|
||||
|
||||
if (includeDelete) {
|
||||
const deleteButton = builder.button().withProperties<azdata.ButtonProperties>({ label: 'Delete' }).component();
|
||||
deleteButton.onDidClick(() => { this.hdfsModel.deleteAclEntry(entry); });
|
||||
deleteContainer.addItem(deleteButton, { flex: '0 0 auto' });
|
||||
}
|
||||
rowContainer.addItem(deleteContainer, { flex: '0 0 auto' });
|
||||
|
||||
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();
|
||||
|
||||
// Section Headers
|
||||
const sectionHeaderContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', justifyContent: 'flex-end' }).component();
|
||||
const accessSectionHeader = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: accessHeader }).component();
|
||||
sectionHeaderContainer.addItem(accessSectionHeader, { CSSStyles: { 'width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
const defaultSectionHeader = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: defaultHeader }).component();
|
||||
sectionHeaderContainer.addItem(defaultSectionHeader, { CSSStyles: { 'width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
|
||||
rowsContainer.addItem(sectionHeaderContainer);
|
||||
|
||||
// Table headers
|
||||
const headerRowContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
|
||||
const ownersCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ 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<azdata.TextComponentProperties>({ value: stickyHeader }).component();
|
||||
headerRowContainer.addItem(stickyCell, { CSSStyles: { 'width': `${permissionsStickyColumnWidth}px`, 'min-width': `${permissionsStickyColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
}
|
||||
|
||||
// Access Permissions Group
|
||||
const accessReadCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: readHeader }).component();
|
||||
headerRowContainer.addItem(accessReadCell, { CSSStyles: { 'width': `${permissionsReadColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
const accessWriteCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: writeHeader }).component();
|
||||
headerRowContainer.addItem(accessWriteCell, { CSSStyles: { 'width': `${permissionsWriteColumnWidth}px`, 'min-width': `${permissionsWriteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
const accessExecuteCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: executeHeader }).component();
|
||||
headerRowContainer.addItem(accessExecuteCell, { CSSStyles: { 'width': `${permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsExecuteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
// Default Permissions Group
|
||||
const defaultReadCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: readHeader }).component();
|
||||
headerRowContainer.addItem(defaultReadCell, { CSSStyles: { 'width': `${permissionsReadColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
const defaultWriteCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: writeHeader }).component();
|
||||
headerRowContainer.addItem(defaultWriteCell, { CSSStyles: { 'width': `${permissionsWriteColumnWidth}px`, 'min-width': `${permissionsWriteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
const defaultExecuteCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: executeHeader }).component();
|
||||
headerRowContainer.addItem(defaultExecuteCell, { CSSStyles: { 'width': `${permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsExecuteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
|
||||
// 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 } {
|
||||
const checkbox = builder.checkBox().withProperties({ checked: checked, height: 25, width: 25 }).component();
|
||||
return {
|
||||
container: builder.flexContainer()
|
||||
.withLayout({ width: containerWidth, height: containerHeight })
|
||||
.withItems([checkbox])
|
||||
.component(),
|
||||
checkbox: checkbox
|
||||
};
|
||||
|
||||
}
|
||||
10
extensions/mssql/src/hdfs/ui/uiConstants.ts
Normal file
10
extensions/mssql/src/hdfs/ui/uiConstants.ts
Normal file
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export namespace cssStyles {
|
||||
export const tableBorder = '1px solid #ccc';
|
||||
export const titleCss = { 'font-size': '20px', 'font-weight': '600' };
|
||||
export const tableHeaderCss = { 'font-weight': 'bold', 'text-transform': 'uppercase', 'font-size': '10px', 'user-select': 'text' };
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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 { everyoneName } from '../localizedConstants';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const ErrorMessageInvalidDataStructure = localize('webhdfs.invalidDataStructure', "Invalid Data Structure");
|
||||
@@ -435,11 +436,11 @@ export class WebHDFS {
|
||||
} 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, response.body.AclStatus.other || '', permissions.other),
|
||||
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: (<any[]>response.body.AclStatus.entries).map(entry => parseAcl(entry)).reduce((acc, parsedEntries) => acc.concat(parsedEntries, []))
|
||||
entries: (<any[]>response.body.AclStatus.entries).map(entry => parseAcl(entry)).reduce((acc, parsedEntries) => acc.concat(parsedEntries), [])
|
||||
};
|
||||
callback(undefined, aclStatus);
|
||||
} else {
|
||||
@@ -449,16 +450,22 @@ export class WebHDFS {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ACL for the given path
|
||||
* Set ACL for the given path. The owner, group and other fields are required - other entries are optional.
|
||||
* @param path The path to the file/folder to set the ACL on
|
||||
* @param aclEntries The ACL entries to set
|
||||
* @param ownerEntry The entry corresponding to the path owner
|
||||
* @param groupEntry The entry corresponding to the path owning group
|
||||
* @param otherEntry The entry corresponding to default permissions for all other users
|
||||
* @param aclEntries The optional additional ACL entries to set
|
||||
* @param callback Callback to handle the response
|
||||
* @returns void
|
||||
*/
|
||||
public setAcl(path: string, aclEntries: AclEntry[], callback: (error: HdfsError) => void): void {
|
||||
public setAcl(path: string, ownerEntry: AclEntry, groupEntry: AclEntry, otherEntry: AclEntry, aclEntries: AclEntry[], callback: (error: HdfsError) => void): void {
|
||||
this.checkArgDefined('path', path);
|
||||
this.checkArgDefined('ownerEntry', ownerEntry);
|
||||
this.checkArgDefined('groupEntry', groupEntry);
|
||||
this.checkArgDefined('otherEntry', otherEntry);
|
||||
this.checkArgDefined('aclEntries', aclEntries);
|
||||
const aclSpec = aclEntries.join(',');
|
||||
const aclSpec = [ownerEntry, groupEntry, otherEntry].concat(aclEntries).map(entry => entry.toAclString()).join(',');
|
||||
let endpoint = this.getOperationEndpoint('setacl', path, { aclspec: aclSpec });
|
||||
this.sendRequest('PUT', endpoint, undefined, (error) => {
|
||||
return callback && callback(error);
|
||||
|
||||
@@ -3,14 +3,36 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
// HDFS Constants //////////////////////////////////////////////////////////
|
||||
export const msgMissingNodeContext = localize('msgMissingNodeContext', 'Node Command called without any node passed');
|
||||
|
||||
// HDFS Manage Access Dialog Constants ////////////////////////////////////
|
||||
|
||||
export const manageAccessTitle = localize('mssql.manageAccessTitle', "Manage Access");
|
||||
export const permissionsHeader = localize('mssql.permissionsTitle', "Permissions");
|
||||
export const ownersHeader = localize('mssql.ownersHeader', "Owners");
|
||||
export const allOthersHeader = localize('mssql.allOthersHeader', "All Others");
|
||||
export const everyoneName = localize('mssql.everyone', "Everyone");
|
||||
export const userLabel = localize('mssql.userLabel', "User");
|
||||
export const groupLabel = localize('mssql.groupLabel', "Group");
|
||||
export const accessHeader = localize('mssql.accessHeader', "Access");
|
||||
export const defaultHeader = localize('mssql.defaultHeader', "Default");
|
||||
export const stickyHeader = localize('mssql.stickyHeader', "Sticky");
|
||||
export const readHeader = localize('mssql.readHeader', "Read");
|
||||
export const writeHeader = localize('mssql.writeHeader', "Write");
|
||||
export const executeHeader = localize('mssql.executeHeader', "Execute");
|
||||
export const addUserOrGroupHeader = localize('mssql.addUserOrGroup', "Add User or Group");
|
||||
export const enterNamePlaceholder = localize('mssql.enterNamePlaceholder', "Enter name");
|
||||
export const addLabel = localize('mssql.addLabel', "Add");
|
||||
export const namedUsersAndGroupsHeader = localize('mssql.namedUsersAndGroups', "Named Users and Groups");
|
||||
export const applyText = localize('mssql.apply', "Apply");
|
||||
export const applyRecursivelyText = localize('mssql.applyRecursively', "Apply Recursively");
|
||||
|
||||
export function errorApplyingAclChanges(errMsg: string): string { return localize('mssql.errorApplyingAclChanges', "Unexpected error occurred while applying changes : {0}", errMsg); }
|
||||
|
||||
// Spark Job Submission Constants //////////////////////////////////////////
|
||||
export const sparkLocalFileDestinationHint = localize('sparkJobSubmission_LocalFileDestinationHint', 'Local file will be uploaded to HDFS. ');
|
||||
export const sparkJobSubmissionEndMessage = localize('sparkJobSubmission_SubmissionEndMessage', '.......................... Submit Spark Job End ............................');
|
||||
|
||||
@@ -13,7 +13,7 @@ import ContextProvider from './contextProvider';
|
||||
import * as Utils from './utils';
|
||||
import { AppContext } from './appContext';
|
||||
import { ApiWrapper } from './apiWrapper';
|
||||
import { UploadFilesCommand, MkDirCommand, SaveFileCommand, PreviewFileCommand, CopyPathCommand, DeleteFilesCommand } from './objectExplorerNodeProvider/hdfsCommands';
|
||||
import { UploadFilesCommand, MkDirCommand, SaveFileCommand, PreviewFileCommand, CopyPathCommand, DeleteFilesCommand, ManageAccessCommand } from './objectExplorerNodeProvider/hdfsCommands';
|
||||
import { IPrompter } from './prompts/question';
|
||||
import CodeAdapter from './prompts/adapter';
|
||||
import { IExtension } from './mssql';
|
||||
@@ -99,6 +99,7 @@ function registerHdfsCommands(context: vscode.ExtensionContext, prompter: IPromp
|
||||
context.subscriptions.push(new PreviewFileCommand(prompter, appContext));
|
||||
context.subscriptions.push(new CopyPathCommand(appContext));
|
||||
context.subscriptions.push(new DeleteFilesCommand(prompter, appContext));
|
||||
context.subscriptions.push(new ManageAccessCommand(appContext));
|
||||
}
|
||||
|
||||
function activateSparkFeatures(appContext: AppContext): void {
|
||||
|
||||
@@ -65,8 +65,20 @@ export interface IFileSource {
|
||||
readFileLines(path: string, maxLines: number): Promise<Buffer>;
|
||||
writeFile(localFile: IFile, remoteDir: string): Promise<string>;
|
||||
delete(path: string, recursive?: boolean): Promise<void>;
|
||||
/**
|
||||
* Get ACL status for given path
|
||||
* @param path The path to the file/folder to get the status of
|
||||
*/
|
||||
getAclStatus(path: string): Promise<IAclStatus>;
|
||||
setAcl(path: string, aclEntries: AclEntry[]): Promise<void>;
|
||||
/**
|
||||
* Sets the ACL status for given path
|
||||
* @param path The path to the file/folder to set the ACL on
|
||||
* @param ownerEntry The entry corresponding to the path owner
|
||||
* @param groupEntry The entry corresponding to the path owning group
|
||||
* @param otherEntry The entry corresponding to default permissions for all other users
|
||||
* @param aclEntries The ACL entries to set
|
||||
*/
|
||||
setAcl(path: string, ownerEntry: AclEntry, groupEntry: AclEntry, otherEntry: AclEntry, aclEntries: AclEntry[]): Promise<void>;
|
||||
exists(path: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -323,11 +335,14 @@ export class HdfsFileSource implements IFileSource {
|
||||
/**
|
||||
* Sets the ACL status for given path
|
||||
* @param path The path to the file/folder to set the ACL on
|
||||
* @param ownerEntry The entry corresponding to the path owner
|
||||
* @param groupEntry The entry corresponding to the path owning group
|
||||
* @param otherEntry The entry corresponding to default permissions for all other users
|
||||
* @param aclEntries The ACL entries to set
|
||||
*/
|
||||
public setAcl(path: string, aclEntries: AclEntry[]): Promise<void> {
|
||||
public setAcl(path: string, ownerEntry: AclEntry, groupEntry: AclEntry, otherEntry: AclEntry, aclEntries: AclEntry[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.setAcl(path, aclEntries, (error: HdfsError) => {
|
||||
this.client.setAcl(path, ownerEntry, groupEntry, otherEntry, aclEntries, (error: HdfsError) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
|
||||
@@ -21,6 +21,7 @@ import * as utils from '../utils';
|
||||
import { AppContext } from '../appContext';
|
||||
import { TreeNode } from './treeNodes';
|
||||
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider';
|
||||
import { ManageAccessDialog } from '../hdfs/ui/hdfsManageAccessDialog';
|
||||
|
||||
async function getSaveableUri(apiWrapper: ApiWrapper, fileName: string, isPreview?: boolean): Promise<vscode.Uri> {
|
||||
let root = utils.getUserHome();
|
||||
@@ -384,3 +385,24 @@ export class CopyPathCommand extends Command {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ManageAccessCommand extends Command {
|
||||
|
||||
constructor(appContext: AppContext) {
|
||||
super('mssqlCluster.manageAccess', appContext);
|
||||
}
|
||||
|
||||
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
|
||||
try {
|
||||
let node = await getNode<HdfsFileSourceNode>(context, this.appContext);
|
||||
if (node) {
|
||||
new ManageAccessDialog(node.hdfsPath, node.fileSource, this.apiWrapper).openDialog();
|
||||
} else {
|
||||
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
|
||||
}
|
||||
} catch (err) {
|
||||
this.apiWrapper.showErrorMessage(
|
||||
localize('manageAccessError', "An unexpected error occurred while opening the Manage Access dialog: {0}", utils.getErrorMessage(err, true)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export class HdfsProvider implements vscode.TreeDataProvider<TreeNode>, ITreeCha
|
||||
}
|
||||
|
||||
export abstract class HdfsFileSourceNode extends TreeNode {
|
||||
constructor(protected context: TreeDataContext, protected _path: string, protected fileSource: IFileSource) {
|
||||
constructor(protected context: TreeDataContext, protected _path: string, public readonly fileSource: IFileSource) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user