Initial Move from Azure DevOps to GitHub

This commit is contained in:
Steve Faulkner
2020-05-25 21:30:55 -05:00
commit 36581fb6d9
986 changed files with 195242 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import * as Entities from "../../Tables/Entities";
import * as TableConstants from "../../Tables/Constants";
import * as Utilities from "../../Tables/Utilities";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import TableEntityPane from "./TableEntityPane";
export default class AddTableEntityPane extends TableEntityPane implements ViewModels.AddTableEntityPane {
private static _excludedFields: string[] = [TableConstants.EntityKeyNames.Timestamp];
private static _readonlyFields: string[] = [
TableConstants.EntityKeyNames.PartitionKey,
TableConstants.EntityKeyNames.RowKey,
TableConstants.EntityKeyNames.Timestamp
];
public enterRequiredValueLabel = "Enter identifier value."; // localize
public enterValueLabel = "Enter value to keep property."; // localize
constructor(options: ViewModels.PaneOptions) {
super(options);
this.submitButtonText("Add Entity");
this.container.isPreferredApiCassandra.subscribe(isCassandra => {
if (isCassandra) {
this.submitButtonText("Add Row");
}
});
this.scrollId = ko.observable<string>("addEntityScroll");
}
public submit() {
if (!this.canApply()) {
return;
}
let entity: Entities.ITableEntity = this.entityFromAttributes(this.displayedAttributes());
this.container.tableDataClient
.createDocument(this.tableViewModel.queryTablesTab.collection, entity)
.then((newEntity: Entities.ITableEntity) => {
this.tableViewModel.addEntityToCache(newEntity).then(() => {
if (!this.tryInsertNewHeaders(this.tableViewModel, newEntity)) {
this.tableViewModel.redrawTableThrottled();
}
});
this.close();
});
}
public open() {
var headers = this.tableViewModel.headers;
if (DataTableUtilities.checkForDefaultHeader(headers)) {
headers = [];
if (this.container.isPreferredApiTable()) {
headers = [TableConstants.EntityKeyNames.PartitionKey, TableConstants.EntityKeyNames.RowKey];
}
}
if (this.container.isPreferredApiCassandra()) {
(<CassandraAPIDataClient>this.container.tableDataClient)
.getTableSchema(this.tableViewModel.queryTablesTab.collection)
.then((columns: CassandraTableKey[]) => {
this.displayedAttributes(
this.constructDisplayedAttributes(
columns.map(col => col.property),
Utilities.getDataTypesFromCassandraSchema(columns)
)
);
this.updateIsActionEnabled();
super.open();
});
} else {
this.displayedAttributes(
this.constructDisplayedAttributes(
headers,
Utilities.getDataTypesFromEntities(headers, this.tableViewModel.items())
)
);
this.updateIsActionEnabled();
super.open();
}
const focusElement = document.getElementById("addTableEntityValue");
focusElement && focusElement.focus();
}
private constructDisplayedAttributes(headers: string[], dataTypes: any): EntityPropertyViewModel[] {
var displayedAttributes: EntityPropertyViewModel[] = [];
headers &&
headers.forEach((key: string) => {
if (!_.contains<string>(AddTableEntityPane._excludedFields, key)) {
if (this.container.isPreferredApiCassandra()) {
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map(key => key.property);
var isRequired: boolean = _.contains<string>(cassandraKeys, key);
var editable: boolean = false;
var placeholderLabel: string = isRequired ? this.enterRequiredValueLabel : this.enterValueLabel;
var entityAttributeType: string = dataTypes[key] || TableConstants.CassandraType.Text; // Default to String if there is no type specified.
// TODO figure out validation story for blob and Inet so we can allow adding/editing them
const nonEditableType: boolean =
entityAttributeType === TableConstants.CassandraType.Blob ||
entityAttributeType === TableConstants.CassandraType.Inet;
var entity: EntityPropertyViewModel = new EntityPropertyViewModel(
this,
key,
entityAttributeType,
"", // default to empty string
/* namePlaceholder */ undefined,
nonEditableType ? "Type is not editable via DataExplorer." : placeholderLabel,
editable,
/* default valid name */ true,
/* default valid value */ true,
/* required value */ isRequired,
/* removable */ false,
/* valueEditable */ !nonEditableType,
/* ignoreEmptyValue */ true
);
} else {
var isRequired: boolean = _.contains<string>(AddTableEntityPane.requiredFieldsForTablesAPI, key);
var editable: boolean = !_.contains<string>(AddTableEntityPane._readonlyFields, key);
var placeholderLabel: string = isRequired ? this.enterRequiredValueLabel : this.enterValueLabel;
var entityAttributeType: string = dataTypes[key] || TableConstants.TableType.String; // Default to String if there is no type specified.
var entity: EntityPropertyViewModel = new EntityPropertyViewModel(
this,
key,
entityAttributeType,
"", // default to empty string
/* namePlaceholder */ undefined,
placeholderLabel,
editable,
/* default valid name */ true,
/* default valid value */ true,
/* required value */ isRequired,
/* removable */ editable,
/* valueEditable */ true,
/* ignoreEmptyValue */ true
);
}
displayedAttributes.push(entity);
}
});
return displayedAttributes;
}
}

View File

@@ -0,0 +1,228 @@
import * as ko from "knockout";
import _ from "underscore";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CassandraTableKey, CassandraAPIDataClient } from "../../Tables/TableDataClient";
import * as Entities from "../../Tables/Entities";
import TableEntityPane from "./TableEntityPane";
import * as Utilities from "../../Tables/Utilities";
import * as TableConstants from "../../Tables/Constants";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
export default class EditTableEntityPane extends TableEntityPane implements ViewModels.EditTableEntityPane {
container: ViewModels.Explorer;
visible: ko.Observable<boolean>;
public originEntity: Entities.ITableEntity;
public originalNumberOfProperties: number;
private originalDocument: any;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.submitButtonText("Update Entity");
this.container.isPreferredApiCassandra.subscribe(isCassandra => {
if (isCassandra) {
this.submitButtonText("Update Row");
}
});
this.scrollId = ko.observable<string>("editEntityScroll");
}
public submit() {
if (!this.canApply()) {
return;
}
let entity: Entities.ITableEntity = this.updateEntity(this.displayedAttributes());
this.container.tableDataClient
.updateDocument(this.tableViewModel.queryTablesTab.collection, this.originalDocument, entity)
.then((newEntity: Entities.ITableEntity) => {
var numberOfProperties = 0;
for (var property in newEntity) {
if (
property !== TableEntityProcessor.keyProperties.attachments &&
property !== TableEntityProcessor.keyProperties.etag &&
property !== TableEntityProcessor.keyProperties.resourceId &&
property !== TableEntityProcessor.keyProperties.self &&
(!this.container.isPreferredApiCassandra() || property !== TableConstants.EntityKeyNames.RowKey)
) {
numberOfProperties++;
}
}
var propertiesDelta = numberOfProperties - this.originalNumberOfProperties;
return this.tableViewModel
.updateCachedEntity(newEntity)
.then(() => {
if (!this.tryInsertNewHeaders(this.tableViewModel, newEntity)) {
this.tableViewModel.redrawTableThrottled();
}
})
.then(() => {
// Selecting updated entity
this.tableViewModel.selected.removeAll();
this.tableViewModel.selected.push(newEntity);
});
});
this.close();
}
public open() {
this.displayedAttributes(this.constructDisplayedAttributes(this.originEntity));
if (this.container.isPreferredApiTable()) {
this.originalDocument = TableEntityProcessor.convertEntitiesToDocuments(
[<Entities.ITableEntityForTablesAPI>this.originEntity],
this.tableViewModel.queryTablesTab.collection
)[0]; // TODO change for Cassandra
this.originalDocument.id = ko.observable<string>(this.originalDocument.id);
} else {
this.originalDocument = this.originEntity;
}
this.updateIsActionEnabled();
super.open();
}
private constructDisplayedAttributes(entity: Entities.ITableEntity): EntityPropertyViewModel[] {
var displayedAttributes: EntityPropertyViewModel[] = [];
const keys = Object.keys(entity);
keys &&
keys.forEach((key: string) => {
if (
key !== TableEntityProcessor.keyProperties.attachments &&
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(!this.container.isPreferredApiCassandra() || key !== TableConstants.EntityKeyNames.RowKey)
) {
if (this.container.isPreferredApiCassandra()) {
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map(key => key.property);
var entityAttribute: Entities.ITableEntityAttribute = entity[key];
var entityAttributeType: string = entityAttribute.$;
var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType);
var removable: boolean = false;
// TODO figure out validation story for blob and Inet so we can allow adding/editing them
const nonEditableType: boolean =
entityAttributeType === TableConstants.CassandraType.Blob ||
entityAttributeType === TableConstants.CassandraType.Inet;
displayedAttributes.push(
new EntityPropertyViewModel(
this,
key,
entityAttributeType,
displayValue,
/* namePlaceholder */ undefined,
/* valuePlaceholder */ undefined,
false,
/* default valid name */ true,
/* default valid value */ true,
/* isRequired */ false,
removable,
/*value editable*/ !_.contains<string>(cassandraKeys, key) && !nonEditableType
)
);
} else {
var entityAttribute: Entities.ITableEntityAttribute = entity[key];
var entityAttributeType: string = entityAttribute.$;
var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType);
var editable: boolean = this.isAttributeEditable(key, entityAttributeType);
// As per VSO:189935, Binary properties are read-only, we still want to be able to remove them.
var removable: boolean = editable || entityAttributeType === TableConstants.TableType.Binary;
displayedAttributes.push(
new EntityPropertyViewModel(
this,
key,
entityAttributeType,
displayValue,
/* namePlaceholder */ undefined,
/* valuePlaceholder */ undefined,
editable,
/* default valid name */ true,
/* default valid value */ true,
/* isRequired */ false,
removable
)
);
}
}
});
if (this.container.isPreferredApiCassandra()) {
(<CassandraAPIDataClient>this.container.tableDataClient)
.getTableSchema(this.tableViewModel.queryTablesTab.collection)
.then((properties: CassandraTableKey[]) => {
properties &&
properties.forEach(property => {
if (!_.contains(keys, property.property)) {
this.insertAttribute(property.property, property.type);
}
});
});
}
return displayedAttributes;
}
private updateEntity(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity {
var updatedEntity: any = {};
displayedAttributes &&
displayedAttributes.forEach((attribute: EntityPropertyViewModel) => {
if (
attribute.name() &&
(!this.tableViewModel.queryTablesTab.container.isPreferredApiCassandra() || attribute.value() !== "")
) {
var value = attribute.getPropertyValue();
var type = attribute.type();
if (type === TableConstants.TableType.Int64) {
value = Utilities.padLongWithZeros(value);
}
updatedEntity[attribute.name()] = {
_: value,
$: type
};
}
});
return updatedEntity;
}
private isAttributeEditable(attributeName: string, entityAttributeType: string) {
return !(
attributeName === TableConstants.EntityKeyNames.PartitionKey ||
attributeName === TableConstants.EntityKeyNames.RowKey ||
attributeName === TableConstants.EntityKeyNames.Timestamp ||
// As per VSO:189935, Making Binary properties read-only in Edit Entity dialog until we have a full story for it.
entityAttributeType === TableConstants.TableType.Binary
);
}
private getPropertyDisplayValue(entity: Entities.ITableEntity, name: string, type: string): any {
var attribute: Entities.ITableEntityAttribute = entity[name];
var displayValue: any = attribute._;
var isBinary: boolean = type === TableConstants.TableType.Binary;
// Showing the value in base64 for binary properties since that is what the Azure Storage Client Library expects.
// This means that, even if the Azure Storage API returns a byte[] of binary content, it needs that same array
// *base64 - encoded * as the value for the updated property or the whole update operation will fail.
if (isBinary && displayValue && $.isArray(displayValue.data)) {
var bytes: number[] = displayValue.data;
displayValue = this.getBase64DisplayValue(bytes);
}
return displayValue;
}
private getBase64DisplayValue(bytes: number[]): string {
var displayValue: string = null;
try {
var chars: string[] = bytes.map((byte: number) => String.fromCharCode(byte));
var toEncode: string = chars.join("");
displayValue = window.btoa(toEncode);
} catch (error) {
// Error
}
return displayValue;
}
}

View File

@@ -0,0 +1,164 @@
import * as ko from "knockout";
import * as DateTimeUtilities from "../../Tables/QueryBuilder/DateTimeUtilities";
import * as EntityPropertyNameValidator from "./Validators/EntityPropertyNameValidator";
import EntityPropertyValueValidator from "./Validators/EntityPropertyValueValidator";
import * as Constants from "../../Tables/Constants";
import * as Utilities from "../../Tables/Utilities";
import TableEntityPane from "./TableEntityPane";
export interface IValidationResult {
isInvalid: boolean;
help: string;
}
export interface IActionEnabledDialog {
updateIsActionEnabled: () => void;
}
/**
* View model for an entity proprety
*/
export default class EntityPropertyViewModel {
/* Constants */
public static noTooltip = "";
// Maximum number of custom properties, see Azure Service Data Model
// At https://msdn.microsoft.com/library/azure/dd179338.aspx
public static maximumNumberOfProperties = 252;
// Labels
public closeButtonLabel: string = "Close"; // localize
/* Observables */
public name: ko.Observable<string>;
public type: ko.Observable<string>;
public value: ko.Observable<any>;
public inputType: ko.Computed<string>;
public nameTooltip: ko.Observable<string>;
public isInvalidName: ko.Observable<boolean>;
public valueTooltip: ko.Observable<string>;
public isInvalidValue: ko.Observable<boolean>;
public namePlaceholder: ko.Observable<string>;
public valuePlaceholder: ko.Observable<string>;
public hasFocus: ko.Observable<boolean>;
public valueHasFocus: ko.Observable<boolean>;
public isDateType: ko.Computed<boolean>;
public editable: boolean; // If a property's name or type is editable, these two are always the same regarding editability.
public valueEditable: boolean; // If a property's value is editable, could be different from name or type.
public removable: boolean; // If a property is removable, usually, PartitionKey, RowKey and TimeStamp (if applicable) are not removable.
public isRequired: boolean; // If a property's value is required, used to differentiate the place holder label.
public ignoreEmptyValue: boolean;
/* Members */
private tableEntityPane: TableEntityPane;
private _validator: EntityPropertyValueValidator;
constructor(
tableEntityPane: TableEntityPane,
name: string,
type: string,
value: any,
namePlaceholder: string = "",
valuePlaceholder: string = "",
editable: boolean = false,
defaultValidName: boolean = true,
defaultValidValue: boolean = false,
isRequired: boolean = false,
removable: boolean = editable,
valueEditable: boolean = editable,
ignoreEmptyValue: boolean = false
) {
this.name = ko.observable<string>(name);
this.type = ko.observable<string>(type);
this.isDateType = ko.pureComputed<boolean>(() => this.type() === Constants.TableType.DateTime);
if (this.isDateType()) {
value = value ? DateTimeUtilities.getLocalDateTime(value) : value;
}
this.value = ko.observable(value);
this.inputType = ko.pureComputed<string>(() => {
if (!this.valueHasFocus() && !this.value() && this.isDateType()) {
return Constants.InputType.Text;
}
return Utilities.getInputTypeFromDisplayedName(this.type());
});
this.namePlaceholder = ko.observable<string>(namePlaceholder);
this.valuePlaceholder = ko.observable<string>(valuePlaceholder);
this.editable = editable;
this.isRequired = isRequired;
this.removable = removable;
this.valueEditable = valueEditable;
this._validator = new EntityPropertyValueValidator(isRequired);
this.tableEntityPane = tableEntityPane;
this.nameTooltip = ko.observable<string>(EntityPropertyViewModel.noTooltip);
this.isInvalidName = ko.observable<boolean>(!defaultValidName);
this.name.subscribe((name: string) => this.validateName(name));
if (!defaultValidName) {
this.validateName(name);
}
this.valueTooltip = ko.observable<string>(EntityPropertyViewModel.noTooltip);
this.isInvalidValue = ko.observable<boolean>(!defaultValidValue);
this.value.subscribe((value: string) => this.validateValue(value, this.type()));
if (!defaultValidValue) {
this.validateValue(value, type);
}
this.type.subscribe((type: string) => this.validateValue(this.value(), type));
this.hasFocus = ko.observable<boolean>(false);
this.valueHasFocus = ko.observable<boolean>(false);
}
/**
* Gets the Javascript value of the entity property based on its EDM type.
*/
public getPropertyValue(): any {
var value: string = this.value();
if (this.type() === Constants.TableType.DateTime) {
value = DateTimeUtilities.getUTCDateTime(value);
}
return this._validator.parseValue(value, this.type());
}
private validateName(name: string): void {
var result: IValidationResult = this.isInvalidNameInput(name);
this.isInvalidName(result.isInvalid);
this.nameTooltip(result.help);
this.namePlaceholder(result.help);
this.tableEntityPane.updateIsActionEnabled();
}
private validateValue(value: string, type: string): void {
var result: IValidationResult = this.isInvalidValueInput(value, type);
if (!result) {
return;
}
this.isInvalidValue(result.isInvalid);
this.valueTooltip(result.help);
this.valuePlaceholder(result.help);
this.tableEntityPane.updateIsActionEnabled();
}
private isInvalidNameInput(name: string): IValidationResult {
return EntityPropertyNameValidator.validate(name);
}
private isInvalidValueInput(value: string, type: string): IValidationResult {
if (this.ignoreEmptyValue && this.value() === "") {
return { isInvalid: false, help: "" };
}
return this._validator.validate(value, type);
}
}

View File

@@ -0,0 +1,174 @@
import * as ko from "knockout";
import _ from "underscore";
import * as Constants from "../../Tables/Constants";
import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ContextualPaneBase } from "../ContextualPaneBase";
export interface ISelectColumn {
columnName: ko.Observable<string>;
selected: ko.Observable<boolean>;
editable: ko.Observable<boolean>;
}
export class QuerySelectPane extends ContextualPaneBase implements ViewModels.QuerySelectPane {
public titleLabel: string = "Select Columns";
public instructionLabel: string = "Select the columns that you want to query.";
public availableColumnsTableQueryLabel: string = "Available Columns";
public noColumnSelectedWarning: string = "At least one column should be selected.";
public columnOptions: ko.ObservableArray<ISelectColumn>;
public anyColumnSelected: ko.Computed<boolean>;
public canSelectAll: ko.Computed<boolean>;
public allSelected: ko.Computed<boolean>;
private selectedColumnOption: ISelectColumn = null;
public queryViewModel: QueryViewModel;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.columnOptions = ko.observableArray<ISelectColumn>();
this.anyColumnSelected = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: ISelectColumn) => {
return value.selected();
});
});
this.canSelectAll = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: ISelectColumn) => {
return !value.selected();
});
});
this.allSelected = ko.pureComputed<boolean>({
read: () => {
return !this.canSelectAll();
},
write: value => {
if (value) {
this.selectAll();
} else {
this.clearAll();
}
},
owner: this
});
}
public submit() {
this.queryViewModel.selectText(this.getParameters());
this.queryViewModel.getSelectMessage();
this.close();
}
public open() {
this.setTableColumns(this.queryViewModel.columnOptions());
this.setDisplayedColumns(this.queryViewModel.selectText(), this.columnOptions());
super.open();
}
private getParameters(): string[] {
if (this.canSelectAll() === false) {
return [];
}
var selectedColumns = this.columnOptions().filter((value: ISelectColumn) => value.selected() === true);
var columns: string[] = selectedColumns.map((value: ISelectColumn) => {
var name: string = value.columnName();
return name;
});
return columns;
}
public setTableColumns(columnNames: string[]): void {
var columns: ISelectColumn[] = columnNames.map((value: string) => {
var columnOption: ISelectColumn = {
columnName: ko.observable<string>(value),
selected: ko.observable<boolean>(true),
editable: ko.observable<boolean>(this.isEntityEditable(value))
};
return columnOption;
});
this.columnOptions(columns);
}
public setDisplayedColumns(querySelect: string[], columns: ISelectColumn[]): void {
if (querySelect == null || _.isEmpty(querySelect)) {
return;
}
this.setSelected(querySelect, columns);
}
private setSelected(querySelect: string[], columns: ISelectColumn[]): void {
this.clearAll();
querySelect &&
querySelect.forEach((value: string) => {
for (var i = 0; i < columns.length; i++) {
if (value === columns[i].columnName()) {
columns[i].selected(true);
}
}
});
}
public availableColumnsCheckboxClick(): boolean {
if (this.canSelectAll()) {
return this.selectAll();
} else {
return this.clearAll();
}
}
public selectAll(): boolean {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: ISelectColumn) => {
value.selected(true);
});
return true;
}
public clearAll(): boolean {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((column: ISelectColumn) => {
if (this.isEntityEditable(column.columnName())) {
column.selected(false);
} else {
column.selected(true);
}
});
return true;
}
public handleClick = (data: ISelectColumn, event: KeyboardEvent): boolean => {
this.selectTargetItem($(event.currentTarget), data);
return true;
};
private selectTargetItem($target: JQuery, targetColumn: ISelectColumn): void {
this.selectedColumnOption = targetColumn;
$(".list-item.selected").removeClass("selected");
$target.addClass("selected");
}
private isEntityEditable(name: string) {
if (this.queryViewModel.queryTablesTab.container.isPreferredApiCassandra()) {
const cassandraKeys = this.queryViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(this.queryViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map(key => key.property);
return !_.contains<string>(cassandraKeys, name);
}
return !(
name === Constants.EntityKeyNames.PartitionKey ||
name === Constants.EntityKeyNames.RowKey ||
name === Constants.EntityKeyNames.Timestamp
);
}
}

View File

@@ -0,0 +1,190 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" style="width:700px;" id="addtableentitypane">
<!-- Add table entity form - Start -->
<div
class="contextual-pane-in"
data-bind="
visible: !isEditing()"
>
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Add table entity header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keydown: onCloseKeyPress }"
>
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Add table entity header - End -->
<div class="tableParamContent paneContentContainer">
<div class="entity-table">
<div class="entity-table-row">
<div class="entity-table-cell entity-table-property-header" data-bind="text: attributeNameLabel"></div>
<div class="entity-table-cell entity-table-type-header" data-bind="text: dataTypeLabel"></div>
<div class="entity-table-cell entity-table-value-header" data-bind="text: attributeValueLabel"></div>
</div>
</div>
<div class="entity-table-scroll-box" id="addEntityScroll">
<div class="entity-table" data-bind="foreach: displayedAttributes">
<div class="entity-table-row">
<div class="entity-table-cell entity-table-property-column">
<input
type="text"
class="entity-table-field entity-table-property-column"
required
data-bind="
textInput: name,
attr: { title: nameTooltip, placeholder: namePlaceholder, 'aria-label': 'property name' },
css: { 'invalid-field': isInvalidName },
readOnly: !editable,
hasFocus: hasFocus"
/>
</div>
<div class="entity-table-cell entity-table-type-column">
<select
class="entity-table-field"
data-bind="
options: $parent.edmTypes,
optionsAfterRender: $parent.setOptionDisable,
value: type,
attr: { 'aria-label': 'type' },
enable: editable,
readOnly: !editable"
>
</select>
</div>
<!-- ko ifnot: isDateType -->
<div class="entity-table-cell entity-table-value-column">
<input
class="entity-table-field"
id="addTableEntityValue"
step="1"
data-bind="
textInput: value,
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType, 'aria-label': 'value' },
css: { 'invalid-field': isInvalidValue },
readOnly: !valueEditable"
/>
</div>
<!-- /ko -->
<!-- ko if: isDateType -->
<div class="entity-table-cell entity-table-value-column">
<input
class="entity-table-field"
step="1"
data-bind="
value: value,
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType },
css: { 'invalid-field': isInvalidValue },
readOnly: !valueEditable,
hasFocus: valueHasFocus"
autocomplete="off"
/>
</div>
<!-- /ko -->
<div class="entity-table-cell entity-table-action-column" data-bind="if: removable || valueEditable">
<span
class="entity-Edit-Cancel"
title="Edit property"
role="button"
aria-label="Edit property"
tabindex="0"
data-bind="click: $parent.editAttribute.bind($data, $index()), visible: valueEditable, event: { keydown: $parent.onEditPropertyKeyDown.bind($data, $index()) }"
>
<img class="entity-Editor-Cancel-Img" src="/Edit_entity.svg" alt="Edit" />
</span>
<span
class="entity-Edit-Cancel"
title="Delete property"
role="button"
aria-label="Delete property"
tabindex="0"
data-bind="click: $parent.removeAttribute.bind($data, $index()), visible: removable, event: { keydown: $parent.onDeletePropertyKeyDown.bind($data, $index()) }"
>
<img class="entity-Editor-Cancel-Img" src="/delete.svg" alt="Cancel" />
</span>
</div>
</div>
</div>
<div class="entity-table addProperty">
<div class="entity-table-row">
<div class="entity-table-cell">
<span
class="commandButton"
id="addProperty"
role="button"
aria-label="Add property"
tabindex="0"
data-bind="visible: canAdd, click: insertAttribute, event: { keydown: onAddPropertyKeyDown }"
autofocus
>
<img class="addPropertyImg" src="/Add-property.svg" alt="Insert attribute" />
<span data-bind="text: addButtonLabel"> </span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
type="submit"
class="btncreatecoll1"
data-bind="value: submitButtonText, event: { keydown: onSubmitKeyPress }"
/>
</div>
</div>
</form>
</div>
<!-- Add table entity form - End -->
<!-- Add table entity editor - Start -->
<div id="editor-panel-addEntity" data-bind="visible: isEditing()" style="display: none">
<div data-bind="with: editingProperty()">
<!-- Add table entity editor header - Start -->
<div class="firstdivbg headerline">
<span
class="backBtn"
aria-label="Back"
role="button"
tabindex="0"
data-bind="
click: $parent.finishEditingAttribute, event: { keydown: $parent.onBackButtonKeyDown }"
>
<img src="/RevertBack.svg" alt="BackIcon" />
</span>
<span class="edit-value-text" data-bind="text: name"></span>
</div>
<!-- Add table entity editor header - End -->
<div class="seconddivbg paddingspan2">
<textarea
class="entity-editor-expanded"
id="textAreaEditProperty"
tabindex="0"
rows="21"
data-bind="value: value, attr: { 'aria-label': name }"
style="width:95%"
autofocus
></textarea>
</div>
</div>
</div>
<!-- Add table entity editor - End -->
</div>
</div>

View File

@@ -0,0 +1,78 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="tablecolumnoptionspane">
<!-- Table Column Options form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Table Column Options header - Start -->
<div class="firstdivbg headerline">
Column Options
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel"
>
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Table Column Options header - End -->
<div class="paneMainContent paneContentContainer">
<div><span>Choose the columns and the order in which you want to display them in the table.</span></div>
<div class="column-options">
<div class="columns-border">
<input class="all-select-check" type="checkbox" data-bind="checked: allSelected" />
<label
style="font-weight:700;"
id="availableColumnsLabel"
data-bind="text: availableColumnsLabel"
></label>
<span class="column-arrows-svg" data-bind="click: moveDown, enable: canMoveDown">
<img class="column-opt-arrow-Img" src="/Down.svg" alt="Move down" />
</span>
<span class="column-arrows-svg" data-bind="click: moveUp, enable: canMoveUp">
<img class="column-opt-arrow-Img" src="/Up.svg" alt="Move up" />
</span>
</div>
<div class="content">
<section>
<ul data-bind="foreach: columnOptions" aria-labelledby="availableColumnsLabel" tabindex="0">
<li
class="list-item columns-border"
data-bind="attr: { title: columnName }, click: $parent.handleClick "
>
<input
type="checkbox"
for="columnName"
data-bind="attr: { title: columnName, 'aria-selected': (selected()? 'true': 'false') }, checked: selected"
/>
<label id="columnName" data-bind="text: columnName"></label>
</li>
</ul>
</section>
</div>
</div>
<div class="row-label" data-bind="style: { visibility: anyColumnSelected() ? 'hidden': 'visible' }">
<label class="warning" role="alert" aria-atomic="true" data-bind="text: noColumnSelectedWarning"></label>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
</form>
</div>
<!-- Table Column Options form - End -->
</div>
</div>

View File

@@ -0,0 +1,195 @@
import * as ko from "knockout";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataTableOperations from "../../Tables/DataTable/DataTableOperations";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import { ContextualPaneBase } from "../ContextualPaneBase";
import _ from "underscore";
/**
* Represents an item shown in the available columns.
* columnName: the name of the column.
* selected: indicate whether user wants to display the column in the table.
* order: the order in the initial table. E.g.,
* Order array of initial table: I = [0, 1, 2, 3, 4, 5, 6, 7, 8] <----> {prop0, prop1, prop2, prop3, prop4, prop5, prop6, prop7, prop8}
* Order array of current table: C = [0, 1, 2, 6, 7, 3, 4, 5, 8] <----> {prop0, prop1, prop2, prop6, prop7, prop3, prop4, prop5, prop8}
* if order = 6, then this column will be the one with column name prop6
* index: index in the observable array, this used for selection and moving up/down.
*/
interface IColumnOption {
columnName: ko.Observable<string>;
selected: ko.Observable<boolean>;
order: number;
index: number;
}
export interface IColumnSetting {
columnNames: string[];
visible?: boolean[];
order?: number[];
}
export class TableColumnOptionsPane extends ContextualPaneBase implements ViewModels.TableColumnOptionsPane {
public titleLabel: string = "Column Options";
public instructionLabel: string = "Choose the columns and the order in which you want to display them in the table.";
public availableColumnsLabel: string = "Available Columns";
public moveUpLabel: string = "Move Up";
public moveDownLabel: string = "Move Down";
public noColumnSelectedWarning: string = "At least one column should be selected.";
public columnOptions: ko.ObservableArray<IColumnOption>;
public allSelected: ko.Computed<boolean>;
public anyColumnSelected: ko.Computed<boolean>;
public canSelectAll: ko.Computed<boolean>;
public canMoveUp: ko.Observable<boolean>;
public canMoveDown: ko.Observable<boolean>;
public tableViewModel: TableEntityListViewModel;
public parameters: IColumnSetting;
private selectedColumnOption: IColumnOption = null;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.columnOptions = ko.observableArray<IColumnOption>();
this.anyColumnSelected = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: IColumnOption) => {
return value.selected();
});
});
this.canSelectAll = ko.computed<boolean>(() => {
return _.some(this.columnOptions(), (value: IColumnOption) => {
return !value.selected();
});
});
this.canMoveUp = ko.observable<boolean>(false);
this.canMoveDown = ko.observable<boolean>(false);
this.allSelected = ko.pureComputed<boolean>({
read: () => {
return !this.canSelectAll();
},
write: value => {
if (value) {
this.selectAll();
} else {
this.clearAll();
}
},
owner: this
});
}
public submit() {
var newColumnSetting = this.getParameters();
DataTableOperations.reorderColumns(this.tableViewModel.table, newColumnSetting.order).then(() => {
DataTableOperations.filterColumns(this.tableViewModel.table, newColumnSetting.visible);
this.visible(false);
});
}
public open() {
this.setDisplayedColumns(this.parameters.columnNames, this.parameters.order, this.parameters.visible);
super.open();
}
private getParameters(): IColumnSetting {
var newColumnSettings: IColumnSetting = <IColumnSetting>{
columnNames: [],
order: [],
visible: []
};
this.columnOptions().map((value: IColumnOption) => {
newColumnSettings.columnNames.push(value.columnName());
newColumnSettings.order.push(value.order);
newColumnSettings.visible.push(value.selected());
});
return newColumnSettings;
}
public setDisplayedColumns(columnNames: string[], order: number[], visible: boolean[]): void {
var options: IColumnOption[] = order.map((value: number, index: number) => {
var columnOption: IColumnOption = {
columnName: ko.observable<string>(columnNames[index]),
order: value,
selected: ko.observable<boolean>(visible[index]),
index: index
};
return columnOption;
});
this.columnOptions(options);
}
public selectAll(): void {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: IColumnOption) => {
value.selected(true);
});
}
public clearAll(): void {
const columnOptions = this.columnOptions && this.columnOptions();
columnOptions &&
columnOptions.forEach((value: IColumnOption) => {
value.selected(false);
});
if (columnOptions && columnOptions.length > 0) {
columnOptions[0].selected(true);
}
}
public moveUp(): void {
if (this.selectedColumnOption) {
var currentSelectedIndex: number = this.selectedColumnOption.index;
var swapTargetIndex: number = currentSelectedIndex - 1;
//Debug.assert(currentSelectedIndex > 0);
this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex);
this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]);
}
}
public moveDown(): void {
if (this.selectedColumnOption) {
var currentSelectedIndex: number = this.selectedColumnOption.index;
var swapTargetIndex: number = currentSelectedIndex + 1;
//Debug.assert(currentSelectedIndex < (this.columnOptions().length - 1));
this.swapColumnOption(this.columnOptions(), swapTargetIndex, currentSelectedIndex);
this.selectTargetItem($(`div.column-options li:eq(${swapTargetIndex})`), this.columnOptions()[swapTargetIndex]);
}
}
public handleClick = (data: IColumnOption, event: KeyboardEvent): boolean => {
this.selectTargetItem($(event.currentTarget), data);
return true;
};
private selectTargetItem($target: JQuery, targetColumn: IColumnOption): void {
this.selectedColumnOption = targetColumn;
this.canMoveUp(targetColumn.index !== 0);
this.canMoveDown(targetColumn.index !== this.columnOptions().length - 1);
$(".list-item.selected").removeClass("selected");
$target.addClass("selected");
}
private swapColumnOption(options: IColumnOption[], indexA: number, indexB: number): void {
var tempColumnName: string = options[indexA].columnName();
var tempSelected: boolean = options[indexA].selected();
var tempOrder: number = options[indexA].order;
options[indexA].columnName(options[indexB].columnName());
options[indexB].columnName(tempColumnName);
options[indexA].selected(options[indexB].selected());
options[indexB].selected(tempSelected);
options[indexA].order = options[indexB].order;
options[indexB].order = tempOrder;
}
}

View File

@@ -0,0 +1,188 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" style="width:700px;" id="edittableentitypane">
<!-- Edit table entity form - Start -->
<div
class="contextual-pane-in"
data-bind="
visible: !isEditing()"
>
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Edit table entity header - Start -->
<div class="firstdivbg headerline">
<span data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keydown: onCloseKeyPress }"
>
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Edit table entity header - End -->
<div class="tableParamContent paneContentContainer">
<div class="entity-table">
<div class="entity-table-row">
<div class="entity-table-cell entity-table-property-header" data-bind="text: attributeNameLabel"></div>
<div class="entity-table-cell entity-table-type-header" data-bind="text: dataTypeLabel"></div>
<div class="entity-table-cell entity-table-value-header" data-bind="text: attributeValueLabel"></div>
</div>
</div>
<div class="entity-table-scroll-box" id="editEntityScroll">
<div class="entity-table" data-bind="foreach: displayedAttributes">
<div class="entity-table-row">
<div class="entity-table-cell entity-table-property-column">
<input
type="text"
class="entity-table-field entity-table-property-column"
required
data-bind="
textInput: name,
attr: { title: nameTooltip, placeholder: namePlaceholder, 'aria-label': 'property name' },
css: { 'invalid-field': isInvalidName },
readOnly: !editable,
hasFocus: hasFocus"
/>
</div>
<div class="entity-table-cell entity-table-type-column">
<select
class="entity-table-field"
data-bind="
options: $parent.edmTypes,
optionsAfterRender: $parent.setOptionDisable,
value: type,
attr: { 'aria-label': 'type' },
enable: editable,
readOnly: !editable"
>
</select>
</div>
<!-- ko ifnot: isDateType -->
<div class="entity-table-cell entity-table-value-column">
<input
class="entity-table-field"
step="1"
data-bind="
textInput: value,
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType, 'aria-label': 'value' },
css: { 'invalid-field': isInvalidValue },
readOnly: !valueEditable"
/>
</div>
<!-- /ko -->
<!-- ko if: isDateType -->
<div class="entity-table-cell entity-table-value-column">
<input
class="entity-table-field"
step="1"
data-bind="
value: value,
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType, 'aria-label': 'value' },
css: { 'invalid-field': isInvalidValue },
readOnly: !valueEditable,
hasFocus: valueHasFocus"
autocomplete="off"
/>
</div>
<!-- /ko -->
<div class="entity-table-cell entity-table-action-column" data-bind="if: removable || valueEditable">
<span
class="entity-Edit-Cancel"
role="button"
aria-label="Edit property"
tabindex="0"
data-bind="click: $parent.editAttribute.bind($data, $index()), visible: valueEditable, event: { keydown: $parent.onEditPropertyKeyDown.bind($data, $index()) }"
title="Edit property"
>
<img class="entity-Editor-Cancel-Img" src="/Edit_entity.svg" alt="Edit attribute" />
</span>
<span
class="entity-Edit-Cancel"
role="button"
aria-label="Delete property"
tabindex="0"
data-bind="click: $parent.removeAttribute.bind($data, $index()), visible: removable, event: { keydown: $parent.onDeletePropertyKeyDown.bind($data, $index()) }"
title="Delete property"
>
<img class="entity-Editor-Cancel-Img" src="/delete.svg" alt="Remove attribute" />
</span>
</div>
</div>
</div>
<div class="entity-table addProperty">
<div class="entity-table-row">
<div class="entity-table-cell">
<span
class="commandButton"
role="button"
aria-label="Add property"
tabindex="0"
data-bind="visible: canAdd, click: insertAttribute, event: { keydown: onAddPropertyKeyDown }"
autofocus
>
<img class="addPropertyImg" src="/Add-property.svg" alt="Add attribute" />
<span data-bind="text: addButtonLabel"> </span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input
type="submit"
value="Update Entity"
class="btncreatecoll1"
data-bind="value: submitButtonText, event: { keydown: onSubmitKeyPress }"
/>
</div>
</div>
</form>
</div>
<!-- Edit table entity form - End -->
<!-- Edit table entity editor - Start -->
<div id="editor-panel-editEntity" data-bind="visible: isEditing()" style="display: none">
<div data-bind="with: editingProperty()">
<!-- Edit table entity editor header - Start -->
<div class="firstdivbg headerline">
<span
class="backBtn"
aria-label="Back"
role="button"
tabindex="0"
data-bind="
click: $parent.finishEditingAttribute, event: { keydown: $parent.onBackButtonKeyDown }"
>
<img src="/RevertBack.svg" alt="BackIcon" />
</span>
<span class="edit-value-text" data-bind="text: name"></span>
</div>
<!-- Edit table entity editor header - End -->
<div class="seconddivbg paddingspan2">
<textarea
class="entity-editor-expanded"
id="editor-area"
tabindex="0"
rows="21"
data-bind="value: value, attr: { 'aria-label': name }"
autofocus
></textarea>
</div>
</div>
</div>
<!-- Edit table entity editor - End -->
</div>
</div>

View File

@@ -0,0 +1,281 @@
import * as ko from "knockout";
import _ from "underscore";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import * as Entities from "../../Tables/Entities";
import EntityPropertyViewModel from "./EntityPropertyViewModel";
import * as TableConstants from "../../Tables/Constants";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import * as Utilities from "../../Tables/Utilities";
import * as ViewModels from "../../../Contracts/ViewModels";
import { KeyCodes } from "../../../Common/Constants";
import { ContextualPaneBase } from "../ContextualPaneBase";
// Class with variables and functions that are common to both adding and editing entities
export default abstract class TableEntityPane extends ContextualPaneBase {
protected static requiredFieldsForTablesAPI: string[] = [
TableConstants.EntityKeyNames.PartitionKey,
TableConstants.EntityKeyNames.RowKey
];
/* Labels */
public attributeNameLabel = "Property Name"; // localize
public dataTypeLabel = "Type"; // localize
public attributeValueLabel = "Value"; // localize
/* Controls */
public removeButtonLabel = "Remove"; // localize
public editButtonLabel = "Edit"; // localize
public addButtonLabel = "Add Property"; // localize
public edmTypes: ko.ObservableArray<string> = ko.observableArray([
TableConstants.TableType.String,
TableConstants.TableType.Boolean,
TableConstants.TableType.Binary,
TableConstants.TableType.DateTime,
TableConstants.TableType.Double,
TableConstants.TableType.Guid,
TableConstants.TableType.Int32,
TableConstants.TableType.Int64
]);
public canAdd: ko.Computed<boolean>;
public canApply: ko.Observable<boolean>;
public displayedAttributes = ko.observableArray<EntityPropertyViewModel>();
public editingProperty = ko.observable<EntityPropertyViewModel>();
public isEditing = ko.observable<boolean>(false);
public submitButtonText = ko.observable<string>();
public tableViewModel: TableEntityListViewModel;
protected scrollId: ko.Observable<string>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.container.isPreferredApiCassandra.subscribe(isCassandra => {
if (isCassandra) {
this.edmTypes([
TableConstants.CassandraType.Text,
TableConstants.CassandraType.Ascii,
TableConstants.CassandraType.Bigint,
TableConstants.CassandraType.Blob,
TableConstants.CassandraType.Boolean,
TableConstants.CassandraType.Decimal,
TableConstants.CassandraType.Double,
TableConstants.CassandraType.Float,
TableConstants.CassandraType.Int,
TableConstants.CassandraType.Uuid,
TableConstants.CassandraType.Varchar,
TableConstants.CassandraType.Varint,
TableConstants.CassandraType.Inet,
TableConstants.CassandraType.Smallint,
TableConstants.CassandraType.Tinyint
]);
}
});
this.canAdd = ko.computed<boolean>(() => {
// Cassandra can't add since the schema can't be changed once created
if (this.container.isPreferredApiCassandra()) {
return false;
}
// Adding '2' to the maximum to take into account PartitionKey and RowKey
return this.displayedAttributes().length < EntityPropertyViewModel.maximumNumberOfProperties + 2;
});
this.canApply = ko.observable<boolean>(true);
this.editingProperty(this.displayedAttributes()[0]);
}
public removeAttribute = (index: number, data: any): void => {
this.displayedAttributes.splice(index, 1);
this.updateIsActionEnabled();
document.getElementById("addProperty").focus();
};
public editAttribute = (index: number, data: EntityPropertyViewModel): void => {
this.editingProperty(data);
this.isEditing(true);
document.getElementById("textAreaEditProperty").focus();
};
public finishEditingAttribute = (): void => {
this.isEditing(false);
this.editingProperty(null);
};
public onKeyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = Utilities.onEsc(event, ($sourceElement: JQuery) => {
this.finishEditingAttribute();
});
return !handled;
};
public onAddPropertyKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.insertAttribute();
event.stopPropagation();
return false;
}
return true;
};
public onEditPropertyKeyDown = (
index: number,
data: EntityPropertyViewModel,
event: KeyboardEvent,
source: any
): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.editAttribute(index, data);
event.stopPropagation();
return false;
}
return true;
};
public onDeletePropertyKeyDown = (
index: number,
data: EntityPropertyViewModel,
event: KeyboardEvent,
source: any
): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.removeAttribute(index, data);
event.stopPropagation();
return false;
}
return true;
};
public onBackButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.finishEditingAttribute();
event.stopPropagation();
return false;
}
return true;
};
public insertAttribute = (name?: string, type?: string): void => {
let entityProperty: EntityPropertyViewModel;
if (!!name && !!type && this.container.isPreferredApiCassandra()) {
// TODO figure out validation story for blob and Inet so we can allow adding/editing them
const nonEditableType: boolean =
type === TableConstants.CassandraType.Blob || type === TableConstants.CassandraType.Inet;
entityProperty = new EntityPropertyViewModel(
this,
name,
type,
"", // default to empty string
/* namePlaceholder */ undefined,
/* valuePlaceholder */ undefined,
/* editable */ false,
/* default valid name */ false,
/* default valid value */ true,
/* isRequired */ false,
/* removable */ false,
/*value editable*/ !nonEditableType
);
} else {
entityProperty = new EntityPropertyViewModel(
this,
"",
this.edmTypes()[0], // default to the first Edm type: 'string'
"", // default to empty string
/* namePlaceholder */ undefined,
/* valuePlaceholder */ undefined,
/* editable */ true,
/* default valid name */ false,
/* default valid value */ true
);
}
this.displayedAttributes.push(entityProperty);
this.updateIsActionEnabled();
this.scrollToBottom();
entityProperty.hasFocus(true);
};
public updateIsActionEnabled(needRequiredFields: boolean = true): void {
var properties: EntityPropertyViewModel[] = this.displayedAttributes() || [];
var disable: boolean = _.some(properties, (property: EntityPropertyViewModel) => {
return property.isInvalidName() || property.isInvalidValue();
});
this.canApply(!disable);
}
protected entityFromAttributes(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity {
var entity: any = {};
displayedAttributes &&
displayedAttributes.forEach((attribute: EntityPropertyViewModel) => {
if (attribute.name() && (attribute.value() !== "" || attribute.isRequired)) {
var value = attribute.getPropertyValue();
var type = attribute.type();
if (type === TableConstants.TableType.Int64) {
value = Utilities.padLongWithZeros(value);
}
entity[attribute.name()] = {
_: value,
$: type
};
}
});
return entity;
}
// Removing Binary from Add Entity dialog until we have a full story for it.
protected setOptionDisable(option: Node, value: string): void {
ko.applyBindingsToNode(option, { disable: value === TableConstants.TableType.Binary }, value);
}
/**
* Parse the updated entity to see if there are any new attributes that old headers don't have.
* In this case, add these attributes names as new headers.
* Remarks: adding new headers will automatically trigger table redraw.
*/
protected tryInsertNewHeaders(viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean {
var newHeaders: string[] = [];
const keys = Object.keys(newEntity);
keys &&
keys.forEach((key: string) => {
if (
!_.contains(viewModel.headers, key) &&
key !== TableEntityProcessor.keyProperties.attachments &&
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(!viewModel.queryTablesTab.container.isPreferredApiCassandra() ||
key !== TableConstants.EntityKeyNames.RowKey)
) {
newHeaders.push(key);
}
});
var newHeadersInserted: boolean = false;
if (newHeaders.length) {
if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) {
newHeaders = viewModel.headers.concat(newHeaders);
}
viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false);
newHeadersInserted = true;
}
return newHeadersInserted;
}
protected scrollToBottom(): void {
var scrollBox = document.getElementById(this.scrollId());
var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1;
if (isScrolledToBottom) {
scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight;
}
}
}

View File

@@ -0,0 +1,79 @@
<div data-bind="visible: visible">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="queryselectpane">
<!-- Query Select form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
data-bind="
submit: submit"
>
<!-- Query Select header - Start -->
<div class="firstdivbg headerline">
Select Column
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keydown: onCloseKeyPress }"
>
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Query Select header - End -->
<div class="paneMainContent paneContentContainer">
<!--<div class="row">
<label id="instructionLabel" data-bind="text: instructionLabel"></label>
</div>-->
<div><span>Select the columns that you want to query.</span></div>
<div class="column-options">
<div class="columns-border">
<input class="all-select-check" type="checkbox" data-bind="checked: allSelected" />
<label
style="font-weight:700;"
id="availableColumnsTableQueryLabel"
data-bind="text: availableColumnsTableQueryLabel"
></label>
</div>
<div class="content">
<section>
<ul data-bind="foreach: columnOptions" aria-labelledby="availableColumnsTableQueryLabel" tabindex="0">
<!-- ko template: {if: editable} -->
<li
class="list-item columns-border"
data-bind="attr: { title: columnName }, click: $parent.handleClick "
>
<input type="checkbox" data-bind="attr: { title: columnName }, checked: selected" />
<span data-bind="text: columnName"></span>
</li>
<!--/ko-->
<!-- ko template: {ifnot: editable} -->
<li class="list-item columns-border" data-bind="attr: { title: columnName } ">
<input type="checkbox" disabled data-bind="checked: selected" />
<span data-bind="text: columnName"></span>
</li>
<!--/ko-->
</ul>
</section>
</div>
</div>
<div class="row-label" data-bind="style: { visibility: anyColumnSelected() ? 'hidden': 'visible' }">
<label class="warning" role="alert" aria-atomic="true" data-bind="text: noColumnSelectedWarning"></label>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
</form>
</div>
<!-- Query Select form - End -->
</div>
</div>

View File

@@ -0,0 +1,306 @@
/* Constants */
var MaximumNameLength = 255;
var noHelp = "";
var detailedHelp = "Enter a name up to 255 characters in size. Most valid C# identifiers are allowed."; // localize
export interface IValidationResult {
isInvalid: boolean;
help: string;
}
export function validate(name: string): IValidationResult {
var help: string = noHelp;
// Note: Disabling encoding check to err the side of lax validation.
// A valid property name should also be XML-serializable.
// Hence, only allowing names that don't require special encoding for network transmission.
// var encoded: string = tryEncode(name);
// var success: boolean = (name === encoded);
var success: boolean = true;
if (success) {
success = name.length <= MaximumNameLength;
if (success) {
success = IsValidIdentifier(name);
}
}
if (!success) {
help = detailedHelp;
}
return { isInvalid: !success, help: help };
}
/*
function tryEncode(name: string): string {
var encoded: string = null;
try {
encoded = encodeURIComponent(name);
} catch (error) {
console.error("tryEncode", "Error encoding:", name, error);
encoded = null;
}
return encoded;
}
*/
// Port of http://referencesource.microsoft.com/#System/compmod/microsoft/csharp/csharpcodeprovider.cs,7b5c20ff8d28dfa7
function IsValidIdentifier(value: string): boolean {
// identifiers must be 1 char or longer
if (!value) {
return false;
}
if (value.length > 512) {
return false;
}
// Note: Disabling keyword check to err the side of lax validation.
// identifiers cannot be a keyword, unless they are escaped with an '@'
/*
if (value[0] !== "@") {
if (IsKeyword(value)) {
return false;
}
} else {
value = value.substring(1);
}
*/
return IsValidLanguageIndependentIdentifier(value);
}
/*
var keywords: string[][] = [
// 2 characters
[
"as",
"do",
"if",
"in",
"is",
],
// 3 characters
[
"for",
"int",
"new",
"out",
"ref",
"try",
],
// 4 characters
[
"base",
"bool",
"byte",
"case",
"char",
"else",
"enum",
"goto",
"lock",
"long",
"null",
"this",
"true",
"uint",
"void",
],
// 5 characters
[
"break",
"catch",
"class",
"const",
"event",
"false",
"fixed",
"float",
"sbyte",
"short",
"throw",
"ulong",
"using",
"while",
],
// 6 characters
[
"double",
"extern",
"object",
"params",
"public",
"return",
"sealed",
"sizeof",
"static",
"string",
"struct",
"switch",
"typeof",
"unsafe",
"ushort",
],
// 7 characters
[
"checked",
"decimal",
"default",
"finally",
"foreach",
"private",
"virtual",
],
// 8 characters
[
"abstract",
"continue",
"delegate",
"explicit",
"implicit",
"internal",
"operator",
"override",
"readonly",
"volatile",
],
// 9 characters
[
"__arglist",
"__makeref",
"__reftype",
"interface",
"namespace",
"protected",
"unchecked",
],
// 10 characters
[
"__refvalue",
"stackalloc",
]
];
function IsKeyword(value: string): boolean {
var isKeyword: boolean = false;
var listCount: number = keywords.length;
for (var i = 0; ((i < listCount) && !isKeyword); ++i) {
var list: string[] = keywords[i];
var listKeywordCount: number = list.length;
for (var j = 0; ((j < listKeywordCount) && !isKeyword); ++j) {
var keyword: string = list[j];
isKeyword = (value === keyword);
}
}
return isKeyword;
}
*/
function IsValidLanguageIndependentIdentifier(value: string): boolean {
return IsValidTypeNameOrIdentifier(value, /* isTypeName */ false);
}
var UnicodeCategory = {
// Uppercase Letter
Lu: /[A-ZÀ-ÖØ-ÞĀĂĄĆĈĊČĎĐĒĔĖĘĚĜĞĠĢĤĦĨĪĬĮİIJĴĶĹĻĽĿŁŃŅŇŊŌŎŐŒŔŖŘŚŜŞŠŢŤŦŨŪŬŮŰŲŴŶŸ-ŹŻŽƁ-ƂƄƆ-ƇƉ-ƋƎ-ƑƓ-ƔƖ-ƘƜ-ƝƟ-ƠƢƤƦ-ƧƩƬƮ-ƯƱ-ƳƵƷ-ƸƼDŽLJNJǍǏǑǓǕǗǙǛǞǠǢǤǦǨǪǬǮDZǴǶ-ǸǺǼǾȀȂȄȆȈȊȌȎȐȒȔȖȘȚȜȞȠȢȤȦȨȪȬȮȰȲȺ-ȻȽ-ȾɁɃ-ɆɈɊɌɎͰͲͶΆΈ-ΊΌΎ-ΏΑ-ΡΣ-ΫϏϒ-ϔϘϚϜϞϠϢϤϦϨϪϬϮϴϷϹ-ϺϽ-ЯѠѢѤѦѨѪѬѮѰѲѴѶѸѺѼѾҀҊҌҎҐҒҔҖҘҚҜҞҠҢҤҦҨҪҬҮҰҲҴҶҸҺҼҾӀ-ӁӃӅӇӉӋӍӐӒӔӖӘӚӜӞӠӢӤӦӨӪӬӮӰӲӴӶӸӺӼӾԀԂԄԆԈԊԌԎԐԒԔԖԘԚԜԞԠԢԱ-ՖႠ-ჅḀḂḄḆḈḊḌḎḐḒḔḖḘḚḜḞḠḢḤḦḨḪḬḮḰḲḴḶḸḺḼḾṀṂṄṆṈṊṌṎṐṒṔṖṘṚṜṞṠṢṤṦṨṪṬṮṰṲṴṶṸṺṼṾẀẂẄẆẈẊẌẎẐẒẔẞẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼẾỀỂỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪỬỮỰỲỴỶỸỺỼỾἈ-ἏἘ-ἝἨ-ἯἸ-ἿὈ-ὍὙὛὝὟὨ-ὯᾸ-ΆῈ-ΉῘ-ΊῨ-ῬῸ-Ώℂℇℋ---ℝℤΩℨK--ℳℾ-ℿⅅↃⰀ-ⰮⱠⱢ-ⱤⱧⱩⱫⱭ-ⱯⱲⱵⲀⲂⲄⲆⲈⲊⲌⲎⲐⲒⲔⲖⲘⲚⲜⲞⲠⲢⲤⲦⲨⲪⲬⲮⲰⲲⲴⲶⲸⲺⲼⲾⳀⳂⳄⳆⳈⳊⳌⳎⳐⳒⳔⳖⳘⳚⳜⳞⳠⳢꙀꙂꙄꙆꙈꙊꙌꙎꙐꙒꙔꙖꙘꙚꙜꙞꙢꙤꙦꙨꙪꙬꚀꚂꚄꚆꚈꚊꚌꚎꚐꚒꚔꚖꜢꜤꜦꜨꜪꜬꜮꜲꜴꜶꜸꜺꜼꜾꝀꝂꝄꝆꝈꝊꝌꝎꝐꝒꝔꝖꝘꝚꝜꝞꝠꝢꝤꝦꝨꝪꝬꝮꝹꝻꝽ-ꝾꞀꞂꞄꞆꞋA-]|\ud801[\udc00-\udc27]|\ud835[\udc00-\udc19\udc34-\udc4d\udc68-\udc81\udc9c\udc9e-\udc9f\udca2\udca5-\udca6\udca9-\udcac\udcae-\udcb5\udcd0-\udce9\udd04-\udd05\udd07-\udd0a\udd0d-\udd14\udd16-\udd1c\udd38-\udd39\udd3b-\udd3e\udd40-\udd44\udd46\udd4a-\udd50\udd6c-\udd85\udda0-\uddb9\uddd4-\udded\ude08-\ude21\ude3c-\ude55\ude70-\ude89\udea8-\udec0\udee2-\udefa\udf1c-\udf34\udf56-\udf6e\udf90-\udfa8\udfca]/,
// Lowercase Letter
Ll: /[a-zªµºß-öø-ÿāăąćĉċčďđēĕėęěĝğġģĥħĩīĭįıijĵķ-ĸĺļľŀłńņň-ʼnŋōŏőœŕŗřśŝşšţťŧũūŭůűųŵŷźżž-ƀƃƅƈƌ-ƍƒƕƙ-ƛƞơƣƥƨƪ-ƫƭưƴƶƹ-ƺƽ-ƿdžljnjǎǐǒǔǖǘǚǜ-ǝǟǡǣǥǧǩǫǭǯ-ǰdzǵǹǻǽǿȁȃȅȇȉȋȍȏȑȓȕȗșțȝȟȡȣȥȧȩȫȭȯȱȳ-ȹȼȿ-ɀɂɇɉɋɍɏ-ʓʕ-ʯͱͳͷͻ-ͽΐά-ώϐ-ϑϕ-ϗϙϛϝϟϡϣϥϧϩϫϭϯ-ϳϵϸϻ-ϼа-џѡѣѥѧѩѫѭѯѱѳѵѷѹѻѽѿҁҋҍҏґғҕҗҙқҝҟҡңҥҧҩҫҭүұҳҵҷҹһҽҿӂӄӆӈӊӌӎ-ӏӑӓӕӗәӛӝӟӡӣӥӧөӫӭӯӱӳӵӷӹӻӽӿԁԃԅԇԉԋԍԏԑԓԕԗԙԛԝԟԡԣա-ևᴀ-ᴫᵢ-ᵷᵹ-ᶚḁḃḅḇḉḋḍḏḑḓḕḗḙḛḝḟḡḣḥḧḩḫḭḯḱḳḵḷḹḻḽḿṁṃṅṇṉṋṍṏṑṓṕṗṙṛṝṟṡṣṥṧṩṫṭṯṱṳṵṷṹṻṽṿẁẃẅẇẉẋẍẏẑẓẕ-ẝẟạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹỻỽỿ-ἇἐ-ἕἠ-ἧἰ-ἷὀ-ὅὐ-ὗὠ-ὧὰ-ώᾀ-ᾇᾐ-ᾗᾠ-ᾧᾰ-ᾴᾶ-ᾷιῂ-ῄῆ-ῇῐ-ΐῖ-ῗῠ-ῧῲ-ῴῶ-ῷⁱⁿℊℎ-ℏℓℯℴℹℼ--ⅉⅎↄⰰ-ⱞⱡⱥ-ⱦⱨⱪⱬⱱⱳ-ⱴⱶ-ⱼⲁⲃⲅⲇⲉⲋⲍⲏⲑⲓⲕⲗⲙⲛⲝⲟⲡⲣⲥⲧⲩⲫⲭⲯⲱⲳⲵⲷⲹⲻⲽⲿⳁⳃⳅⳇⳉⳋⳍⳏⳑⳓⳕⳗⳙⳛⳝⳟⳡⳣ-ⳤⴀ-ⴥꙁꙃꙅꙇꙉꙋꙍꙏꙑꙓꙕꙗꙙꙛꙝꙟꙣꙥꙧꙩꙫꙭꚁꚃꚅꚇꚉꚋꚍꚏꚑꚓꚕꚗꜣꜥꜧꜩꜫꜭꜯ-ꜱꜳꜵꜷꜹꜻꜽꜿꝁꝃꝅꝇꝉꝋꝍꝏꝑꝓꝕꝗꝙꝛꝝꝟꝡꝣꝥꝧꝩꝫꝭꝯꝱ-ꝸꝺꝼꝿꞁꞃꞅꞇꞌff-stﬓ-ﬗa-]|\ud801[\udc28-\udc4f]|\ud835[\udc1a-\udc33\udc4e-\udc54\udc56-\udc67\udc82-\udc9b\udcb6-\udcb9\udcbb\udcbd-\udcc3\udcc5-\udccf\udcea-\udd03\udd1e-\udd37\udd52-\udd6b\udd86-\udd9f\uddba-\uddd3\uddee-\ude07\ude22-\ude3b\ude56-\ude6f\ude8a-\udea5\udec2-\udeda\udedc-\udee1\udefc-\udf14\udf16-\udf1b\udf36-\udf4e\udf50-\udf55\udf70-\udf88\udf8a-\udf8f\udfaa-\udfc2\udfc4-\udfc9\udfcb]/,
// Titlecase Letter
Lt: /[DžLjNjDzᾈ-ᾏᾘ-ᾟᾨ-ᾯᾼῌῼ]/,
// Modifier/Number Letter
Lm: /[ʰ-ˁˆ-ˑˠ-ˤˬˮʹͺՙـۥ-ۦߴ-ߵߺॱๆໆჼៗᡃᱸ-ᱽᴬ-ᵡᵸᶛ-ᶿₐ-ₔⱽⵯⸯ々〱-〵〻ゝ-ゞー-ヾꀕꘌꙿꜗ-ꜟꝰꞈー゙-゚]/,
// Other Letter
Lo: /[ƻǀ-ǃʔא-תװ-ײء-ؿف-يٮ-ٯٱ-ۓەۮ-ۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪऄ-हऽॐक़-ॡॲॻ-ॿঅ-ঌএ-ঐও-নপ-রলশ-হঽৎড়-ঢ়য়-ৡৰ-ৱਅ-ਊਏ-ਐਓ-ਨਪ-ਰਲ-ਲ਼ਵ-ਸ਼ਸ-ਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલ-ળવ-હઽૐૠ-ૡଅ-ଌଏ-ଐଓ-ନପ-ରଲ-ଳଵ-ହଽଡ଼-ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கங-சஜஞ-டண-தந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-ళవ-హఽౘ-ౙౠ-ౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠ-ೡഅ-ഌഎ-ഐഒ-നപ-ഹഽൠ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะา-ำเ-ๅກ-ຂຄງ-ຈຊຍດ-ທນ-ຟມ-ຣລວສ-ຫອ-ະາ-ຳຽເ-ໄໜ-ໝༀཀ-ཇཉ-ཬྈ-ྋက-ဪဿၐ-ၕၚ-ၝၡၥ-ၦၮ-ၰၵ-ႁႎა-ჺᄀ-ᅙᅟ-ᆢᆨ-ᇹሀ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏼᐁ-ᙬᙯ-ᙶᚁ-ᚚᚠ-ᛪᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៜᠠ-ᡂᡄ-ᡷᢀ-ᢨᢪᤀ-ᤜᥐ-ᥭᥰ-ᥴᦀ-ᦩᧁ-ᧇᨀ-ᨖᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮ-ᮯᰀ-ᰣᱍ-ᱏᱚ-ᱷℵ-ℸⴰ-ⵥⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞ〆〼ぁ-ゖゟァ-ヺヿㄅ-ㄭㄱ-ㆎㆠ-ㆷㇰ-ㇿ㐀-䶵一-鿃ꀀ-ꀔꀖ-ꒌꔀ-ꘋꘐ-ꘟꘪ-ꘫꙮꟻ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꤊ-ꤥꤰ-ꥆꨀ-ꨨꩀ-ꩂꩄ-ꩋ가-힣豈-鶴侮-頻並-龎יִײַ-ﬨשׁ-זּטּ-לּמּנּ-סּףּ-פּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼヲ-ッア-ンᅠ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ]|[\ud840-\ud868][\udc00-\udfff]|\ud800[\udc00-\udc0b\udc0d-\udc26\udc28-\udc3a\udc3c-\udc3d\udc3f-\udc4d\udc50-\udc5d\udc80-\udcfa\ude80-\ude9c\udea0-\uded0\udf00-\udf1e\udf30-\udf40\udf42-\udf49\udf80-\udf9d\udfa0-\udfc3\udfc8-\udfcf]|\ud801[\udc50-\udc9d]|\ud802[\udc00-\udc05\udc08\udc0a-\udc35\udc37-\udc38\udc3c\udc3f\udd00-\udd15\udd20-\udd39\ude00\ude10-\ude13\ude15-\ude17\ude19-\ude33]|\ud808[\udc00-\udf6e]|\ud869[\udc00-\uded6]|\ud87e[\udc00-\ude1d]/,
// Non Spacing Mark
Mn: /[\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0901-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0954\u0962-\u0963\u0981\u09bc\u09c1-\u09c4\u09cd\u09e2-\u09e3\u0a01-\u0a02\u0a3c\u0a41-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51\u0a70-\u0a71\u0a75\u0a81-\u0a82\u0abc\u0ac1-\u0ac5\u0ac7-\u0ac8\u0acd\u0ae2-\u0ae3\u0b01\u0b3c\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b62-\u0b63\u0b82\u0bc0\u0bcd\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c62-\u0c63\u0cbc\u0cbf\u0cc6\u0ccc-\u0ccd\u0ce2-\u0ce3\u0d41-\u0d44\u0d4d\u0d62-\u0d63\u0dca\u0dd2-\u0dd4\u0dd6\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86-\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039-\u103a\u103d-\u103e\u1058-\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108d\u135f\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193b\u1a17-\u1a18\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80-\u1b81\u1ba2-\u1ba5\u1ba8-\u1ba9\u1c2c-\u1c33\u1c36-\u1c37\u1dc0-\u1de6\u1dfe-\u1dff\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2de0-\u2dff\u302a-\u302f\u3099-\u309a\ua66f\ua67c-\ua67d\ua802\ua806\ua80b\ua825-\ua826\ua8c4\ua926-\ua92d\ua947-\ua951\uaa29-\uaa2e\uaa31-\uaa32\uaa35-\uaa36\uaa43\uaa4c\ufb1e\ufe00-\ufe0f\ufe20-\ufe26]|\ud800\uddfd|\ud802[\ude01-\ude03\ude05-\ude06\ude0c-\ude0f\ude38-\ude3a\ude3f]|\ud834[\udd67-\udd69\udd7b-\udd82\udd85-\udd8b\uddaa-\uddad\ude42-\ude44]|\udb40[\udd00-\uddef]/,
// Spacing Combining Mark
Mc: /[\u0903\u093e-\u0940\u0949-\u094c\u0982-\u0983\u09be-\u09c0\u09c7-\u09c8\u09cb-\u09cc\u09d7\u0a03\u0a3e-\u0a40\u0a83\u0abe-\u0ac0\u0ac9\u0acb-\u0acc\u0b02-\u0b03\u0b3e\u0b40\u0b47-\u0b48\u0b4b-\u0b4c\u0b57\u0bbe-\u0bbf\u0bc1-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcc\u0bd7\u0c01-\u0c03\u0c41-\u0c44\u0c82-\u0c83\u0cbe\u0cc0-\u0cc4\u0cc7-\u0cc8\u0cca-\u0ccb\u0cd5-\u0cd6\u0d02-\u0d03\u0d3e-\u0d40\u0d46-\u0d48\u0d4a-\u0d4c\u0d57\u0d82-\u0d83\u0dcf-\u0dd1\u0dd8-\u0ddf\u0df2-\u0df3\u0f3e-\u0f3f\u0f7f\u102b-\u102c\u1031\u1038\u103b-\u103c\u1056-\u1057\u1062-\u1064\u1067-\u106d\u1083-\u1084\u1087-\u108c\u108f\u17b6\u17be-\u17c5\u17c7-\u17c8\u1923-\u1926\u1929-\u192b\u1930-\u1931\u1933-\u1938\u19b0-\u19c0\u19c8-\u19c9\u1a19-\u1a1b\u1b04\u1b35\u1b3b\u1b3d-\u1b41\u1b43-\u1b44\u1b82\u1ba1\u1ba6-\u1ba7\u1baa\u1c24-\u1c2b\u1c34-\u1c35\ua823-\ua824\ua827\ua880-\ua881\ua8b4-\ua8c3\ua952-\ua953\uaa2f-\uaa30\uaa33-\uaa34\uaa4d]|\ud834[\udd65-\udd66\udd6d-\udd72]/,
// Connector Punctuation
Pc: /[_‿-⁀⁔︳-︴﹍-_]/,
// Decimal Digit Number
Nd: /[0-9٠-٩۰-۹߀-߉०-९০-৯੦-੯૦-૯୦-୯௦-௯౦-౯೦-೯൦-൯๐-๙໐-໙༠-༩၀-၉႐-႙០-៩᠐-᠙᥆-᥏᧐-᧙᭐-᭙᮰-᮹᱀-᱉᱐-᱙꘠-꘩꣐-꣙꤀-꤉꩐-꩙0-]|\ud801[\udca0-\udca9]|\ud835[\udfce-\udfff]/
};
function IsValidTypeNameOrIdentifier(value: string, isTypeName: boolean): boolean {
var nextMustBeStartChar: boolean = true;
if (!value.length) {
return false;
}
// each char must be Lu, Ll, Lt, Lm, Lo, Nd, Mn, Mc, Pc
for (var i = 0; i < value.length; i++) {
var ch: string = value[i];
if (
UnicodeCategory.Lu.test(ch) ||
UnicodeCategory.Ll.test(ch) ||
UnicodeCategory.Lt.test(ch) ||
UnicodeCategory.Lm.test(ch) ||
UnicodeCategory.Lo.test(ch)
) {
nextMustBeStartChar = false;
} else if (
UnicodeCategory.Mn.test(ch) ||
UnicodeCategory.Mc.test(ch) ||
UnicodeCategory.Pc.test(ch) ||
UnicodeCategory.Nd.test(ch)
) {
// Underscore is a valid starting character, even though it is a ConnectorPunctuation.
if (nextMustBeStartChar && ch !== "_") {
return false;
}
nextMustBeStartChar = false;
} else {
// We only check the special Type chars for type names.
if (!isTypeName) {
return false;
} else {
var ref: { nextMustBeStartChar: boolean } = { nextMustBeStartChar: nextMustBeStartChar };
var isSpecialTypeChar = IsSpecialTypeChar(ch, ref);
nextMustBeStartChar = ref.nextMustBeStartChar;
if (!isSpecialTypeChar) {
return false;
}
}
}
}
return true;
}
// This can be a special character like a separator that shows up in a type name
// This is an odd set of characters. Some come from characters that are allowed by C++, like < and >.
// Others are characters that are specified in the type and assembly name grammer.
function IsSpecialTypeChar(ch: string, ref: { nextMustBeStartChar: boolean }): boolean {
switch (ch) {
case ":":
case ".":
case "$":
case "+":
case "<":
case ">":
case "-":
case "[":
case "]":
case ",":
case "&":
case "*":
ref.nextMustBeStartChar = true;
return true;
case "`":
return true;
}
return false;
}

View File

@@ -0,0 +1,23 @@
export var Int32 = {
Min: -2147483648,
Max: 2147483647
};
export var Int64 = {
Min: -9223372036854775808,
Max: 9223372036854775807
};
var yearMonthDay = "\\d{4}[- ][01]\\d[- ][0-3]\\d";
var timeOfDay = "T[0-2]\\d:[0-5]\\d(:[0-5]\\d(\\.\\d+)?)?";
var timeZone = "Z|[+-][0-2]\\d:[0-5]\\d";
export var ValidationRegExp = {
Guid: /^[{(]?[0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$/i,
Float: /^[+-]?\d+(\.\d+)?(e[+-]?\d+)?$/i,
// OData seems to require an "L" suffix for Int64 values, yet Azure Storage errors out with it. See http://www.odata.org/documentation/odata-version-2-0/overview/
Integer: /^[+-]?\d+$/i, // Used for both Int32 and Int64 values
Boolean: /^"?(true|false)"?$/i,
DateTime: new RegExp(`^${yearMonthDay}${timeOfDay}${timeZone}$`),
PrimaryKey: /^[^/\\#?\u0000-\u001F\u007F-\u009F]*$/
};

View File

@@ -0,0 +1,341 @@
import * as Utilities from "../../../Tables/Utilities";
import * as StorageExplorerConstants from "../../../Tables/Constants";
import * as EntityPropertyValidationCommon from "./EntityPropertyValidationCommon";
interface IValidationResult {
isInvalid: boolean;
help: string;
}
interface IValueValidator {
validate: (value: string) => IValidationResult;
parseValue: (value: string) => any;
}
/* Constants */
var noHelp: string = "";
var MaximumStringLength = 64 * 1024; // 64 KB
var MaximumRequiredStringLength = 1 * 1024; // 1 KB
class ValueValidator implements IValueValidator {
public validate(value: string): IValidationResult {
// throw new Errors.NotImplementedFunctionError("ValueValidator.validate");
return null;
}
public parseValue(value: string): any {
return value; // default pass-thru implementation
}
}
class KeyValidator implements ValueValidator {
private static detailedHelp = "Enter a string ('/', '\\', '#', '?' and control characters not allowed)."; // Localize
public validate(value: string): IValidationResult {
if (
value == null ||
value.trim().length === 0 ||
EntityPropertyValidationCommon.ValidationRegExp.PrimaryKey.test(value)
) {
return { isInvalid: false, help: noHelp };
} else {
return { isInvalid: true, help: KeyValidator.detailedHelp };
}
}
public parseValue(value: string): string {
return value;
}
}
class BooleanValueValidator extends ValueValidator {
private detailedHelp = "Enter true or false."; // localize
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
success = EntityPropertyValidationCommon.ValidationRegExp.Boolean.test(value);
}
if (!success) {
help = this.detailedHelp;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): boolean {
// OData seems to require lowercase boolean values, see http://www.odata.org/documentation/odata-version-2-0/overview/
return value.toString().toLowerCase() === "true";
}
}
class DateTimeValueValidator extends ValueValidator {
private detailedHelp = "Enter a date and time."; // localize
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
// Try to parse the value to see if it is a valid date string
var parsed: number = Date.parse(value);
success = !isNaN(parsed);
}
if (!success) {
help = this.detailedHelp;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): Date {
var millisecondTime = Date.parse(value);
var parsed: Date = new Date(millisecondTime);
return parsed;
}
}
class DoubleValueValidator extends ValueValidator {
private detailedHelp = "Enter a 64-bit floating point value."; // localize
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
success = EntityPropertyValidationCommon.ValidationRegExp.Float.test(value);
}
if (!success) {
help = this.detailedHelp;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): number {
return parseFloat(value);
}
}
class GuidValueValidator extends ValueValidator {
private detailedHelp = "Enter a 16-byte (128-bit) GUID value."; // localize
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
success = EntityPropertyValidationCommon.ValidationRegExp.Guid.test(value);
}
if (!success) {
help = this.detailedHelp;
}
return { isInvalid: !success, help: help };
}
}
class IntegerValueValidator extends ValueValidator {
private detailedInt32Help = "Enter a signed 32-bit integer."; // localize
private detailedInt64Help = "Enter a signed 64-bit integer, in the range (-2^53 - 1, 2^53 - 1)."; // localize
private isInt64: boolean;
constructor(isInt64: boolean = true) {
super();
this.isInt64 = isInt64;
}
public validate(value: string): IValidationResult {
var success: boolean = false;
var help: string = noHelp;
if (value) {
success = EntityPropertyValidationCommon.ValidationRegExp.Integer.test(value) && Utilities.isSafeInteger(value);
if (success) {
var intValue = parseInt(value, 10);
success = !isNaN(intValue);
if (success && !this.isInt64) {
success =
EntityPropertyValidationCommon.Int32.Min <= intValue &&
intValue <= EntityPropertyValidationCommon.Int32.Max;
}
}
}
if (!success) {
help = this.isInt64 ? this.detailedInt64Help : this.detailedInt32Help;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): number {
return parseInt(value, 10);
}
}
// Allow all values for string type, unless the property is required, in which case an empty string is invalid.
class StringValidator extends ValueValidator {
private detailedHelp = "Enter a value up to 64 KB in size."; // localize
private isRequiredHelp = "Enter a value up to 1 KB in size."; // localize
private emptyStringHelp = "Empty string."; // localize
private isRequired: boolean;
constructor(isRequired: boolean) {
super();
this.isRequired = isRequired;
}
public validate(value: string): IValidationResult {
var help: string = this.isRequired ? this.isRequiredHelp : this.detailedHelp;
if (value === null) {
return { isInvalid: false, help: help };
}
// Ensure we validate the string projection of value.
value = String(value);
var success = true;
if (success) {
success = value.length <= (this.isRequired ? MaximumRequiredStringLength : MaximumStringLength);
}
if (success && this.isRequired) {
help = value ? noHelp : this.emptyStringHelp;
}
return { isInvalid: !success, help: help };
}
public parseValue(value: string): string {
return String(value); // Ensure value is converted to string.
}
}
class NotSupportedValidator extends ValueValidator {
private type: string;
constructor(type: string) {
super();
this.type = type;
}
public validate(ignoredValue: string): IValidationResult {
//throw new Errors.NotSupportedError(this.getMessage());
return null;
}
public parseValue(ignoredValue: string): any {
//throw new Errors.NotSupportedError(this.getMessage());
return null;
}
private getMessage(): string {
return "Properties of type " + this.type + " are not supported.";
}
}
class PropertyValidatorFactory {
public getValidator(type: string, isRequired: boolean) {
var validator: IValueValidator = null;
// TODO classify rest of Cassandra types/create validators for them
switch (type) {
case StorageExplorerConstants.TableType.Boolean:
case StorageExplorerConstants.CassandraType.Boolean:
validator = new BooleanValueValidator();
break;
case StorageExplorerConstants.TableType.DateTime:
validator = new DateTimeValueValidator();
break;
case StorageExplorerConstants.TableType.Double:
case StorageExplorerConstants.CassandraType.Decimal:
case StorageExplorerConstants.CassandraType.Double:
case StorageExplorerConstants.CassandraType.Float:
validator = new DoubleValueValidator();
break;
case StorageExplorerConstants.TableType.Guid:
case StorageExplorerConstants.CassandraType.Uuid:
validator = new GuidValueValidator();
break;
case StorageExplorerConstants.TableType.Int32:
case StorageExplorerConstants.CassandraType.Int:
// TODO create separate validators for smallint and tinyint
case StorageExplorerConstants.CassandraType.Smallint:
case StorageExplorerConstants.CassandraType.Tinyint:
validator = new IntegerValueValidator(/* isInt64 */ false);
break;
case StorageExplorerConstants.TableType.Int64:
case StorageExplorerConstants.CassandraType.Bigint:
case StorageExplorerConstants.CassandraType.Varint:
validator = new IntegerValueValidator(/* isInt64 */ true);
break;
case StorageExplorerConstants.TableType.String:
case StorageExplorerConstants.CassandraType.Text:
case StorageExplorerConstants.CassandraType.Ascii:
case StorageExplorerConstants.CassandraType.Varchar:
validator = new StringValidator(isRequired);
break;
case "Key":
validator = new KeyValidator();
break;
default:
validator = new NotSupportedValidator(type);
break;
}
return validator;
}
}
interface ITypeValidatorMap {
[type: string]: IValueValidator;
}
export default class EntityPropertyValueValidator {
private validators: ITypeValidatorMap;
private validatorFactory: PropertyValidatorFactory;
private isRequired: boolean;
constructor(isRequired: boolean) {
this.validators = {};
this.validatorFactory = new PropertyValidatorFactory();
this.isRequired = isRequired;
}
public validate(value: string, type: string): IValidationResult {
var validator: IValueValidator = this.getValidator(type);
return validator ? validator.validate(value) : null; // Should not happen.
}
public parseValue(value: string, type: string): any {
var validator: IValueValidator = this.getValidator(type);
return validator ? validator.parseValue(value) : null; // Should not happen.
}
private getValidator(type: string): IValueValidator {
var validator: IValueValidator = this.validators[type];
if (!validator) {
validator = this.validatorFactory.getValidator(type, this.isRequired);
this.validators[type] = validator;
}
return validator;
}
}