SQL Operations Studio Public Preview 1 (0.23) release source code
19
src/sql/parts/grid/common/gridContentEvents.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export let ResizeContents = 'ResizeContents';
|
||||
export let RefreshContents = 'RefreshContents';
|
||||
export let ToggleResultPane = 'ToggleResultPane';
|
||||
export let ToggleMessagePane = 'ToggleMessagePane';
|
||||
export let CopySelection = 'CopySelection';
|
||||
export let CopyWithHeaders = 'CopyWithHeaders';
|
||||
export let CopyMessagesSelection = 'CopyMessagesSelection';
|
||||
export let SelectAll = 'SelectAll';
|
||||
export let SelectAllMessages = 'SelectAllMessages';
|
||||
export let SaveAsCsv = 'SaveAsCSV';
|
||||
export let SaveAsJSON = 'SaveAsJSON';
|
||||
export let SaveAsExcel = 'SaveAsExcel';
|
||||
124
src/sql/parts/grid/common/interfaces.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IColumnDefinition, IObservableCollection, IGridDataRow } from 'angular2-slickgrid';
|
||||
|
||||
export interface ISlickRange {
|
||||
fromCell: number;
|
||||
fromRow: number;
|
||||
toCell: number;
|
||||
toRow: number;
|
||||
}
|
||||
|
||||
export interface IGridIcon {
|
||||
showCondition: () => boolean;
|
||||
icon: () => string;
|
||||
hoverText: () => string;
|
||||
functionality: (batchId: number, resultId: number, index: number) => void;
|
||||
}
|
||||
|
||||
export interface IMessageLink {
|
||||
uri: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface IMessage {
|
||||
batchId?: number;
|
||||
time: string;
|
||||
message: string;
|
||||
isError: boolean;
|
||||
link?: IMessageLink;
|
||||
}
|
||||
|
||||
export interface IGridIcon {
|
||||
showCondition: () => boolean;
|
||||
icon: () => string;
|
||||
hoverText: () => string;
|
||||
functionality: (batchId: number, resultId: number, index: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified interface for a Range object returned by the Rangy javascript plugin
|
||||
*
|
||||
* @export
|
||||
* @interface IRange
|
||||
*/
|
||||
export interface IRange {
|
||||
selectNodeContents(el): void;
|
||||
/**
|
||||
* Returns any user-visible text covered under the range, using standard HTML Range API calls
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
* @memberOf IRange
|
||||
*/
|
||||
toString(): string;
|
||||
/**
|
||||
* Replaces the current selection with this range. Equivalent to rangy.getSelection().setSingleRange(range).
|
||||
*
|
||||
*
|
||||
* @memberOf IRange
|
||||
*/
|
||||
select(): void;
|
||||
|
||||
/**
|
||||
* Returns the `Document` element containing the range
|
||||
*
|
||||
* @returns {Document}
|
||||
*
|
||||
* @memberOf IRange
|
||||
*/
|
||||
getDocument(): Document;
|
||||
|
||||
/**
|
||||
* Detaches the range so it's no longer tracked by Rangy using DOM manipulation
|
||||
*
|
||||
*
|
||||
* @memberOf IRange
|
||||
*/
|
||||
detach(): void;
|
||||
|
||||
/**
|
||||
* Gets formatted text under a range. This is an improvement over toString() which contains unnecessary whitespac
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
* @memberOf IRange
|
||||
*/
|
||||
text(): string;
|
||||
}
|
||||
|
||||
export interface IGridDataSet {
|
||||
dataRows: IObservableCollection<IGridDataRow>;
|
||||
columnDefinitions: IColumnDefinition[];
|
||||
resized: any; // EventEmitter<any>;
|
||||
totalRows: number;
|
||||
batchId: number;
|
||||
resultId: number;
|
||||
maxHeight: number | string;
|
||||
minHeight: number | string;
|
||||
}
|
||||
|
||||
export enum SaveFormat {
|
||||
CSV = 'csv',
|
||||
JSON = 'json',
|
||||
EXCEL = 'excel',
|
||||
XML = 'xml'
|
||||
}
|
||||
|
||||
export interface IGridInfo {
|
||||
batchIndex: number;
|
||||
resultSetNumber: number;
|
||||
selection: ISlickRange[];
|
||||
gridIndex: number;
|
||||
rowIndex?: number;
|
||||
}
|
||||
export interface ISaveRequest {
|
||||
format: SaveFormat;
|
||||
batchIndex: number;
|
||||
resultSetNumber: number;
|
||||
selection: ISlickRange[];
|
||||
}
|
||||
26
src/sql/parts/grid/directives/mousedown.directive.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/* --------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
import { ElementRef, Directive, Inject, Output, EventEmitter, forwardRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[mousedown]'
|
||||
})
|
||||
export class MouseDownDirective {
|
||||
@Output('mousedown') onMouseDown = new EventEmitter();
|
||||
|
||||
constructor(@Inject(forwardRef(() => ElementRef)) private _el: ElementRef) {
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
let $gridCanvas = $(this._el.nativeElement).find('.grid-canvas');
|
||||
$gridCanvas.on('mousedown', () => {
|
||||
self.onMouseDown.emit();
|
||||
});
|
||||
let jQueryCast: any = $;
|
||||
let mouseDownFuncs: any[] = jQueryCast._data($gridCanvas[0], 'events')['mousedown'];
|
||||
// reverse the event array so that our event fires first.
|
||||
mouseDownFuncs.reverse();
|
||||
});
|
||||
}
|
||||
}
|
||||
24
src/sql/parts/grid/directives/scroll.directive.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/* --------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
* ------------------------------------------------------------------------------------------ */
|
||||
import { ElementRef, Directive, Input, Output, EventEmitter, forwardRef,
|
||||
Inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
@Directive({
|
||||
selector: '[onScroll]'
|
||||
})
|
||||
export class ScrollDirective {
|
||||
@Input() scrollEnabled: boolean = true;
|
||||
@Output('onScroll') onScroll = new EventEmitter();
|
||||
|
||||
constructor(@Inject(forwardRef(() => ElementRef)) private _el: ElementRef) {
|
||||
const self = this;
|
||||
Observable.fromEvent(this._el.nativeElement, 'scroll').subscribe((event) => {
|
||||
if (self.scrollEnabled) {
|
||||
self.onScroll.emit(self._el.nativeElement.scrollTop);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
220
src/sql/parts/grid/load/css/qp.css
Normal file
@@ -0,0 +1,220 @@
|
||||
div.qp-node {
|
||||
background-color: #FFFFCC;
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
div.qp-statement-header {
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
font-size: 12px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
div.qp-node,
|
||||
div.qp-tt {
|
||||
font-size: 11px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.qp-node>div {
|
||||
font-family: Monospace;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div[class|='qp-icon'] {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.qp-tt {
|
||||
top: 4em;
|
||||
left: 2em;
|
||||
border: 1px solid black;
|
||||
background-color: #FFFFEE;
|
||||
padding: 2px;
|
||||
width: 30em;
|
||||
}
|
||||
|
||||
.qp-tt div,
|
||||
.qp-tt table {
|
||||
font-family: Sans-Serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.qp-tt table {
|
||||
border-width: 0px;
|
||||
border-spacing: 0px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qp-tt td,
|
||||
.qp-tt th {
|
||||
font-size: 11px;
|
||||
border-bottom: solid 1px Black;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.qp-tt td {
|
||||
text-align: right;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.qp-tt th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.qp-bold,
|
||||
.qp-tt-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.qp-tt-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.qp-icon-Catchall{background: url('qp_icons.png') -96px -0px }
|
||||
.qp-icon-ArithmeticExpression{background: url('qp_icons.png') -0px -0px }
|
||||
.qp-icon-Assert{background: url('qp_icons.png') -32px -0px }
|
||||
.qp-icon-Assign{background: url('qp_icons.png') -64px -0px }
|
||||
.qp-icon-Bitmap{background: url('qp_icons.png') -96px -0px }
|
||||
.qp-icon-BookmarkLookup{background: url('qp_icons.png') -128px -0px }
|
||||
.qp-icon-ClusteredIndexDelete{background: url('qp_icons.png') -160px -0px }
|
||||
.qp-icon-ClusteredIndexInsert{background: url('qp_icons.png') -192px -0px }
|
||||
.qp-icon-ClusteredIndexScan{background: url('qp_icons.png') -224px -0px }
|
||||
.qp-icon-ClusteredIndexSeek{background: url('qp_icons.png') -256px -0px }
|
||||
.qp-icon-KeyLookup{background: url('qp_icons.png') -256px -0px }
|
||||
.qp-icon-ClusteredIndexUpdate{background: url('qp_icons.png') -288px -0px }
|
||||
.qp-icon-Collapse{background: url('qp_icons.png') -0px -32px }
|
||||
.qp-icon-ComputeScalar{background: url('qp_icons.png') -32px -32px }
|
||||
.qp-icon-Concatenation{background: url('qp_icons.png') -64px -32px }
|
||||
.qp-icon-ConstantScan{background: url('qp_icons.png') -96px -32px }
|
||||
.qp-icon-Convert{background: url('qp_icons.png') -128px -32px }
|
||||
.qp-icon-CursorCatchall{background: url('qp_icons.png') -96px -0px }
|
||||
.qp-icon-Declare{background: url('qp_icons.png') -160px -32px }
|
||||
.qp-icon-Delete{background: url('qp_icons.png') -288px -160px }
|
||||
.qp-icon-DistributeStreams{background: url('qp_icons.png') -224px -32px }
|
||||
.qp-icon-Dynamic{background: url('qp_icons.png') -256px -32px }
|
||||
.qp-icon-EagerSpool{background: url('qp_icons.png') -192px -160px }
|
||||
.qp-icon-FetchQuery{background: url('qp_icons.png') -288px -32px }
|
||||
.qp-icon-Filter{background: url('qp_icons.png') -0px -64px }
|
||||
.qp-icon-GatherStreams{background: url('qp_icons.png') -32px -64px }
|
||||
.qp-icon-HashMatch{background: url('qp_icons.png') -64px -64px }
|
||||
.qp-icon-HashMatchRoot{background: url('qp_icons.png') -64px -64px }
|
||||
.qp-icon-HashMatchTeam{background: url('qp_icons.png') -64px -64px }
|
||||
.qp-icon-If{background: url('qp_icons.png') -96px -64px }
|
||||
.qp-icon-Insert{background: url('qp_icons.png') -0px -192px }
|
||||
.qp-icon-InsertedScan{background: url('qp_icons.png') -128px -64px }
|
||||
.qp-icon-Intrinsic{background: url('qp_icons.png') -160px -64px }
|
||||
.qp-icon-IteratorCatchall{background: url('qp_icons.png') -96px -0px }
|
||||
.qp-icon-Keyset{background: url('qp_icons.png') -192px -64px }
|
||||
.qp-icon-LanguageElementCatchall{background: url('qp_icons.png') -96px -0px }
|
||||
.qp-icon-LazySpool{background: url('qp_icons.png') -192px -160px }
|
||||
.qp-icon-LogRowScan{background: url('qp_icons.png') -224px -64px }
|
||||
.qp-icon-MergeInterval{background: url('qp_icons.png') -256px -64px }
|
||||
.qp-icon-MergeJoin{background: url('qp_icons.png') -288px -64px }
|
||||
.qp-icon-NestedLoops{background: url('qp_icons.png') -0px -96px }
|
||||
.qp-icon-NonclusteredIndexDelete{background: url('qp_icons.png') -32px -96px }
|
||||
.qp-icon-NonclusteredIndexInsert{background: url('qp_icons.png') -64px -96px }
|
||||
.qp-icon-IndexScan{background: url('qp_icons.png') -96px -96px }
|
||||
.qp-icon-IndexSeek{background: url('qp_icons.png') -128px -96px }
|
||||
.qp-icon-NonclusteredIndexSpool{background: url('qp_icons.png') -160px -96px }
|
||||
.qp-icon-NonclusteredIndexUpdate{background: url('qp_icons.png') -192px -96px }
|
||||
.qp-icon-OnlineIndexInsert{background: url('qp_icons.png') -224px -96px }
|
||||
.qp-icon-ParameterTableScan{background: url('qp_icons.png') -256px -96px }
|
||||
.qp-icon-PopulationQuery{background: url('qp_icons.png') -288px -96px }
|
||||
.qp-icon-RdiLookup{background: url('qp_icons.png') -0px -128px }
|
||||
.qp-icon-RefreshQuery{background: url('qp_icons.png') -32px -128px }
|
||||
.qp-icon-RemoteDelete{background: url('qp_icons.png') -64px -128px }
|
||||
.qp-icon-RemoteInsert{background: url('qp_icons.png') -96px -128px }
|
||||
.qp-icon-RemoteQuery{background: url('qp_icons.png') -128px -128px }
|
||||
.qp-icon-RemoteScan{background: url('qp_icons.png') -160px -128px }
|
||||
.qp-icon-RemoteUpdate{background: url('qp_icons.png') -192px -128px }
|
||||
.qp-icon-RepartitionStreams{background: url('qp_icons.png') -224px -128px }
|
||||
.qp-icon-Result{background: url('qp_icons.png') -256px -128px }
|
||||
.qp-icon-RowCountSpool{background: url('qp_icons.png') -288px -128px }
|
||||
.qp-icon-Segment{background: url('qp_icons.png') -0px -160px }
|
||||
.qp-icon-Sequence{background: url('qp_icons.png') -32px -160px }
|
||||
.qp-icon-Sequenceproject{background: url('qp_icons.png') -64px -160px }
|
||||
.qp-icon-Snapshot{background: url('qp_icons.png') -96px -160px }
|
||||
.qp-icon-Sort{background: url('qp_icons.png') -128px -160px }
|
||||
.qp-icon-Split{background: url('qp_icons.png') -160px -160px }
|
||||
.qp-icon-Spool{background: url('qp_icons.png') -192px -160px }
|
||||
.qp-icon-Statement{background: url('qp_icons.png') -256px -128px }
|
||||
.qp-icon-StreamAggregate{background: url('qp_icons.png') -224px -160px }
|
||||
.qp-icon-Switch{background: url('qp_icons.png') -256px -160px }
|
||||
.qp-icon-TableDelete{background: url('qp_icons.png') -288px -160px }
|
||||
.qp-icon-TableInsert{background: url('qp_icons.png') -0px -192px }
|
||||
.qp-icon-TableScan{background: url('qp_icons.png') -32px -192px }
|
||||
.qp-icon-TableSpool{background: url('qp_icons.png') -64px -192px }
|
||||
.qp-icon-TableUpdate{background: url('qp_icons.png') -96px -192px }
|
||||
.qp-icon-TableValuedFunction{background: url('qp_icons.png') -128px -192px }
|
||||
.qp-icon-Top{background: url('qp_icons.png') -160px -192px }
|
||||
.qp-icon-Udx{background: url('qp_icons.png') -192px -192px }
|
||||
.qp-icon-Update{background: url('qp_icons.png') -96px -192px }
|
||||
.qp-icon-While{background: url('qp_icons.png') -224px -192px }
|
||||
.qp-icon-PopulateQuery{background: url('qp_icons.png') -96px -0px }
|
||||
.qp-icon-StmtCursor{background: url('qp_icons.png') -96px -0px }
|
||||
.qp-icon-SequenceProject{background: url('qp_icons.png') -96px -0px }
|
||||
.qp-icon-FastForward{background: url('qp_icons.png') -96px -0px }
|
||||
.qp-icon-SnapShot{background: url('qp_icons.png') -96px -0px }
|
||||
|
||||
/* Layout - can't touch this */
|
||||
.qp-tt {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
white-space: normal;
|
||||
-webkit-transition-delay: 0.5s;
|
||||
transition-delay: 0.5s;
|
||||
}
|
||||
|
||||
div.qp-node .qp-tt,
|
||||
.qp-noCssTooltip div.qp-node:hover .qp-tt {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
div.qp-node:hover .qp-tt {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.qp-tt table {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qp-node {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.qp-tr {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.qp-tr>div {
|
||||
display: table-cell;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.qp-root {
|
||||
display: table;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.qp-root svg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
BIN
src/sql/parts/grid/load/css/qp_icons.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
2
src/sql/parts/grid/load/loadJquery.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const jQuery = require('jquery');
|
||||
const $ = jQuery;
|
||||
1
src/sql/parts/grid/media/collapsedArrow.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16'><path fill='#646465' d='M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z'/></svg>
|
||||
|
After Width: | Height: | Size: 142 B |
1
src/sql/parts/grid/media/collapsedArrow_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16'><path fill='#e8e8e8' d='M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z'/></svg>
|
||||
|
After Width: | Height: | Size: 142 B |
1
src/sql/parts/grid/media/exitFullScreen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 16H0V0h16v16z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M12 7H9V4l1-1v2l2-2 1 1-2 2h2l-1 1zm-2 1H9v4H4V8h4V6H3v7h7V8zm5-7v14H1V1h14zm-1 1H2v12h12V2z" id="iconBg"/><path class="icon-vs-fg" d="M12.414 8L14 6.414V14H2V2h7.586L8 3.586V6H3v7h7V8h2.414zM4 8v4h5V8H4z" id="iconFg" style="display: none;"/></svg>
|
||||
|
After Width: | Height: | Size: 633 B |
1
src/sql/parts/grid/media/exitFullScreen_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 16H0V0h16v16z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M12 7H9V4l1-1v2l2-2 1 1-2 2h2l-1 1zm-2 1H9v4H4V8h4V6H3v7h7V8zm5-7v14H1V1h14zm-1 1H2v12h12V2z" id="iconBg"/><path class="icon-vs-fg" d="M12.414 8L14 6.414V14H2V2h7.586L8 3.586V6H3v7h7V8h2.414zM4 8v4h5V8H4z" id="iconFg" style="display: none;"/></svg>
|
||||
|
After Width: | Height: | Size: 633 B |
1
src/sql/parts/grid/media/extendFullScreen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> <style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1} </style> <path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas" /> <path class="icon-vs-out" d="M16 16H0V0h16v16z" id="outline" style="display: none;" /> <path class="icon-vs-fg" d="M11 9.414l3-3V14H2V2h7.586l-3 3h2l-1 1H3v7h7V8.414l1-1v2zM4 12h5V8H4v4z" id="iconFg" style="display: none;" /> <path class="icon-vs-bg" d="M7.586 6H3v7h7V8.414L7.586 6zM9 12H4V8h5v4zM1 1v14h14V1H1zm13 13H2V2h12v12zM9 6l2-2H9l1-1h3v3l-1 1V5l-2 2-1-1z" id="iconBg" /> </svg >
|
||||
|
After Width: | Height: | Size: 665 B |
1
src/sql/parts/grid/media/extendFullScreen_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 16H0V0h16v16z" id="outline" style="display: none;"/><path class="icon-vs-fg" d="M11 9.414l3-3V14H2V2h7.586l-3 3h2l-1 1H3v7h7V8.414l1-1v2zM4 12h5V8H4v4z" id="iconFg" style="display: none;"/><path class="icon-vs-bg" d="M7.586 6H3v7h7V8.414L7.586 6zM9 12H4V8h5v4zM1 1v14h14V1H1zm13 13H2V2h12v12zM9 6l2-2H9l1-1h3v3l-1 1V5l-2 2-1-1z" id="iconBg"/></svg>
|
||||
|
After Width: | Height: | Size: 653 B |
79
src/sql/parts/grid/media/flexbox.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.fullsize {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.headersVisible > .fullsize {
|
||||
height: calc(100% - 38px);
|
||||
}
|
||||
|
||||
/* vertical box styles */
|
||||
.vertBox {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.vertBox .boxRow.header {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.vertBox .boxRow.content {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.edgesPadding {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.messagesTopSpacing {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.results {
|
||||
flex: 100 1 0;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.maxHeight {
|
||||
max-height: fit-content;
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1 100 0;
|
||||
min-height: 25%;
|
||||
}
|
||||
|
||||
.minHeight {
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
/* horizontal box style */
|
||||
.horzBox {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
.horzBox .boxCol.content {
|
||||
flex: 1 1 1%;
|
||||
overflow: auto;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* testing
|
||||
|
||||
.vertBox {
|
||||
border: 1px solid green;
|
||||
}
|
||||
|
||||
.horzBox {
|
||||
border: 1px solid red;
|
||||
}
|
||||
*/
|
||||
1
src/sql/parts/grid/media/saveCsv.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}.icon-vs-action-blue{fill:#00539c}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M14 4.552V13c-.028.825-.593 2-2.035 2h-8C3.012 15 2 14.299 2 13V8H.586l2-2H0V2h2.586l-2-2h4.828l1 1h3.608L14 4.552z" id="outline" style="display: none;"/><path class="icon-vs-fg" d="M9 3.586L8.414 3H9v.586zM12 6h-2v7h2V6zm-6 7V7.414L5.414 8H4v5h2zm1 0h2V4.414l-1 1V6h-.586L7 6.414V13z" id="iconFg" style="display: none;"/><path class="icon-vs-bg" d="M8 5.414V6h-.586L8 5.414zM13 5v8s-.035 1-1.035 1h-8S3 14 3 13V8h1v5h2V7.414l1-1V13h2V4.414L9.414 4 9 3.586V3h-.586l-1-1h2.227L13 5zm-1 1h-2v7h2V6z" id="iconBg"/><path class="icon-vs-action-blue" d="M8 4L5 7H3l2-2H1V3h4L3 1h2l3 3z" id="colorAction"/></svg>
|
||||
|
After Width: | Height: | Size: 940 B |
1
src/sql/parts/grid/media/saveCsv_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}.icon-vs-action-blue{fill:#75beff}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M14 4.552V13c-.028.825-.593 2-2.035 2h-8C3.012 15 2 14.299 2 13V8H.586l2-2H0V2h2.586l-2-2h4.828l1 1h3.608L14 4.552z" id="outline" style="display: none;"/><path class="icon-vs-fg" d="M9 3.586L8.414 3H9v.586zM12 6h-2v7h2V6zm-6 7V7.414L5.414 8H4v5h2zm1 0h2V4.414l-1 1V6h-.586L7 6.414V13z" id="iconFg" style="display: none;"/><path class="icon-vs-bg" d="M8 5.414V6h-.586L8 5.414zM13 5v8s-.035 1-1.035 1h-8S3 14 3 13V8h1v5h2V7.414l1-1V13h2V4.414L9.414 4 9 3.586V3h-.586l-1-1h2.227L13 5zm-1 1h-2v7h2V6z" id="iconBg"/><path class="icon-vs-action-blue" d="M8 4L5 7H3l2-2H1V3h4L3 1h2l3 3z" id="colorAction"/></svg>
|
||||
|
After Width: | Height: | Size: 940 B |
1
src/sql/parts/grid/media/saveExcel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-fg{fill:#f0eff1}.icon-vs-action-blue{fill:#00539c}</style><path class="icon-canvas-transparent" d="M16 0v16H0V0h16z" id="canvas"/><path class="icon-vs-out" d="M16 7v8H3.964C3.012 15 2 14.299 2 13V8H.586l2-2H0V2h2.586l-2-2h4.828l1 1h3.608L14 4.552V7h2z" id="outline" style="display: none;"/><path class="icon-vs-fg" d="M9 6h3v1H7v6H4V8h1.414L9 4.414V6zm0-3h-.586L9 3.586V3zm0 10h1v-1H9v1zm0-2h1v-1H9v1zm2 2h1v-1h-1v1zm2-3v1h1v-1h-1zm-2 1h1v-1h-1v1zm2 2h1v-1h-1v1z" id="iconFg" style="display: none;"/><path class="icon-vs-bg" d="M8.414 3l-1-1h2.227L13 5v2h-1V6H9V4.414L9.414 4 9 3.586V3h-.586zM4 8H3v5c0 1 .964 1 .964 1H7v-1H4V8zm11 0v6H8V8h7zm-5 4H9v1h1v-1zm0-2H9v1h1v-1zm2 2h-1v1h1v-1zm0-2h-1v1h1v-1zm2 2h-1v1h1v-1zm0-2h-1v1h1v-1z" id="iconBg"/><g id="colorAction"><path class="icon-vs-action-blue" d="M8 4L5 7H3l2-2H1V3h4L3 1h2l3 3z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1022 B |
1
src/sql/parts/grid/media/saveExcel_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-fg{fill:#2b282e}.icon-vs-action-blue{fill:#75beff}</style><path class="icon-canvas-transparent" d="M16 0v16H0V0h16z" id="canvas"/><path class="icon-vs-out" d="M16 7v8H3.964C3.012 15 2 14.299 2 13V8H.586l2-2H0V2h2.586l-2-2h4.828l1 1h3.608L14 4.552V7h2z" id="outline" style="display: none;"/><path class="icon-vs-fg" d="M9 6h3v1H7v6H4V8h1.414L9 4.414V6zm0-3h-.586L9 3.586V3zm0 10h1v-1H9v1zm0-2h1v-1H9v1zm2 2h1v-1h-1v1zm2-3v1h1v-1h-1zm-2 1h1v-1h-1v1zm2 2h1v-1h-1v1z" id="iconFg" style="display: none;"/><path class="icon-vs-bg" d="M8.414 3l-1-1h2.227L13 5v2h-1V6H9V4.414L9.414 4 9 3.586V3h-.586zM4 8H3v5c0 1 .964 1 .964 1H7v-1H4V8zm11 0v6H8V8h7zm-5 4H9v1h1v-1zm0-2H9v1h1v-1zm2 2h-1v1h1v-1zm0-2h-1v1h1v-1zm2 2h-1v1h1v-1zm0-2h-1v1h1v-1z" id="iconBg"/><g id="colorAction"><path class="icon-vs-action-blue" d="M8 4L5 7H3l2-2H1V3h4L3 1h2l3 3z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1022 B |
1
src/sql/parts/grid/media/saveJson.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#f6f6f6}.icon-vs-out{fill:#f6f6f6}.icon-vs-bg{fill:#424242}.icon-vs-action-blue{fill:#00539c}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 8.38v2.258s-.992-.001-.997-.003c-.012.052-.003.14-.003.281v1.579c0 1.271-.37 2.185-1.054 2.746-.638.522-1.576.759-2.822.759H10v-3.247s1.014-.001 1.037-.003c.004-.046-.037-.117-.037-.214V11.08c0-.943.222-1.606.539-2.072C11.223 8.527 11 7.843 11 6.869V5.468c0-.087.102-.286.094-.325-.02-.002.063-.143.03-.143H10V2h1.124c1.251 0 2.193.265 2.832.81C14.633 3.387 15 4.3 15 5.522V7h.919L16 8.38zM9.414 4l-4-4H.586l2 2H0v4h2v.586L1.586 7H1v.586L.586 8H1v1.638L1.329 11H2v1.536c0 1.247.495 2.149 1.19 2.711.641.517 1.697.753 2.937.753H7v-3.247s-1.011-.001-1.033-.003c-.008-.053.033-.127.033-.228v-1.401c0-.962-.224-1.637-.542-2.111.256-.378.444-.89.511-1.564L9.414 4z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M15 8.38v1.258c-.697 0-1.046.426-1.046 1.278v1.579c0 .961-.223 1.625-.666 1.989-.445.364-1.166.547-2.164.547v-1.278c.383 0 .661-.092.834-.277s.26-.498.26-.94V11.08c0-1.089.349-1.771 1.046-2.044v-.027c-.697-.287-1.046-1-1.046-2.14V5.468c0-.793-.364-1.189-1.094-1.189V3c.993 0 1.714.19 2.16.571s.67 1.031.67 1.952v1.565c0 .861.349 1.292 1.046 1.292zm-9.967 4.142v-1.401c0-1.117-.351-1.816-1.053-2.099v-.027c.429-.175.71-.519.877-.995H3.049c-.173.247-.436.38-.805.38v1.258c.692 0 1.039.419 1.039 1.258v1.641c0 .934.226 1.584.677 1.948s1.174.547 2.167.547v-1.278c-.388 0-.666-.093-.838-.28-.17-.188-.256-.505-.256-.952z" id="iconBg"/><path class="icon-vs-action-blue" d="M8 4L5 7H3l2-2H1V3h4L3 1h2l3 3z" id="colorAction"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
src/sql/parts/grid/media/saveJson_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.icon-canvas-transparent{opacity:0;fill:#2d2d30}.icon-vs-out{fill:#2d2d30}.icon-vs-bg{fill:#c5c5c5}.icon-vs-action-blue{fill:#75beff}</style><path class="icon-canvas-transparent" d="M16 16H0V0h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 8.38v2.258s-.992-.001-.997-.003c-.012.052-.003.14-.003.281v1.579c0 1.271-.37 2.185-1.054 2.746-.638.522-1.576.759-2.822.759H10v-3.247s1.014-.001 1.037-.003c.004-.046-.037-.117-.037-.214V11.08c0-.943.222-1.606.539-2.072C11.223 8.527 11 7.843 11 6.869V5.468c0-.087.102-.286.094-.325-.02-.002.063-.143.03-.143H10V2h1.124c1.251 0 2.193.265 2.832.81C14.633 3.387 15 4.3 15 5.522V7h.919L16 8.38zM9.414 4l-4-4H.586l2 2H0v4h2v.586L1.586 7H1v.586L.586 8H1v1.638L1.329 11H2v1.536c0 1.247.495 2.149 1.19 2.711.641.517 1.697.753 2.937.753H7v-3.247s-1.011-.001-1.033-.003c-.008-.053.033-.127.033-.228v-1.401c0-.962-.224-1.637-.542-2.111.256-.378.444-.89.511-1.564L9.414 4z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M15 8.38v1.258c-.697 0-1.046.426-1.046 1.278v1.579c0 .961-.223 1.625-.666 1.989-.445.364-1.166.547-2.164.547v-1.278c.383 0 .661-.092.834-.277s.26-.498.26-.94V11.08c0-1.089.349-1.771 1.046-2.044v-.027c-.697-.287-1.046-1-1.046-2.14V5.468c0-.793-.364-1.189-1.094-1.189V3c.993 0 1.714.19 2.16.571s.67 1.031.67 1.952v1.565c0 .861.349 1.292 1.046 1.292zm-9.967 4.142v-1.401c0-1.117-.351-1.816-1.053-2.099v-.027c.429-.175.71-.519.877-.995H3.049c-.173.247-.436.38-.805.38v1.258c.692 0 1.039.419 1.039 1.258v1.641c0 .934.226 1.584.677 1.948s1.174.547 2.167.547v-1.278c-.388 0-.666-.093-.838-.28-.17-.188-.256-.505-.256-.952z" id="iconBg"/><path class="icon-vs-action-blue" d="M8 4L5 7H3l2-2H1V3h4L3 1h2l3 3z" id="colorAction"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
190
src/sql/parts/grid/media/slick.grid.css
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
IMPORTANT:
|
||||
In order to preserve the uniform grid appearance, all cell styles need to have padding, margin and border sizes.
|
||||
No built-in (selected, editable, highlight, flashing, invalid, loading, :focus) or user-specified CSS
|
||||
classes should alter those!
|
||||
*/
|
||||
|
||||
.slick-header.ui-state-default, .slick-headerrow.ui-state-default, .slick-footerrow.ui-state-default {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-left: 0px !important;
|
||||
}
|
||||
|
||||
.slick-header-columns, .slick-headerrow-columns, .slick-footerrow-columns {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slick-header-column.ui-state-default {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
/*box-sizing: content-box !important; use this for Firefox! */
|
||||
overflow: hidden;
|
||||
-o-text-overflow: ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
border-right: 1px solid silver;
|
||||
border-left: 0px !important;
|
||||
border-top: 0px !important;
|
||||
border-bottom: 2px solid #bbb;
|
||||
float: left;
|
||||
background-color: #eee;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.slick-headerrow-column.ui-state-default, .slick-footerrow-column.ui-state-default {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.slick-header-column-sorted {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.slick-sort-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 5px;
|
||||
margin-left: 4px;
|
||||
margin-top: 6px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.slick-sort-indicator-desc {
|
||||
background: url(images/sort-desc.gif);
|
||||
}
|
||||
|
||||
.slick-sort-indicator-asc {
|
||||
background: url(images/sort-asc.gif);
|
||||
}
|
||||
|
||||
.slick-resizable-handle {
|
||||
position: absolute;
|
||||
font-size: 0.1px;
|
||||
display: block;
|
||||
cursor: col-resize;
|
||||
width: 4px;
|
||||
right: 0px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slick-sortable-placeholder {
|
||||
background: silver;
|
||||
}
|
||||
|
||||
.grid-canvas {
|
||||
position: relative;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.slick-row.ui-widget-content, .slick-row.ui-state-active {
|
||||
position: absolute;
|
||||
border: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slick-cell, .slick-headerrow-column , .slick-footerrow-column{
|
||||
position: absolute;
|
||||
border: 1px solid transparent;
|
||||
border-right: 1px dotted silver;
|
||||
border-bottom-color: silver;
|
||||
overflow: hidden;
|
||||
-o-text-overflow: ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
z-index: 1;
|
||||
padding: 1px 2px 2px 1px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
.slick-cell, .slick-headerrow-column{
|
||||
border-bottom-color: silver;
|
||||
}
|
||||
.slick-footerrow-column {
|
||||
border-top-color: silver;
|
||||
}
|
||||
|
||||
.slick-group {
|
||||
}
|
||||
|
||||
.slick-group-toggle {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.slick-cell.highlighted {
|
||||
background: lightskyblue;
|
||||
background: rgba(0, 0, 255, 0.2);
|
||||
-webkit-transition: all 0.5s;
|
||||
-moz-transition: all 0.5s;
|
||||
-o-transition: all 0.5s;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.slick-cell.flashing {
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
|
||||
.slick-cell.editable {
|
||||
z-index: 11;
|
||||
overflow: visible;
|
||||
background: white;
|
||||
border-color: black;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.slick-cell:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slick-cell > .row-number {
|
||||
color: var(--color-content);
|
||||
font-style: italic;
|
||||
font-weight: lighter;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.slick-reorder-proxy {
|
||||
display: inline-block;
|
||||
background: blue;
|
||||
opacity: 0.15;
|
||||
filter: alpha(opacity = 15);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.slick-reorder-guide {
|
||||
display: inline-block;
|
||||
height: 2px;
|
||||
background: blue;
|
||||
opacity: 0.7;
|
||||
filter: alpha(opacity = 70);
|
||||
}
|
||||
|
||||
.slick-selection {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
border: 2px dashed black;
|
||||
}
|
||||
|
||||
.slick-column-icon {
|
||||
margin-right: 0.5em;
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
.slick-header-column.ui-state-default.slick-header-with-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.slick-header-with-icon .slick-column-name {
|
||||
width: calc(100% - 1em);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
390
src/sql/parts/grid/media/slickColorTheme.css
Normal file
@@ -0,0 +1,390 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* Theme agnostic
|
||||
*
|
||||
*/
|
||||
|
||||
.errorMessage {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.batchMessage {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.slick-cell a, a:link {
|
||||
color: var(--color-grid-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.slick-cell a:hover {
|
||||
color: var(--color-grid-link-hover);
|
||||
}
|
||||
|
||||
.resultsMessageValue a, a:link {
|
||||
color: var(--color-grid-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.resultsMessageValue a:hover {
|
||||
color: var(--color-grid-link-hover);
|
||||
}
|
||||
|
||||
.grid .slick-cell.dirtyCell {
|
||||
color: var(--color-grid-dirty-text);
|
||||
background-color: var(--color-grid-dirty-background);
|
||||
}
|
||||
|
||||
.grid .slick-cell.dirtyRowHeader {
|
||||
background-color: var(--color-grid-dirty-background);
|
||||
}
|
||||
|
||||
|
||||
.slick-cell.dirtyRowHeader > .row-number {
|
||||
color: var(--color-grid-dirty-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* vs theme
|
||||
*
|
||||
*/
|
||||
|
||||
.vs .slickgridContainer {
|
||||
--color-content: #101010;
|
||||
--color-content-disabled: #a9a9a9;
|
||||
--color-error: #E81123;
|
||||
--color-success: #7CD300;
|
||||
--color-bg-header: hsla(0,0%,50%,.2);
|
||||
--color-resize-handle: grey;
|
||||
--color-bg-content-header: #F5F5F5; /* used for color of grid headers */
|
||||
--color-cell-border-active: grey;
|
||||
--color-cell-bg-grid-selected: rgb(173, 214, 255);
|
||||
--color-grid-link: #0078D7;
|
||||
--color-grid-link-hover: #0b93ff;
|
||||
--color-grid-dirty-background: #CCC;
|
||||
--color-grid-dirty-text: #101010;
|
||||
}
|
||||
/* grid styling */
|
||||
|
||||
.vs slick-grid.active .grid .slick-cell.active {
|
||||
border: dotted 1px var(--color-cell-border-active);
|
||||
}
|
||||
|
||||
.vs slick-grid.active .grid .slick-cell.selected {
|
||||
background-color: var(--color-cell-bg-grid-selected);
|
||||
}
|
||||
|
||||
.vs .grid .slick-cell.selected .grid-cell-value-container.missing-value {
|
||||
color: var(--color-content) !important;
|
||||
}
|
||||
|
||||
.vs .boxRow.content.horzBox.slickgrid {
|
||||
border: solid 1px #EEEEF2;
|
||||
}
|
||||
|
||||
/* icons */
|
||||
.vs .gridIcon.extendFullScreen {
|
||||
/* ExtendToFullScreen_16x_vscode */
|
||||
background-image: url("extendFullScreen.svg");
|
||||
}
|
||||
|
||||
.vs .gridIcon.exitFullScreen {
|
||||
/* ExitFullScreen_16x_vscode */
|
||||
background-image: url("exitFullScreen.svg");
|
||||
}
|
||||
|
||||
.vs .gridIcon.saveJson {
|
||||
/* ResultToJSON_16x_vscode */
|
||||
background-image: url("saveJson.svg");
|
||||
}
|
||||
|
||||
.vs .gridIcon.saveCsv {
|
||||
/* ResultToCSV_16x_vscode */
|
||||
background-image: url("saveCsv.svg");
|
||||
}
|
||||
|
||||
.vs .gridIcon.saveExcel {
|
||||
/* ResultToXlsx_16x_vscode */
|
||||
background-image: url("saveExcel.svg");
|
||||
}
|
||||
|
||||
.vs .gridIcon.viewChart {
|
||||
/* ResultToXlsx_16x_vscode */
|
||||
background-image: url("viewChart.svg");
|
||||
}
|
||||
|
||||
/* headers */
|
||||
.vs .resultsMessageHeader {
|
||||
background: var(--color-bg-header);
|
||||
color: var(--color-content);
|
||||
}
|
||||
|
||||
.vs .resultsViewCollapsible:not(.collapsed) {
|
||||
background-image: url("uncollapsedArrow.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 2px;
|
||||
}
|
||||
|
||||
.vs .resultsViewCollapsible {
|
||||
background-image: url("collapsedArrow.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 2px;
|
||||
}
|
||||
|
||||
.vs .queryResultsShortCut {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
/* scroll bar */
|
||||
|
||||
.vs ::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
}
|
||||
.vs ::-webkit-scrollbar-thumb {
|
||||
background: hsla(0,0%,47%,.4);
|
||||
}
|
||||
|
||||
.vs ::-webkit-scrollbar-thumb:hover {
|
||||
background: hsla(0,0%,39%,.7);
|
||||
}
|
||||
|
||||
.vs ::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(85,85,85,0.8);
|
||||
}
|
||||
|
||||
.vs ::-webkit-scrollbar-track {
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
.vs ::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vs .monaco-workbench input {
|
||||
color: var(--color-content);
|
||||
}
|
||||
|
||||
.vs .monaco-workbench .input {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/*
|
||||
* vs-dark theme
|
||||
*
|
||||
*/
|
||||
|
||||
.vs-dark .slickgridContainer {
|
||||
--color-content: #E5E5E5;
|
||||
--color-content-disabled: grey;
|
||||
--color-error: #E81123;
|
||||
--color-success: #7CD300;
|
||||
--color-bg-header: hsla(0,0%,50%,.2); /* used for pane toolbars */
|
||||
--color-resize-handle: #4d4d4d;
|
||||
--color-bg-content-header: #333334; /* used for color of grid headers */
|
||||
--color-cell-border-active: white;
|
||||
--color-cell-bg-grid-selected: rgb(38, 79, 120);
|
||||
--color-grid-link: #FF6000;
|
||||
--color-grid-link-hover: #ff8033;
|
||||
--color-grid-dirty-background: #4d4d4d;
|
||||
--color-grid-dirty-text: #E5E5E5;
|
||||
}
|
||||
|
||||
/* grid styling */
|
||||
|
||||
.vs-dark slick-grid.active .grid .slick-cell.active {
|
||||
border: dotted 1px var(--color-cell-border-active);
|
||||
}
|
||||
|
||||
.vs-dark slick-grid.active .grid .slick-cell.selected {
|
||||
background-color: var(--color-cell-bg-grid-selected);
|
||||
}
|
||||
|
||||
.vs-dark .grid .slick-cell.selected .grid-cell-value-container.missing-value {
|
||||
color: var(--color-content) !important;
|
||||
}
|
||||
|
||||
.vs-dark .boxRow.content.horzBox.slickgrid {
|
||||
border: solid 1px #2D2D30;
|
||||
}
|
||||
|
||||
/* icons */
|
||||
.vs-dark .gridIcon.extendFullScreen,
|
||||
.hc-black .gridIcon.extendFullScreen {
|
||||
/* ExtendToFullScreen_16x_vscode_inverse.svg */
|
||||
background-image: url("extendFullScreen_inverse.svg");
|
||||
}
|
||||
|
||||
.vs-dark .gridIcon.exitFullScreen,
|
||||
.hc-black .gridIcon.exitFullScreen {
|
||||
/* ExitFullScreen_16x_vscode_inverse.svg */
|
||||
background-image: url("exitFullScreen_inverse.svg");
|
||||
}
|
||||
|
||||
.vs-dark .gridIcon.saveJson,
|
||||
.hc-black .gridIcon.saveJson {
|
||||
/* ResultToJSON_16x_vscode_inverse.svg */
|
||||
background-image: url("saveJson_inverse.svg");
|
||||
}
|
||||
|
||||
.vs-dark .gridIcon.saveCsv,
|
||||
.hc-black .gridIcon.saveCsv {
|
||||
/* ResultToCSV_16x_vscode_inverse.svg */
|
||||
background-image: url("saveCsv_inverse.svg");
|
||||
}
|
||||
|
||||
.vs-dark .gridIcon.saveExcel,
|
||||
.hc-black .gridIcon.saveExcel {
|
||||
/* ResultToXlsx_16x_vscode_inverse.svg */
|
||||
background-image: url("saveExcel_inverse.svg");
|
||||
}
|
||||
|
||||
.vs-dark .gridIcon.viewChart,
|
||||
.hc-black .gridIcon.viewChart {
|
||||
/* ResultToXlsx_16x_vscode */
|
||||
background-image: url("viewChart_inverse.svg");
|
||||
}
|
||||
|
||||
|
||||
/* headers */
|
||||
.vs-dark .resultsMessageHeader {
|
||||
background: var(--color-bg-header);
|
||||
color: var(--color-content);
|
||||
}
|
||||
|
||||
.vs-dark .resultsViewCollapsible:not(.collapsed),
|
||||
.hc-black .resultsViewCollapsible:not(.collapsed) {
|
||||
background-image:url("uncollapsedArrow_inverse.svg");
|
||||
background-repeat:no-repeat;
|
||||
background-position: 2px;
|
||||
}
|
||||
|
||||
.vs-dark .resultsViewCollapsible,
|
||||
.hc-black .resultsViewCollapsible {
|
||||
background-image: url("collapsedArrow_inverse.svg");
|
||||
background-repeat:no-repeat;
|
||||
background-position: 2px;
|
||||
}
|
||||
|
||||
.vs-dark .queryResultsShortCut {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
/* scroll bar */
|
||||
|
||||
.vs-dark ::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.vs-dark ::-webkit-scrollbar-thumb {
|
||||
background: hsla(0,0%,47%,.4);
|
||||
}
|
||||
|
||||
.vs-dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: hsla(0,0%,39%,.7);
|
||||
}
|
||||
|
||||
.vs-dark ::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(85,85,85,0.8);
|
||||
}
|
||||
|
||||
.vs-dark ::-webkit-scrollbar-track {
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
.vs-dark ::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench input, .vs-dark .monaco-workbench .input {
|
||||
color: var(--color-content);
|
||||
background-color: #3C3C3C;
|
||||
}
|
||||
|
||||
/*
|
||||
* hc-black theme
|
||||
*
|
||||
*/
|
||||
|
||||
.hc-black .slickgridContainer {
|
||||
--color-content: #E5E5E5;
|
||||
--color-content-disabled: grey;
|
||||
--color-error: #E81123;
|
||||
--color-success: #7CD300;
|
||||
--color-bg-header: hsla(0,0%,50%,.2); /* used for pane toolbars */
|
||||
--color-resize-handle: #4d4d4d;
|
||||
--color-bg-content-header: #333334; /* used for color of grid headers */
|
||||
--color-cell-border-active: orange;
|
||||
--color-cell-bg-grid-selected: rgb(38, 79, 120);
|
||||
--color-grid-link: #FF6000;
|
||||
--color-grid-link-hover: #ff8033;
|
||||
--color-grid-dirty-background: #FFF;
|
||||
--color-grid-dirty-text: #000;
|
||||
}
|
||||
|
||||
/* grid styling */
|
||||
|
||||
.hc-black slick-grid.active .grid .slick-cell.active {
|
||||
border: solid 1px var(--color-cell-border-active);
|
||||
}
|
||||
|
||||
.hc-black slick-grid.active .grid .slick-cell.selected {
|
||||
background-color: var(--color-cell-bg-grid-selected);
|
||||
}
|
||||
|
||||
.hc-black .grid .slick-cell.selected .grid-cell-value-container.missing-value {
|
||||
color: var(--color-content) !important;
|
||||
}
|
||||
|
||||
.hc-black .boxRow.content.horzBox.slickgrid {
|
||||
border: solid 1px #2D2D30;
|
||||
}
|
||||
|
||||
/* headers */
|
||||
.hc-black .resultsMessageHeader {
|
||||
background: var(--color-bg-header);
|
||||
color: var(--color-content);
|
||||
}
|
||||
|
||||
.hc-black .queryResultsShortCut {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
/* scroll bar */
|
||||
|
||||
.hc-black ::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.hc-black ::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(111, 195, 223, 0.3);
|
||||
}
|
||||
|
||||
.hc-black ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(111, 195, 223, 0.8);
|
||||
}
|
||||
|
||||
.hc-black ::-webkit-scrollbar-thumb:active {
|
||||
background-color: rgba(111, 195, 223, 0.8);
|
||||
}
|
||||
|
||||
.hc-black ::-webkit-scrollbar-track {
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
.hc-black ::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench input {
|
||||
color: #000;
|
||||
background-color: #FFF;
|
||||
}
|
||||
116
src/sql/parts/grid/media/slickGrid.css
Normal file
@@ -0,0 +1,116 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.grid-cell-padding {
|
||||
padding: .5em .8em .4em
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
border-top-style: none;
|
||||
color: var(--color-content, #101010);
|
||||
letter-spacing: .03em;
|
||||
border-color: var(--border-color-default, #000000);
|
||||
font-size: .95em;
|
||||
border-bottom-width: 1px;
|
||||
border-left-style: none;
|
||||
border-right-color: #ACACAC;
|
||||
border-right-style: dotted;
|
||||
padding: .5em .8em .4em
|
||||
}
|
||||
|
||||
.grid {
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.grid .slick-header-column {
|
||||
border-top-style: none;
|
||||
color: var(--color-content, #101010);
|
||||
letter-spacing: .03em;
|
||||
border-color: var(--border-color-default, #000000);
|
||||
font-size: .95em;
|
||||
border-bottom-width: 1px;
|
||||
border-left-style: none;
|
||||
border-right-color: #ACACAC;
|
||||
border-right-style: dotted;
|
||||
padding: .5em .8em .4em;
|
||||
background-color: var( --color-bg-content-header, #F5F5F5);
|
||||
border-bottom-color: #ACACAC
|
||||
}
|
||||
|
||||
.grid .slick-cell {
|
||||
border-top-style: none;
|
||||
color: var(--color-content, #101010);
|
||||
letter-spacing: .03em;
|
||||
border-color: var(--border-color-default, #000000);
|
||||
font-size: .95em;
|
||||
border-bottom-width: 1px;
|
||||
border-left-style: none;
|
||||
border-right-color: #ACACAC;
|
||||
border-right-style: dotted;
|
||||
padding: .5em .8em .4em;
|
||||
/* background-color: var(--background-color, white); */
|
||||
}
|
||||
|
||||
.grid .slick-cell.selected {
|
||||
background-color: var(--color-cell-bg-grid-selected, rgb(173, 214, 255));
|
||||
color: var(--color-content, #101010);
|
||||
}
|
||||
|
||||
.grid .slick-cell.selected .grid-cell-value-container.missing-value {
|
||||
color: black
|
||||
}
|
||||
|
||||
.grid .slick-cell.editable {
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.grid .slick-cell.editable input {
|
||||
padding: .5em .8em .4em;
|
||||
height: 100%;
|
||||
padding: .4em .65em .1em;
|
||||
letter-spacing: .03em
|
||||
}
|
||||
|
||||
.grid .slick-cell.editable input:focus {
|
||||
outline-offset: 0
|
||||
}
|
||||
|
||||
.grid .slick-cell .grid-cell-value-container {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
.grid .slick-cell .grid-cell-value-container.override-cell {
|
||||
color: black
|
||||
}
|
||||
|
||||
.grid .slick-cell .grid-cell-value-container.highlighted {
|
||||
color: black
|
||||
}
|
||||
|
||||
.grid .slick-cell .grid-cell-value-container.blurred {
|
||||
color: #ABABAB
|
||||
}
|
||||
|
||||
.grid .slick-cell .grid-cell-value-container.context {
|
||||
color: darkblue;
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
.grid .slick-cell .grid-cell-value-container.loading-cell {
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
.grid .slick-cell .grid-cell-value-container.right-justified {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.grid input.editor-text {
|
||||
width: 100%;
|
||||
}
|
||||
104
src/sql/parts/grid/media/styles.css
Normal file
@@ -0,0 +1,104 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* slick grid */
|
||||
|
||||
.boxRow.content.horzBox.slickgrid {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.boxRow.content.horzBox.slickgrid:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.boxRow.content.padded {
|
||||
padding: 15px
|
||||
}
|
||||
|
||||
/* icon */
|
||||
.gridIcon {
|
||||
padding: 8px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0px;
|
||||
}
|
||||
|
||||
.gridIconContainer {
|
||||
margin: 5px 5px;
|
||||
}
|
||||
|
||||
/* messages */
|
||||
.resultsMessageTable {
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
}
|
||||
|
||||
.wideResultsMessage {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
#messageTable td {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
#messageTable tbody tr:last-child > td {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.resultsMessageValue {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* headers */
|
||||
.resultsMessageHeader {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
height: 22px;
|
||||
padding-left: 20px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.resultsMessageHeader > span {
|
||||
float: left;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.resizableHandle {
|
||||
font-size: 0.1px;
|
||||
display: block;
|
||||
float: right;
|
||||
position: absolute;
|
||||
cursor: row-resize;
|
||||
top: 0;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#resizeHandle {
|
||||
font-size: 0.1px;
|
||||
position: absolute;
|
||||
cursor: row-resize;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
background-color: var(--color-resize-handle);
|
||||
}
|
||||
|
||||
.header > span.shortCut {
|
||||
float: right;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.queryLink {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
1
src/sql/parts/grid/media/uncollapsedArrow.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16'><path fill='#646465' d='M11 10.07H5.344L11 4.414v5.656z'/></svg>
|
||||
|
After Width: | Height: | Size: 127 B |
1
src/sql/parts/grid/media/uncollapsedArrow_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16'><path fill='#e8e8e8' d='M11 10.07H5.344L11 4.414v5.656z'/></svg>
|
||||
|
After Width: | Height: | Size: 127 B |
1
src/sql/parts/grid/media/viewChart.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#c91f1f;}</style></defs><title>view_as_chart_16x16</title><rect x="0.5" y="14.22" width="14.99" height="1"/><rect x="13.52" y="0.2" width="1" height="13.03"/><path d="M2,12.34a.71.71,0,0,0,.44-.15v1h-1V12.1A.74.74,0,0,0,2,12.34Z"/><path d="M5.52,6.22v.29a2.58,2.58,0,0,1-.14-.29Z"/><polygon points="4.52 10.16 5.52 9.16 5.52 13.22 4.52 13.22 4.52 10.16"/><path d="M7.54,8.74a3.68,3.68,0,0,0,1,.18v4.3h-1Z"/><path d="M10.93,8.36a3.07,3.07,0,0,0,.59-.43v5.31h-1V8.57A3.18,3.18,0,0,0,10.93,8.36Z"/><path class="cls-1" d="M5.38,6.22a2.58,2.58,0,0,0,.14.29V6.22Z"/><path class="cls-1" d="M5.38,6.22a2.58,2.58,0,0,0,.14.29V6.22Z"/><path d="M12.42,3.76a3.71,3.71,0,0,0-.37-.89A3.85,3.85,0,0,0,10.7,1.52a3.35,3.35,0,0,0-.89-.37,3.78,3.78,0,0,0-2.88.37,4.3,4.3,0,0,0-.76.59,3.92,3.92,0,0,0-.58.76,3.73,3.73,0,0,0-.38.89,3.63,3.63,0,0,0-.13,1A3.61,3.61,0,0,0,5.3,6a1.19,1.19,0,0,0,.08.2h.14v.29a4.32,4.32,0,0,0,.42.63,4,4,0,0,1-.3.3L5.1,8l-.58.56-.11.11-.76.75-.73.74-.47.48-.16.18q-.29.29-.45.48a.67.67,0,0,0-.16.24.41.41,0,0,0,.09.26.31.31,0,0,0,.25.11.33.33,0,0,0,.24-.1l.2-.2,2.06-2,1-1,.91-.91a3.87,3.87,0,0,0,1.11.64,3.37,3.37,0,0,0,1,.21h.28a3.64,3.64,0,0,0,1-.13,3.41,3.41,0,0,0,.71-.29L10.7,8a3.61,3.61,0,0,0,.76-.59l.06-.07a3.46,3.46,0,0,0,.53-.68,4.08,4.08,0,0,0,.37-.89,3.61,3.61,0,0,0,0-2Zm-.79,2.18a1.63,1.63,0,0,1-.11.23,3.13,3.13,0,0,1-.54.74,3.32,3.32,0,0,1-.46.38,3.15,3.15,0,0,1-.51.28,3,3,0,0,1-1.19.24H8.54a3.12,3.12,0,0,1-.91-.23l-.09,0a3,3,0,0,1-.88-.62,3,3,0,0,1-.9-2.16A3,3,0,0,1,6,3.56a3.09,3.09,0,0,1,.66-1,3,3,0,0,1,1-.65,2.82,2.82,0,0,1,1.19-.25A2.86,2.86,0,0,1,10,1.94a4.12,4.12,0,0,1,.51.27,3.31,3.31,0,0,1,.46.38,3.6,3.6,0,0,1,.54.74,1.63,1.63,0,0,1,.11.23,2.82,2.82,0,0,1,.25,1.19A2.82,2.82,0,0,1,11.63,5.94Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
src/sql/parts/grid/media/viewChart_inverse.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>view_as_chart_inverse_16x16</title><rect class="cls-1" x="0.5" y="14.43" width="14.99" height="1"/><rect class="cls-1" x="13.52" y="0.41" width="1" height="13.03"/><path class="cls-1" d="M2,12.55a.71.71,0,0,0,.44-.15v1h-1V12.31A.74.74,0,0,0,2,12.55Z"/><path class="cls-1" d="M5.52,6.43v.29a2.58,2.58,0,0,1-.14-.29Z"/><polygon class="cls-1" points="4.52 10.37 5.52 9.37 5.52 13.43 4.52 13.43 4.52 10.37"/><path class="cls-1" d="M7.54,8.95a3.68,3.68,0,0,0,1,.18v4.3h-1Z"/><path class="cls-1" d="M10.93,8.57a3.07,3.07,0,0,0,.59-.43v5.31h-1V8.78A3.18,3.18,0,0,0,10.93,8.57Z"/><path class="cls-1" d="M5.38,6.43a2.58,2.58,0,0,0,.14.29V6.43Z"/><path class="cls-1" d="M5.38,6.43a2.58,2.58,0,0,0,.14.29V6.43Z"/><path class="cls-1" d="M12.42,4a3.71,3.71,0,0,0-.37-.89A3.85,3.85,0,0,0,10.7,1.73a3.35,3.35,0,0,0-.89-.37,3.78,3.78,0,0,0-2.88.37,4.3,4.3,0,0,0-.76.59,3.92,3.92,0,0,0-.58.76A3.73,3.73,0,0,0,5.21,4a3.63,3.63,0,0,0-.13,1A3.61,3.61,0,0,0,5.3,6.23a1.19,1.19,0,0,0,.08.2h.14v.29a4.32,4.32,0,0,0,.42.63,4,4,0,0,1-.3.3l-.54.54-.58.56-.11.11-.76.75-.73.74-.47.48L2.29,11q-.29.29-.45.48a.67.67,0,0,0-.16.24.41.41,0,0,0,.09.26A.31.31,0,0,0,2,12.1a.33.33,0,0,0,.24-.1l.2-.2,2.06-2,1-1,.91-.91a3.87,3.87,0,0,0,1.11.64,3.37,3.37,0,0,0,1,.21h.28a3.64,3.64,0,0,0,1-.13,3.41,3.41,0,0,0,.71-.29l.18-.09a3.61,3.61,0,0,0,.76-.59l.06-.07a3.46,3.46,0,0,0,.53-.68A4.08,4.08,0,0,0,12.42,6a3.61,3.61,0,0,0,0-2Zm-.79,2.18a1.63,1.63,0,0,1-.11.23,3.13,3.13,0,0,1-.54.74,3.32,3.32,0,0,1-.46.38,3.15,3.15,0,0,1-.51.28A3,3,0,0,1,8.82,8H8.54a3.12,3.12,0,0,1-.91-.23l-.09,0a3,3,0,0,1-.88-.62A3,3,0,0,1,5.76,5,3,3,0,0,1,6,3.77a3.09,3.09,0,0,1,.66-1,3,3,0,0,1,1-.65A2.82,2.82,0,0,1,8.82,1.9,2.86,2.86,0,0,1,10,2.15a4.12,4.12,0,0,1,.51.27A3.31,3.31,0,0,1,11,2.8a3.6,3.6,0,0,1,.54.74,1.63,1.63,0,0,1,.11.23A2.82,2.82,0,0,1,11.88,5,2.82,2.82,0,0,1,11.63,6.15Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
211
src/sql/parts/grid/services/dataService.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { Observer } from 'rxjs/Observer';
|
||||
|
||||
import { ResultSetSubset, EditUpdateCellResult, EditSubsetResult, EditCreateRowResult } from 'data';
|
||||
import { IQueryModelService } from 'sql/parts/query/execution/queryModel';
|
||||
import { ResultSerializer } from 'sql/parts/query/common/resultSerializer';
|
||||
import { ISaveRequest } from 'sql/parts/grid/common/interfaces';
|
||||
|
||||
import { ISlickRange } from 'angular2-slickgrid';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IQueryEditorService } from 'sql/parts/query/common/queryEditorService';
|
||||
|
||||
/**
|
||||
* DataService handles the interactions between QueryModel and app.component. Thus, it handles
|
||||
* query running and grid interaction communication for a single URI.
|
||||
*/
|
||||
export class DataService {
|
||||
|
||||
public queryEventObserver: Subject<any>;
|
||||
public gridContentObserver: Subject<any>;
|
||||
private editQueue: Promise<any>;
|
||||
|
||||
constructor(
|
||||
private _uri: string,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IQueryModelService private _queryModel: IQueryModelService,
|
||||
@IQueryEditorService private _queryEditorService: IQueryEditorService
|
||||
) {
|
||||
this.queryEventObserver = new Subject();
|
||||
this.gridContentObserver = new Subject();
|
||||
this.editQueue = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specified number of rows starting at a specified row for
|
||||
* the current results set. Used for query results only.
|
||||
* @param start The starting row or the requested rows
|
||||
* @param numberOfRows The amount of rows to return
|
||||
* @param batchId The batch id of the batch you are querying
|
||||
* @param resultId The id of the result you want to get the rows for
|
||||
*/
|
||||
getQueryRows(rowStart: number, numberOfRows: number, batchId: number, resultId: number): Observable<ResultSetSubset> {
|
||||
const self = this;
|
||||
return Observable.create(function (observer: Observer<ResultSetSubset>) {
|
||||
self._queryModel.getQueryRows(self._uri, rowStart, numberOfRows, batchId, resultId).then(results => {
|
||||
observer.next(results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specified number of rows starting at a specified row. Should only
|
||||
* be used for edit sessions.
|
||||
* @param rowStart The row to start retrieving from (inclusive)
|
||||
* @param numberOfRows The maximum number of rows to return
|
||||
*/
|
||||
getEditRows(rowStart: number, numberOfRows: number): Observable<EditSubsetResult> {
|
||||
const self = this;
|
||||
return Observable.create(function (observer: Observer<EditSubsetResult>) {
|
||||
self._queryModel.getEditRows(self._uri, rowStart, numberOfRows).then(results => {
|
||||
observer.next(results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateCell(rowId: number, columnId: number, newValue: string): Thenable<EditUpdateCellResult> {
|
||||
const self = this;
|
||||
self.editQueue = self.editQueue.then(() => {
|
||||
return self._queryModel.updateCell(self._uri, rowId, columnId, newValue).then(result => {
|
||||
return result;
|
||||
}, error => {
|
||||
// Start our editQueue over due to the rejected promise
|
||||
self.editQueue = Promise.resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
return self.editQueue;
|
||||
}
|
||||
|
||||
commitEdit(): Thenable<void> {
|
||||
const self = this;
|
||||
self.editQueue = self.editQueue.then(() => {
|
||||
return self._queryModel.commitEdit(self._uri).then(result => {
|
||||
return result;
|
||||
}, error => {
|
||||
// Start our editQueue over due to the rejected promise
|
||||
self.editQueue = Promise.resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
return self.editQueue;
|
||||
}
|
||||
|
||||
createRow(): Thenable<EditCreateRowResult> {
|
||||
const self = this;
|
||||
self.editQueue = self.editQueue.then(() => {
|
||||
return self._queryModel.createRow(self._uri).then(result => {
|
||||
return result;
|
||||
}, error => {
|
||||
// Start our editQueue over due to the rejected promise
|
||||
self.editQueue = Promise.resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
return self.editQueue;
|
||||
}
|
||||
|
||||
deleteRow(rowId: number): Thenable<void> {
|
||||
const self = this;
|
||||
self.editQueue = self.editQueue.then(() => {
|
||||
return self._queryModel.deleteRow(self._uri, rowId).then(result => {
|
||||
return result;
|
||||
}, error => {
|
||||
// Start our editQueue over due to the rejected promise
|
||||
self.editQueue = Promise.resolve();
|
||||
self._queryModel.showCommitError(error.message);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
return self.editQueue;
|
||||
}
|
||||
|
||||
revertCell(rowId: number, columnId: number): Thenable<void> {
|
||||
const self = this;
|
||||
self.editQueue = self.editQueue.then(() => {
|
||||
return self._queryModel.revertCell(self._uri, rowId, columnId).then(result => {
|
||||
return result;
|
||||
}, error => {
|
||||
// Start our editQueue over due to the rejected promise
|
||||
self.editQueue = Promise.resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
return self.editQueue;
|
||||
}
|
||||
|
||||
revertRow(rowId: number): Thenable<void> {
|
||||
const self = this;
|
||||
self.editQueue = self.editQueue.then(() => {
|
||||
return self._queryModel.revertRow(self._uri, rowId).then(result => {
|
||||
return result;
|
||||
}, error => {
|
||||
// Start our editQueue over due to the rejected promise
|
||||
self.editQueue = Promise.resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
return self.editQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* send request to save the selected result set as csv
|
||||
* @param uri of the calling document
|
||||
* @param batchId The batch id of the batch with the result to save
|
||||
* @param resultId The id of the result to save as csv
|
||||
*/
|
||||
sendSaveRequest(saveRequest: ISaveRequest): void {
|
||||
let serializer = this._instantiationService.createInstance(ResultSerializer);
|
||||
serializer.saveResults(this._uri, saveRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* send request to open content in new editor
|
||||
* @param content The content to be opened
|
||||
* @param columnName The column name of the content
|
||||
*/
|
||||
openLink(content: string, columnName: string, linkType: string): void {
|
||||
let serializer = this._instantiationService.createInstance(ResultSerializer);
|
||||
serializer.openLink(content, columnName, linkType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a copy request
|
||||
* @param selection The selection range to copy
|
||||
* @param batchId The batch id of the result to copy from
|
||||
* @param resultId The result id of the result to copy from
|
||||
* @param includeHeaders [Optional]: Should column headers be included in the copy selection
|
||||
*/
|
||||
copyResults(selection: ISlickRange[], batchId: number, resultId: number, includeHeaders?: boolean): void {
|
||||
this._queryModel.copyResults(this._uri, selection, batchId, resultId, includeHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to set the selection in the QueryEditor.
|
||||
*/
|
||||
setEditorSelection(index: number) {
|
||||
this._queryModel.setEditorSelection(this._uri, index);
|
||||
}
|
||||
|
||||
showWarning(message: string): void {
|
||||
}
|
||||
|
||||
showError(message: string): void {
|
||||
}
|
||||
|
||||
get config(): Promise<{ [key: string]: any }> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onAngularLoaded(): void {
|
||||
this._queryModel.onAngularLoaded(this._uri);
|
||||
}
|
||||
}
|
||||
57
src/sql/parts/grid/services/sharedServices.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as Utils from 'sql/parts/connection/common/utils';
|
||||
|
||||
export class DBCellValue {
|
||||
displayValue: string;
|
||||
isNull: boolean;
|
||||
|
||||
public static isDBCellValue(object: any): boolean {
|
||||
return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format xml field into a hyperlink and performs HTML entity encoding
|
||||
*/
|
||||
export function hyperLinkFormatter(row: number, cell: any, value: any, columnDef: any, dataContext: any): string {
|
||||
let cellClasses = 'grid-cell-value-container';
|
||||
let valueToDisplay: string = '';
|
||||
|
||||
if (DBCellValue.isDBCellValue(value)) {
|
||||
valueToDisplay = 'NULL';
|
||||
if (!value.isNull) {
|
||||
cellClasses += ' xmlLink';
|
||||
valueToDisplay = Utils.htmlEntities(value.displayValue);
|
||||
return `<a class="${cellClasses}" href="#" >${valueToDisplay}</a>`;
|
||||
} else {
|
||||
cellClasses += ' missing-value';
|
||||
}
|
||||
}
|
||||
return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format all text to replace all new lines with spaces and performs HTML entity encoding
|
||||
*/
|
||||
export function textFormatter(row: number, cell: any, value: any, columnDef: any, dataContext: any): string {
|
||||
let cellClasses = 'grid-cell-value-container';
|
||||
let valueToDisplay: string = '';
|
||||
|
||||
if (DBCellValue.isDBCellValue(value)) {
|
||||
valueToDisplay = 'NULL';
|
||||
if (!value.isNull) {
|
||||
valueToDisplay = Utils.htmlEntities(value.displayValue.replace(/(\r\n|\n|\r)/g, ' '));
|
||||
} else {
|
||||
cellClasses += ' missing-value';
|
||||
}
|
||||
} else if (typeof value === 'string'){
|
||||
valueToDisplay = value;
|
||||
|
||||
}
|
||||
|
||||
return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`;
|
||||
}
|
||||
34
src/sql/parts/grid/views/editData/editData.component.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
|
||||
<div class="fullsize vertBox editdata-component">
|
||||
<div id="results" *ngIf="renderedDataSets.length > 0" class="results vertBox scrollable"
|
||||
(onScroll)="onScroll($event)" [class.hidden]="!resultActive">
|
||||
<div class="boxRow content horzBox slickgrid editable" *ngFor="let dataSet of renderedDataSets; let i = index"
|
||||
[style.max-height]="dataSet.maxHeight" [style.min-height]="dataSet.minHeight">
|
||||
<slick-grid #slickgrid id="slickgrid_{{i}}" [columnDefinitions]="dataSet.columnDefinitions"
|
||||
[ngClass]="i === activeGrid ? 'active' : ''"
|
||||
[dataRows]="dataSet.dataRows"
|
||||
enableAsyncPostRender="true"
|
||||
showDataTypeIcon="false"
|
||||
showHeader="true"
|
||||
[resized]="dataSet.resized"
|
||||
[plugins]="slickgridPlugins"
|
||||
(cellEditBegin)="onCellEditBegin($event)"
|
||||
(cellEditExit)="onCellEditEnd($event)"
|
||||
(rowEditBegin)="onRowEditBegin($event)"
|
||||
(rowEditExit)="onRowEditEnd($event)"
|
||||
(contextMenu)="openContextMenu($event, dataSet.batchId, dataSet.resultId, i)"
|
||||
[isColumnEditable]="onIsColumnEditable"
|
||||
[isCellEditValid]="onIsCellEditValid"
|
||||
[overrideCellFn]="overrideCellFn"
|
||||
enableEditing="true"
|
||||
class="boxCol content vertBox slickgrid">
|
||||
</slick-grid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
496
src/sql/parts/grid/views/editData/editData.component.ts
Normal file
@@ -0,0 +1,496 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!sql/parts/grid/media/slickColorTheme';
|
||||
import 'vs/css!sql/parts/grid/media/flexbox';
|
||||
import 'vs/css!sql/parts/grid/media/styles';
|
||||
import 'vs/css!sql/parts/grid/media/slick.grid';
|
||||
import 'vs/css!sql/parts/grid/media/slickGrid';
|
||||
import 'vs/css!./media/editData';
|
||||
|
||||
import { ElementRef, ChangeDetectorRef, OnInit, OnDestroy, Component, Inject, forwardRef, EventEmitter } from '@angular/core';
|
||||
import { IGridDataRow, VirtualizedCollection } from 'angular2-slickgrid';
|
||||
import { IGridDataSet } from 'sql/parts/grid/common/interfaces';
|
||||
import * as Services from 'sql/parts/grid/services/sharedServices';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { EditDataComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import { GridParentComponent } from 'sql/parts/grid/views/gridParentComponent';
|
||||
import { EditDataGridActionProvider } from 'sql/parts/grid/views/editData/editDataGridActions';
|
||||
import { error } from 'sql/base/common/log';
|
||||
|
||||
import { clone } from 'vs/base/common/objects';
|
||||
|
||||
export const EDITDATA_SELECTOR: string = 'editdata-component';
|
||||
|
||||
@Component({
|
||||
selector: EDITDATA_SELECTOR,
|
||||
host: { '(window:keydown)': 'keyEvent($event)', '(window:gridnav)': 'keyEvent($event)' },
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/grid/views/editData/editData.component.html'))
|
||||
})
|
||||
|
||||
export class EditDataComponent extends GridParentComponent implements OnInit, OnDestroy {
|
||||
// CONSTANTS
|
||||
private scrollTimeOutTime = 200;
|
||||
private windowSize = 50;
|
||||
|
||||
// FIELDS
|
||||
// All datasets
|
||||
private dataSet: IGridDataSet;
|
||||
private scrollTimeOut: number;
|
||||
private messagesAdded = false;
|
||||
private scrollEnabled = true;
|
||||
private firstRender = true;
|
||||
private totalElapsedTimeSpan: number;
|
||||
private complete = false;
|
||||
private idMapping: { [row: number]: number } = {};
|
||||
|
||||
private currentCell: { row: number, column: number } = null;
|
||||
private rowEditInProgress: boolean = false;
|
||||
private removingNewRow: boolean = false;
|
||||
|
||||
// Edit Data functions
|
||||
public onCellEditEnd: (event: { row: number, column: number, newValue: any }) => void;
|
||||
public onCellEditBegin: (event: { row: number, column: number }) => void;
|
||||
public onRowEditBegin: (event: { row: number }) => void;
|
||||
public onRowEditEnd: (event: { row: number }) => void;
|
||||
public onIsCellEditValid: (row: number, column: number, newValue: any) => boolean;
|
||||
public onIsColumnEditable: (column: number) => boolean;
|
||||
public overrideCellFn: (rowNumber, columnId, value?, data?) => string;
|
||||
public loadDataFunction: (offset: number, count: number) => Promise<IGridDataRow[]>;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) bootstrapService: IBootstrapService
|
||||
) {
|
||||
super(el, cd, bootstrapService);
|
||||
this._el.nativeElement.className = 'slickgridContainer';
|
||||
let editDataParameters: EditDataComponentParams = this._bootstrapService.getBootstrapParams(this._el.nativeElement.tagName);
|
||||
this.dataService = editDataParameters.dataService;
|
||||
this.actionProvider = new EditDataGridActionProvider(this.dataService, this.onGridSelectAll(), this.onDeleteRow(), this.onRevertRow());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by Angular when the object is initialized
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const self = this;
|
||||
this.baseInit();
|
||||
|
||||
// Add the subscription to the list of things to be disposed on destroy, or else on a new component init
|
||||
// may get the "destroyed" object still getting called back.
|
||||
this.subscribeWithDispose(this.dataService.queryEventObserver, (event) => {
|
||||
switch (event.type) {
|
||||
case 'start':
|
||||
self.handleStart(self, event);
|
||||
break;
|
||||
case 'complete':
|
||||
self.handleComplete(self, event);
|
||||
break;
|
||||
case 'message':
|
||||
self.handleMessage(self, event);
|
||||
break;
|
||||
case 'resultSet':
|
||||
self.handleResultSet(self, event);
|
||||
break;
|
||||
case 'editSessionReady':
|
||||
self.handleEditSessionReady(self, event);
|
||||
break;
|
||||
default:
|
||||
error('Unexpected query event type "' + event.type + '" sent');
|
||||
break;
|
||||
}
|
||||
self._cd.detectChanges();
|
||||
});
|
||||
|
||||
this.dataService.onAngularLoaded();
|
||||
}
|
||||
|
||||
protected initShortcuts(shortcuts: { [name: string]: Function }): void {
|
||||
// TODO add any Edit Data-specific shortcuts here
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.baseDestroy();
|
||||
}
|
||||
|
||||
handleStart(self: EditDataComponent, event: any): void {
|
||||
self.dataSet = undefined;
|
||||
self.placeHolderDataSets = [];
|
||||
self.renderedDataSets = self.placeHolderDataSets;
|
||||
self.totalElapsedTimeSpan = undefined;
|
||||
self.complete = false;
|
||||
self.messagesAdded = false;
|
||||
|
||||
// Hooking up edit functions
|
||||
this.onIsCellEditValid = (row, column, value): boolean => {
|
||||
// TODO can only run sync code
|
||||
return true;
|
||||
};
|
||||
|
||||
this.onCellEditEnd = (event: { row: number, column: number, newValue: any }): void => {
|
||||
self.rowEditInProgress = true;
|
||||
|
||||
// Update the cell accordingly
|
||||
self.dataService.updateCell(this.idMapping[event.row], event.column, event.newValue)
|
||||
.then(
|
||||
result => {
|
||||
self.setCellDirtyState(event.row, event.column + 1, result.cell.isDirty);
|
||||
self.setRowDirtyState(event.row, result.isRowDirty);
|
||||
},
|
||||
error => {
|
||||
// On error updating cell, jump back to the cell that was being edited
|
||||
self.focusCell(event.row, event.column + 1);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.onCellEditBegin = (event: { row: number, column: number }): void => { };
|
||||
|
||||
this.onRowEditBegin = (event: { row: number }): void => { };
|
||||
|
||||
this.onRowEditEnd = (event: { row: number }): void => { };
|
||||
|
||||
this.onIsColumnEditable = (column: number): boolean => {
|
||||
let result = false;
|
||||
// Check that our variables exist
|
||||
if (column !== undefined && !!this.dataSet && !!this.dataSet.columnDefinitions[column]) {
|
||||
result = this.dataSet.columnDefinitions[column].isEditable;
|
||||
}
|
||||
|
||||
// If no column definition exists then the row is not editable
|
||||
return result;
|
||||
};
|
||||
|
||||
this.overrideCellFn = (rowNumber, columnId, value?, data?): string => {
|
||||
let returnVal = '';
|
||||
if (Services.DBCellValue.isDBCellValue(value)) {
|
||||
returnVal = value.displayValue;
|
||||
} else if (typeof value === 'string') {
|
||||
returnVal = value;
|
||||
}
|
||||
return returnVal;
|
||||
};
|
||||
|
||||
// Setup a function for generating a promise to lookup result subsets
|
||||
this.loadDataFunction = (offset: number, count: number): Promise<IGridDataRow[]> => {
|
||||
return new Promise<IGridDataRow[]>((resolve, reject) => {
|
||||
self.dataService.getEditRows(offset, count).subscribe(result => {
|
||||
let rowIndex = offset;
|
||||
let gridData: IGridDataRow[] = result.subset.map(row => {
|
||||
self.idMapping[rowIndex] = row.id;
|
||||
rowIndex++;
|
||||
return { values: row.cells, row: row.id };
|
||||
});
|
||||
|
||||
// Append a NULL row to the end of gridData
|
||||
let newLastRow = gridData.length === 0 ? 0 : (gridData[gridData.length - 1].row + 1);
|
||||
gridData.push({ values: self.dataSet.columnDefinitions.map(cell => { return { displayValue: 'NULL', isNull: false }; }), row: newLastRow });
|
||||
resolve(gridData);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
onDeleteRow(): (index: number) => void {
|
||||
const self = this;
|
||||
return (index: number): void => {
|
||||
self.dataService.deleteRow(index).then(() => {
|
||||
self.dataService.commitEdit().then(() => {
|
||||
self.removeRow(index, 0);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
onRevertRow(): (index: number) => void {
|
||||
const self = this;
|
||||
return (index: number): void => {
|
||||
// Force focus to the first cell (completing any active edit operation)
|
||||
self.focusCell(index, 0, false);
|
||||
|
||||
// Perform a revert row operation
|
||||
self.dataService.revertRow(index)
|
||||
.then(() => { self.dataService.commitEdit(); })
|
||||
.then(() => { self.refreshResultsets(); });
|
||||
};
|
||||
}
|
||||
|
||||
onCellSelect(row: number, column: number): void {
|
||||
let self = this;
|
||||
|
||||
// TODO: We can skip this step if we're allowing multiple commits
|
||||
if (this.rowEditInProgress) {
|
||||
// We're in the middle of a row edit, so we need to commit if we move to a different row
|
||||
if (row !== this.currentCell.row) {
|
||||
this.dataService.commitEdit()
|
||||
.then(
|
||||
result => {
|
||||
// Committing was successful. Clean the grid, turn off the row edit flag, then select again
|
||||
self.setGridClean();
|
||||
self.rowEditInProgress = false;
|
||||
self.onCellSelect(row, column);
|
||||
}, error => {
|
||||
// Committing failed, so jump back to the last selected cell
|
||||
self.focusCell(self.currentCell.row, self.currentCell.column);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// We're not in the middle of a row edit, so we can move anywhere
|
||||
// Checking for removing new row makes sure we don't re-add the new row after we've
|
||||
// jumped to the first cell of the "new row"
|
||||
if (this.isNullRow(row) && !this.removingNewRow) {
|
||||
// We moved into the "new row", add another new row
|
||||
this.addRow(row, column);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the cell we moved to as the current cell
|
||||
this.currentCell = { row: row, column: column };
|
||||
}
|
||||
|
||||
handleComplete(self: EditDataComponent, event: any): void {
|
||||
self.totalElapsedTimeSpan = event.data;
|
||||
self.complete = true;
|
||||
self.messagesAdded = true;
|
||||
}
|
||||
|
||||
handleEditSessionReady(self, event): void {
|
||||
// TODO: update when edit session is ready
|
||||
}
|
||||
|
||||
handleMessage(self: EditDataComponent, event: any): void {
|
||||
// TODO: what do we do with messages?
|
||||
}
|
||||
|
||||
handleResultSet(self: EditDataComponent, event: any): void {
|
||||
// Clone the data before altering it to avoid impacting other subscribers
|
||||
let resultSet = Object.assign({}, event.data);
|
||||
|
||||
// Add an extra 'new row'
|
||||
resultSet.rowCount++;
|
||||
// Precalculate the max height and min height
|
||||
let maxHeight = this.getMaxHeight(resultSet.rowCount);
|
||||
let minHeight = this.getMinHeight(resultSet.rowCount);
|
||||
|
||||
// Store the result set from the event
|
||||
let dataSet: IGridDataSet = {
|
||||
resized: undefined,
|
||||
batchId: resultSet.batchId,
|
||||
resultId: resultSet.id,
|
||||
totalRows: resultSet.rowCount,
|
||||
maxHeight: maxHeight,
|
||||
minHeight: minHeight,
|
||||
dataRows: new VirtualizedCollection(
|
||||
self.windowSize,
|
||||
resultSet.rowCount,
|
||||
this.loadDataFunction,
|
||||
index => { return { values: [] }; }
|
||||
),
|
||||
columnDefinitions: resultSet.columnInfo.map((c, i) => {
|
||||
let isLinked = c.isXml || c.isJson;
|
||||
let linkType = c.isXml ? 'xml' : 'json';
|
||||
return {
|
||||
id: i.toString(),
|
||||
name: c.columnName === 'Microsoft SQL Server 2005 XML Showplan'
|
||||
? 'XML Showplan'
|
||||
: c.columnName,
|
||||
type: self.stringToFieldType('string'),
|
||||
formatter: isLinked ? Services.hyperLinkFormatter : Services.textFormatter,
|
||||
asyncPostRender: isLinked ? self.linkHandler(linkType) : undefined,
|
||||
isEditable: c.isUpdatable
|
||||
};
|
||||
})
|
||||
};
|
||||
self.dataSet = dataSet;
|
||||
|
||||
// Create a dataSet to render without rows to reduce DOM size
|
||||
let undefinedDataSet = clone(dataSet);
|
||||
undefinedDataSet.columnDefinitions = dataSet.columnDefinitions;
|
||||
undefinedDataSet.dataRows = undefined;
|
||||
undefinedDataSet.resized = new EventEmitter();
|
||||
self.placeHolderDataSets.push(undefinedDataSet);
|
||||
self.messagesAdded = true;
|
||||
self.onScroll(0);
|
||||
|
||||
// Reset selected cell state
|
||||
this.currentCell = null;
|
||||
this.rowEditInProgress = false;
|
||||
this.removingNewRow = false;
|
||||
|
||||
// HACK: unsafe reference to the slickgrid object
|
||||
// TODO: Reimplement by adding selectCell event to angular2-slickgrid
|
||||
self._cd.detectChanges();
|
||||
let slick: any = self.slickgrids.toArray()[0];
|
||||
let grid: Slick.Grid<any> = slick._grid;
|
||||
|
||||
grid.onActiveCellChanged.subscribe((event: Slick.EventData, data: Slick.OnActiveCellChangedEventArgs<any>) => {
|
||||
self.onCellSelect(data.row, data.cell);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles rendering the results to the DOM that are currently being shown
|
||||
* and destroying any results that have moved out of view
|
||||
* @param scrollTop The scrolltop value, if not called by the scroll event should be 0
|
||||
*/
|
||||
onScroll(scrollTop): void {
|
||||
const self = this;
|
||||
clearTimeout(self.scrollTimeOut);
|
||||
this.scrollTimeOut = setTimeout(() => {
|
||||
self.scrollEnabled = false;
|
||||
for (let i = 0; i < self.placeHolderDataSets.length; i++) {
|
||||
self.placeHolderDataSets[i].dataRows = self.dataSet.dataRows;
|
||||
self.placeHolderDataSets[i].resized.emit();
|
||||
}
|
||||
|
||||
|
||||
self._cd.detectChanges();
|
||||
|
||||
if (self.firstRender) {
|
||||
let setActive = function () {
|
||||
if (self.firstRender && self.slickgrids.toArray().length > 0) {
|
||||
self.slickgrids.toArray()[0].setActive();
|
||||
self.firstRender = false;
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setActive();
|
||||
});
|
||||
}
|
||||
}, self.scrollTimeOutTime);
|
||||
}
|
||||
|
||||
protected tryHandleKeyEvent(e): boolean {
|
||||
let handled: boolean = false;
|
||||
// If the esc key was pressed while in a create session
|
||||
let currentNewRowIndex = this.dataSet.totalRows - 2;
|
||||
|
||||
if (e.keyCode === jQuery.ui.keyCode.ESCAPE && this.currentCell.row === currentNewRowIndex) {
|
||||
// revert our last new row
|
||||
this.removingNewRow = true;
|
||||
|
||||
this.dataService.revertRow(this.idMapping[currentNewRowIndex])
|
||||
.then(() => {
|
||||
this.removeRow(currentNewRowIndex, 0);
|
||||
this.rowEditInProgress = false;
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
// Private Helper Functions ////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Checks if input row is our NULL new row
|
||||
private isNullRow(row: number): boolean {
|
||||
// Null row is always at index (totalRows - 1)
|
||||
return (row === this.dataSet.totalRows - 1);
|
||||
}
|
||||
|
||||
// Adds CSS classes to slickgrid cells to indicate a dirty state
|
||||
private setCellDirtyState(row: number, column: number, dirtyState: boolean): void {
|
||||
let slick: any = this.slickgrids.toArray()[0];
|
||||
let grid = slick._grid;
|
||||
if (dirtyState) {
|
||||
// Change cell color
|
||||
$(grid.getCellNode(row, column)).addClass('dirtyCell').removeClass('selected');
|
||||
} else {
|
||||
$(grid.getCellNode(row, column)).removeClass('dirtyCell');
|
||||
}
|
||||
}
|
||||
|
||||
// Adds CSS classes to slickgrid rows to indicate a dirty state
|
||||
private setRowDirtyState(row: number, dirtyState: boolean): void {
|
||||
let slick: any = this.slickgrids.toArray()[0];
|
||||
let grid = slick._grid;
|
||||
if (dirtyState) {
|
||||
// Change row header color
|
||||
$(grid.getCellNode(row, 0)).addClass('dirtyRowHeader');
|
||||
} else {
|
||||
$(grid.getCellNode(row, 0)).removeClass('dirtyRowHeader');
|
||||
}
|
||||
}
|
||||
|
||||
// Sets CSS to clean the entire grid of dirty state cells and rows
|
||||
private setGridClean(): void {
|
||||
// Remove dirty classes from the entire table
|
||||
let allRows = $($('.grid-canvas').children());
|
||||
let allCells = $(allRows.children());
|
||||
allCells.removeClass('dirtyCell').removeClass('dirtyRowHeader');
|
||||
}
|
||||
|
||||
// Adds an extra row to the end of slickgrid (just for rendering purposes)
|
||||
// Then sets the focused call afterwards
|
||||
private addRow(row: number, column: number): void {
|
||||
// Add a new row to the edit session in the tools service
|
||||
this.dataService.createRow();
|
||||
|
||||
// Adding an extra row for 'new row' functionality
|
||||
this.rowEditInProgress = true;
|
||||
this.dataSet.totalRows++;
|
||||
this.dataSet.maxHeight = this.getMaxHeight(this.dataSet.totalRows);
|
||||
this.dataSet.minHeight = this.getMinHeight(this.dataSet.totalRows);
|
||||
this.dataSet.dataRows = new VirtualizedCollection(
|
||||
this.windowSize,
|
||||
this.dataSet.totalRows,
|
||||
this.loadDataFunction,
|
||||
index => { return { values: [] }; }
|
||||
);
|
||||
|
||||
// Refresh grid
|
||||
this.onScroll(0);
|
||||
|
||||
// Mark the row as dirty once the scroll has completed
|
||||
setTimeout(() => {
|
||||
this.setRowDirtyState(row, true);
|
||||
}, this.scrollTimeOutTime);
|
||||
}
|
||||
|
||||
// removes a row from the end of slickgrid (just for rendering purposes)
|
||||
// Then sets the focused call afterwards
|
||||
private removeRow(row: number, column: number): void {
|
||||
// Removing the new row
|
||||
this.dataSet.totalRows--;
|
||||
this.dataSet.dataRows = new VirtualizedCollection(
|
||||
this.windowSize,
|
||||
this.dataSet.totalRows,
|
||||
this.loadDataFunction,
|
||||
index => { return { values: [] }; }
|
||||
);
|
||||
|
||||
// refresh results view
|
||||
this.onScroll(0);
|
||||
|
||||
// Set focus to the row index column of the removed row
|
||||
setTimeout(() => {
|
||||
this.focusCell(row, 0);
|
||||
this.removingNewRow = false;
|
||||
}, this.scrollTimeOutTime);
|
||||
}
|
||||
|
||||
private focusCell(row: number, column: number, forceEdit: boolean=true): void {
|
||||
let slick: any = this.slickgrids.toArray()[0];
|
||||
let grid = slick._grid;
|
||||
grid.gotoCell(row, column, forceEdit);
|
||||
}
|
||||
|
||||
private getMaxHeight(rowCount: number): any {
|
||||
return rowCount < this._defaultNumShowingRows
|
||||
? ((rowCount + 1) * this._rowHeight) + 10
|
||||
: 'inherit';
|
||||
}
|
||||
|
||||
private getMinHeight(rowCount: number): any {
|
||||
return rowCount > this._defaultNumShowingRows
|
||||
? (this._defaultNumShowingRows + 1) * this._rowHeight + 10
|
||||
: this.getMaxHeight(rowCount);
|
||||
}
|
||||
}
|
||||
45
src/sql/parts/grid/views/editData/editData.module.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { ApplicationRef, ComponentFactoryResolver, NgModule, Inject, forwardRef } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
|
||||
import { EditDataComponent, EDITDATA_SELECTOR } from 'sql/parts/grid/views/editData/editData.component';
|
||||
import { SlickGrid } from 'angular2-slickgrid';
|
||||
|
||||
@NgModule({
|
||||
|
||||
imports: [
|
||||
CommonModule,
|
||||
BrowserModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
EditDataComponent,
|
||||
SlickGrid
|
||||
],
|
||||
|
||||
entryComponents: [
|
||||
EditDataComponent
|
||||
]
|
||||
})
|
||||
export class EditDataModule {
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService
|
||||
) {
|
||||
}
|
||||
|
||||
ngDoBootstrap(appRef: ApplicationRef) {
|
||||
const factory = this._resolver.resolveComponentFactory(EditDataComponent);
|
||||
const uniqueSelector: string = this._bootstrapService.getUniqueSelector(EDITDATA_SELECTOR);
|
||||
(<any>factory).factory.selector = uniqueSelector;
|
||||
appRef.bootstrap(factory);
|
||||
}
|
||||
}
|
||||
68
src/sql/parts/grid/views/editData/editDataGridActions.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IGridInfo } from 'sql/parts/grid/common/interfaces';
|
||||
import { DataService } from 'sql/parts/grid/services/dataService';
|
||||
import { GridActionProvider } from 'sql/parts/grid/views/gridActions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IAction, Action } from 'vs/base/common/actions';
|
||||
|
||||
export class EditDataGridActionProvider extends GridActionProvider {
|
||||
|
||||
constructor(dataService: DataService, selectAllCallback: (index: number) => void,
|
||||
private _deleteRowCallback: (index: number) => void,
|
||||
private _revertRowCallback: (index: number) => void) {
|
||||
super(dataService, selectAllCallback);
|
||||
}
|
||||
/**
|
||||
* Return actions given a click on an edit data grid
|
||||
*/
|
||||
public getGridActions(): TPromise<IAction[]> {
|
||||
let actions: IAction[] = [];
|
||||
actions.push(new DeleteRowAction(DeleteRowAction.ID, DeleteRowAction.LABEL, this._deleteRowCallback));
|
||||
actions.push(new RevertRowAction(RevertRowAction.ID, RevertRowAction.LABEL, this._revertRowCallback));
|
||||
|
||||
return TPromise.as(actions);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteRowAction extends Action {
|
||||
public static ID = 'grid.deleteRow';
|
||||
public static LABEL = localize('deleteRow', 'Delete Row');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private callback: (index: number) => void
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(gridInfo: IGridInfo): TPromise<boolean> {
|
||||
this.callback(gridInfo.rowIndex);
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class RevertRowAction extends Action {
|
||||
public static ID = 'grid.revertRow';
|
||||
public static LABEL = localize('revertRow', 'Revert Row');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private callback: (index: number) => void
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(gridInfo: IGridInfo): TPromise<boolean> {
|
||||
this.callback(gridInfo.rowIndex);
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
8
src/sql/parts/grid/views/editData/media/editData.css
Normal file
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.editdata-component * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
163
src/sql/parts/grid/views/gridActions.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IGridInfo, IRange, SaveFormat } from 'sql/parts/grid/common/interfaces';
|
||||
import { DataService } from 'sql/parts/grid/services/dataService';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IAction, Action } from 'vs/base/common/actions';
|
||||
|
||||
export const GRID_SAVECSV_ID = 'grid.saveAsCsv';
|
||||
export const GRID_SAVEJSON_ID = 'grid.saveAsJson';
|
||||
export const GRID_SAVEEXCEL_ID = 'grid.saveAsExcel';
|
||||
export const GRID_COPY_ID = 'grid.copySelection';
|
||||
export const GRID_COPYWITHHEADERS_ID = 'grid.copyWithHeaders';
|
||||
export const GRID_SELECTALL_ID = 'grid.selectAll';
|
||||
export const MESSAGES_SELECTALL_ID = 'grid.messages.selectAll';
|
||||
export const MESSAGES_COPY_ID = 'grid.messages.copy';
|
||||
export const TOGGLERESULTS_ID = 'grid.toggleResultPane';
|
||||
export const TOGGLEMESSAGES_ID = 'grid.toggleMessagePane';
|
||||
|
||||
export class GridActionProvider {
|
||||
|
||||
constructor(protected _dataService: DataService, protected _selectAllCallback: (index: number) => void) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return actions given a click on a grid
|
||||
*/
|
||||
public getGridActions(): TPromise<IAction[]> {
|
||||
let actions: IAction[] = [];
|
||||
actions.push(new SaveResultAction(SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveFormat.CSV, this._dataService));
|
||||
actions.push(new SaveResultAction(SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveFormat.JSON, this._dataService));
|
||||
actions.push(new SaveResultAction(SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveFormat.EXCEL, this._dataService));
|
||||
actions.push(new SelectAllGridAction(SelectAllGridAction.ID, SelectAllGridAction.LABEL, this._selectAllCallback));
|
||||
actions.push(new CopyResultAction(CopyResultAction.COPY_ID, CopyResultAction.COPY_LABEL, false, this._dataService));
|
||||
actions.push(new CopyResultAction(CopyResultAction.COPYWITHHEADERS_ID, CopyResultAction.COPYWITHHEADERS_LABEL, true, this._dataService));
|
||||
|
||||
return TPromise.as(actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return actions given a click on a messages pane
|
||||
*/
|
||||
public getMessagesActions(dataService: DataService, selectAllCallback: () => void): TPromise<IAction[]> {
|
||||
let actions: IAction[] = [];
|
||||
actions.push(new CopyMessagesAction(CopyMessagesAction.ID, CopyMessagesAction.LABEL));
|
||||
actions.push(new SelectAllMessagesAction(SelectAllMessagesAction.ID, SelectAllMessagesAction.LABEL, selectAllCallback));
|
||||
return TPromise.as(actions);
|
||||
}
|
||||
}
|
||||
|
||||
export class SaveResultAction extends Action {
|
||||
public static SAVECSV_ID = GRID_SAVECSV_ID;
|
||||
public static SAVECSV_LABEL = localize('saveAsCsv', 'Save As CSV');
|
||||
|
||||
public static SAVEJSON_ID = GRID_SAVEJSON_ID;
|
||||
public static SAVEJSON_LABEL = localize('saveAsJson', 'Save As JSON');
|
||||
|
||||
public static SAVEEXCEL_ID = GRID_SAVEEXCEL_ID;
|
||||
public static SAVEEXCEL_LABEL = localize('saveAsExcel', 'Save As Excel');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private format: SaveFormat,
|
||||
private dataService: DataService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(gridInfo: IGridInfo): TPromise<boolean> {
|
||||
this.dataService.sendSaveRequest({
|
||||
batchIndex: gridInfo.batchIndex,
|
||||
resultSetNumber: gridInfo.resultSetNumber,
|
||||
selection: gridInfo.selection,
|
||||
format: this.format
|
||||
});
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyResultAction extends Action {
|
||||
public static COPY_ID = GRID_COPY_ID;
|
||||
public static COPY_LABEL = localize('copySelection', 'Copy');
|
||||
|
||||
public static COPYWITHHEADERS_ID = GRID_COPYWITHHEADERS_ID;
|
||||
public static COPYWITHHEADERS_LABEL = localize('copyWithHeaders', 'Copy With Headers');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private copyHeader: boolean,
|
||||
private dataService: DataService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(gridInfo: IGridInfo): TPromise<boolean> {
|
||||
this.dataService.copyResults(gridInfo.selection, gridInfo.batchIndex, gridInfo.resultSetNumber, this.copyHeader);
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectAllGridAction extends Action {
|
||||
public static ID = GRID_SELECTALL_ID;
|
||||
public static LABEL = localize('selectAll', 'Select All');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private selectAllCallback: (index: number) => void
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(gridInfo: IGridInfo): TPromise<boolean> {
|
||||
this.selectAllCallback(gridInfo.gridIndex);
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectAllMessagesAction extends Action {
|
||||
public static ID = MESSAGES_SELECTALL_ID;
|
||||
public static LABEL = localize('selectAll', 'Select All');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
private selectAllCallback: () => void
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(): TPromise<boolean> {
|
||||
this.selectAllCallback();
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyMessagesAction extends Action {
|
||||
public static ID = MESSAGES_COPY_ID;
|
||||
public static LABEL = localize('copyMessages', 'Copy');
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(selectedRange: IRange): TPromise<boolean> {
|
||||
let selectedText = selectedRange.text();
|
||||
WorkbenchUtils.executeCopy(selectedText);
|
||||
return TPromise.as(true);
|
||||
}
|
||||
}
|
||||
76
src/sql/parts/grid/views/gridCommands.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as GridContentEvents from 'sql/parts/grid/common/gridContentEvents';
|
||||
import { IQueryModelService } from 'sql/parts/query/execution/queryModel';
|
||||
import { QueryEditor } from 'sql/parts/query/editor/queryEditor';
|
||||
import { EditDataEditor } from 'sql/parts/editData/editor/editDataEditor';
|
||||
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
function runActionOnActiveResultsEditor (accessor: ServicesAccessor, eventName: string): void {
|
||||
let editorService = accessor.get(IWorkbenchEditorService);
|
||||
const candidates = [editorService.getActiveEditor(), ...editorService.getVisibleEditors()].filter(e => {
|
||||
if (e) {
|
||||
let id = e.getId();
|
||||
if (id === QueryEditor.ID || id === EditDataEditor.ID) {
|
||||
// This is a query or edit data editor
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (candidates.length > 0) {
|
||||
let queryModelService: IQueryModelService = accessor.get(IQueryModelService);
|
||||
let uri = (<any> candidates[0].input).uri;
|
||||
queryModelService.sendGridContentEvent(uri, eventName);
|
||||
}
|
||||
}
|
||||
|
||||
export const copySelection = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.CopySelection);
|
||||
};
|
||||
|
||||
export const copyMessagesSelection = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.CopyMessagesSelection);
|
||||
};
|
||||
|
||||
export const copyWithHeaders = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.CopyWithHeaders);
|
||||
};
|
||||
|
||||
export const toggleMessagePane = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.ToggleMessagePane);
|
||||
};
|
||||
|
||||
export const toggleResultsPane = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.ToggleResultPane);
|
||||
};
|
||||
|
||||
export const saveAsCsv = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsCsv);
|
||||
};
|
||||
|
||||
export const saveAsJson = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsJSON);
|
||||
};
|
||||
|
||||
export const saveAsExcel = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsExcel);
|
||||
};
|
||||
|
||||
export const selectAll = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.SelectAll);
|
||||
};
|
||||
|
||||
export const selectAllMessages = (accessor: ServicesAccessor) => {
|
||||
runActionOnActiveResultsEditor(accessor, GridContentEvents.SelectAllMessages);
|
||||
};
|
||||
|
||||
|
||||
554
src/sql/parts/grid/views/gridParentComponent.ts
Normal file
@@ -0,0 +1,554 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!sql/parts/grid/media/slickColorTheme';
|
||||
import 'vs/css!sql/parts/grid/media/flexbox';
|
||||
import 'vs/css!sql/parts/grid/media/styles';
|
||||
import 'vs/css!sql/parts/grid/media/slick.grid';
|
||||
import 'vs/css!sql/parts/grid/media/slickGrid';
|
||||
|
||||
import { Subscription, Subject } from 'rxjs/Rx';
|
||||
import { ElementRef, QueryList, ChangeDetectorRef, ViewChildren } from '@angular/core';
|
||||
import { IGridDataRow, ISlickRange, SlickGrid, FieldType } from 'angular2-slickgrid';
|
||||
import { toDisposableSubscription } from 'sql/parts/common/rxjsUtils';
|
||||
import * as Constants from 'sql/parts/query/common/constants';
|
||||
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
|
||||
import { IGridInfo, IRange, IGridDataSet, SaveFormat } from 'sql/parts/grid/common/interfaces';
|
||||
import * as Utils from 'sql/parts/connection/common/utils';
|
||||
import { DataService } from 'sql/parts/grid/services/dataService';
|
||||
import * as actions from 'sql/parts/grid/views/gridActions';
|
||||
import * as Services from 'sql/parts/grid/services/sharedServices';
|
||||
import * as GridContentEvents from 'sql/parts/grid/common/gridContentEvents';
|
||||
import { ResultsVisibleContext, ResultsGridFocussedContext, ResultsMessagesFocussedContext } from 'sql/parts/query/common/queryContext';
|
||||
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
import * as rangy from 'sql/base/node/rangy';
|
||||
import { error } from 'sql/base/common/log';
|
||||
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { AutoColumnSize } from 'sql/base/browser/ui/table/plugins/autoSizeColumns.plugin';
|
||||
import { DragCellSelectionModel } from 'sql/base/browser/ui/table/plugins/dragCellSelectionModel.plugin';
|
||||
|
||||
export abstract class GridParentComponent {
|
||||
// CONSTANTS
|
||||
// tslint:disable:no-unused-variable
|
||||
protected get selectionModel(): DragCellSelectionModel<any> {
|
||||
return new DragCellSelectionModel<any>();
|
||||
}
|
||||
protected get slickgridPlugins(): Array<any> {
|
||||
return [
|
||||
new AutoColumnSize<any>({})
|
||||
];
|
||||
}
|
||||
protected _rowHeight = 29;
|
||||
protected _defaultNumShowingRows = 8;
|
||||
protected Constants = Constants;
|
||||
protected LocalizedConstants = LocalizedConstants;
|
||||
protected Utils = Utils;
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
protected startString = new Date().toLocaleTimeString();
|
||||
|
||||
protected shortcutfunc: { [name: string]: Function };
|
||||
|
||||
// tslint:enable
|
||||
|
||||
// FIELDS
|
||||
// Service for interaction with the IQueryModel
|
||||
protected dataService: DataService;
|
||||
protected keybindingService: IKeybindingService;
|
||||
protected scopedContextKeyService: IContextKeyService;
|
||||
protected contextMenuService: IContextMenuService;
|
||||
protected actionProvider: actions.GridActionProvider;
|
||||
|
||||
protected toDispose: IDisposable[];
|
||||
|
||||
|
||||
// Context keys to set when keybindings are available
|
||||
private resultsVisibleContextKey: IContextKey<boolean>;
|
||||
private gridFocussedContextKey: IContextKey<boolean>;
|
||||
private messagesFocussedContextKey: IContextKey<boolean>;
|
||||
|
||||
// All datasets
|
||||
// Place holder data sets to buffer between data sets and rendered data sets
|
||||
protected placeHolderDataSets: IGridDataSet[] = [];
|
||||
// Datasets currently being rendered on the DOM
|
||||
protected renderedDataSets: IGridDataSet[] = this.placeHolderDataSets;
|
||||
protected resultActive = true;
|
||||
protected _messageActive = true;
|
||||
protected activeGrid = 0;
|
||||
|
||||
@ViewChildren('slickgrid') slickgrids: QueryList<SlickGrid>;
|
||||
|
||||
// Edit Data functions
|
||||
public onCellEditEnd: (event: { row: number, column: number, newValue: any }) => void;
|
||||
public onCellEditBegin: (event: { row: number, column: number }) => void;
|
||||
public onRowEditBegin: (event: { row: number }) => void;
|
||||
public onRowEditEnd: (event: { row: number }) => void;
|
||||
public onIsCellEditValid: (row: number, column: number, newValue: any) => boolean;
|
||||
public onIsColumnEditable: (column: number) => boolean;
|
||||
public overrideCellFn: (rowNumber, columnId, value?, data?) => string;
|
||||
public loadDataFunction: (offset: number, count: number) => Promise<IGridDataRow[]>;
|
||||
|
||||
set messageActive(input: boolean) {
|
||||
this._messageActive = input;
|
||||
if (this.resultActive) {
|
||||
this.resizeGrids();
|
||||
}
|
||||
}
|
||||
|
||||
get messageActive(): boolean {
|
||||
return this._messageActive;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected _el: ElementRef,
|
||||
protected _cd: ChangeDetectorRef,
|
||||
protected _bootstrapService: IBootstrapService
|
||||
) {
|
||||
this.toDispose = [];
|
||||
}
|
||||
|
||||
protected baseInit(): void {
|
||||
const self = this;
|
||||
this.initShortcutsBase();
|
||||
if (this._bootstrapService.configurationService) {
|
||||
let sqlConfig = this._bootstrapService.configurationService.getConfiguration('sql');
|
||||
if (sqlConfig) {
|
||||
this._messageActive = sqlConfig['messagesDefaultOpen'];
|
||||
}
|
||||
}
|
||||
this.subscribeWithDispose(this.dataService.gridContentObserver, (type) => {
|
||||
switch (type) {
|
||||
case GridContentEvents.RefreshContents:
|
||||
self.refreshResultsets();
|
||||
break;
|
||||
case GridContentEvents.ResizeContents:
|
||||
self.resizeGrids();
|
||||
break;
|
||||
case GridContentEvents.CopySelection:
|
||||
self.copySelection();
|
||||
break;
|
||||
case GridContentEvents.CopyWithHeaders:
|
||||
self.copyWithHeaders();
|
||||
break;
|
||||
case GridContentEvents.CopyMessagesSelection:
|
||||
self.copyMessagesSelection();
|
||||
break;
|
||||
case GridContentEvents.ToggleResultPane:
|
||||
self.toggleResultPane();
|
||||
break;
|
||||
case GridContentEvents.ToggleMessagePane:
|
||||
self.toggleMessagePane();
|
||||
break;
|
||||
case GridContentEvents.SelectAll:
|
||||
self.onSelectAllForActiveGrid();
|
||||
break;
|
||||
case GridContentEvents.SelectAllMessages:
|
||||
self.selectAllMessages();
|
||||
break;
|
||||
case GridContentEvents.SaveAsCsv:
|
||||
self.sendSaveRequest(SaveFormat.CSV);
|
||||
break;
|
||||
case GridContentEvents.SaveAsJSON:
|
||||
self.sendSaveRequest(SaveFormat.JSON);
|
||||
break;
|
||||
case GridContentEvents.SaveAsExcel:
|
||||
self.sendSaveRequest(SaveFormat.EXCEL);
|
||||
break;
|
||||
default:
|
||||
error('Unexpected grid content event type "' + type + '" sent');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.contextMenuService = this._bootstrapService.contextMenuService;
|
||||
this.keybindingService = this._bootstrapService.keybindingService;
|
||||
|
||||
this.bindKeys(this._bootstrapService.contextKeyService);
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the subscription to the list of things to be disposed on destroy, or else on a new component init
|
||||
* may get the "destroyed" object still getting called back.
|
||||
*/
|
||||
protected subscribeWithDispose<T>(subject: Subject<T>, event: (value: any) => void): void {
|
||||
let sub: Subscription = subject.subscribe(event);
|
||||
this.toDispose.push(toDisposableSubscription(sub));
|
||||
}
|
||||
|
||||
private bindKeys(contextKeyService: IContextKeyService): void {
|
||||
if (contextKeyService) {
|
||||
let gridContextKeyService = this._bootstrapService.contextKeyService.createScoped(this._el.nativeElement);
|
||||
this.toDispose.push(gridContextKeyService);
|
||||
this.resultsVisibleContextKey = ResultsVisibleContext.bindTo(gridContextKeyService);
|
||||
this.resultsVisibleContextKey.set(true);
|
||||
|
||||
this.gridFocussedContextKey = ResultsGridFocussedContext.bindTo(gridContextKeyService);
|
||||
this.messagesFocussedContextKey = ResultsMessagesFocussedContext.bindTo(gridContextKeyService);
|
||||
}
|
||||
}
|
||||
|
||||
protected baseDestroy(): void {
|
||||
this.toDispose = dispose(this.toDispose);
|
||||
}
|
||||
|
||||
private toggleResultPane(): void {
|
||||
this.resultActive = !this.resultActive;
|
||||
}
|
||||
|
||||
private toggleMessagePane(): void {
|
||||
this.messageActive = !this.messageActive;
|
||||
}
|
||||
|
||||
protected onGridFocus() {
|
||||
this.gridFocussedContextKey.set(true);
|
||||
}
|
||||
|
||||
protected onGridFocusout() {
|
||||
this.gridFocussedContextKey.set(false);
|
||||
}
|
||||
|
||||
protected onMessagesFocus() {
|
||||
this.messagesFocussedContextKey.set(true);
|
||||
}
|
||||
|
||||
protected onMessagesFocusout() {
|
||||
this.messagesFocussedContextKey.set(false);
|
||||
}
|
||||
|
||||
private copySelection(): void {
|
||||
let messageText = this.getMessageText();
|
||||
if (messageText.length > 0) {
|
||||
WorkbenchUtils.executeCopy(messageText);
|
||||
} else {
|
||||
let activeGrid = this.activeGrid;
|
||||
let selection = this.slickgrids.toArray()[activeGrid].getSelectedRanges();
|
||||
this.dataService.copyResults(selection, this.renderedDataSets[activeGrid].batchId, this.renderedDataSets[activeGrid].resultId);
|
||||
}
|
||||
}
|
||||
|
||||
private copyWithHeaders(): void {
|
||||
let activeGrid = this.activeGrid;
|
||||
let selection = this.slickgrids.toArray()[activeGrid].getSelectedRanges();
|
||||
this.dataService.copyResults(selection, this.renderedDataSets[activeGrid].batchId,
|
||||
this.renderedDataSets[activeGrid].resultId, true);
|
||||
}
|
||||
|
||||
private copyMessagesSelection(): void {
|
||||
let messageText = this.getMessageText();
|
||||
if (messageText.length === 0) {
|
||||
// Since we know we're specifically copying messages, do a select all if nothing is selected
|
||||
this.selectAllMessages();
|
||||
messageText = this.getMessageText();
|
||||
}
|
||||
if (messageText.length > 0) {
|
||||
WorkbenchUtils.executeCopy(messageText);
|
||||
}
|
||||
}
|
||||
|
||||
private getMessageText(): string {
|
||||
let range: IRange = this.getSelectedRangeUnderMessages();
|
||||
return range ? range.text() : '';
|
||||
}
|
||||
|
||||
private initShortcutsBase(): void {
|
||||
let shortcuts = {
|
||||
'ToggleResultPane': () => {
|
||||
this.toggleResultPane();
|
||||
},
|
||||
'ToggleMessagePane': () => {
|
||||
this.toggleMessagePane();
|
||||
},
|
||||
'CopySelection': () => {
|
||||
this.copySelection();
|
||||
},
|
||||
'CopyWithHeaders': () => {
|
||||
this.copyWithHeaders();
|
||||
},
|
||||
'SelectAll': () => {
|
||||
this.onSelectAllForActiveGrid();
|
||||
},
|
||||
'SaveAsCSV': () => {
|
||||
this.sendSaveRequest(SaveFormat.CSV);
|
||||
},
|
||||
'SaveAsJSON': () => {
|
||||
this.sendSaveRequest(SaveFormat.JSON);
|
||||
},
|
||||
'SaveAsExcel': () => {
|
||||
this.sendSaveRequest(SaveFormat.EXCEL);
|
||||
}
|
||||
};
|
||||
|
||||
this.initShortcuts(shortcuts);
|
||||
this.shortcutfunc = shortcuts;
|
||||
}
|
||||
|
||||
protected abstract initShortcuts(shortcuts: { [name: string]: Function }): void;
|
||||
|
||||
/**
|
||||
* Send save result set request to service
|
||||
*/
|
||||
handleContextClick(event: { type: string, batchId: number, resultId: number, index: number, selection: ISlickRange[] }): void {
|
||||
switch (event.type) {
|
||||
case 'savecsv':
|
||||
this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.CSV, selection: event.selection });
|
||||
break;
|
||||
case 'savejson':
|
||||
this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.JSON, selection: event.selection });
|
||||
break;
|
||||
case 'saveexcel':
|
||||
this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.EXCEL, selection: event.selection });
|
||||
break;
|
||||
case 'selectall':
|
||||
this.activeGrid = event.index;
|
||||
this.onSelectAllForActiveGrid();
|
||||
break;
|
||||
case 'copySelection':
|
||||
this.dataService.copyResults(event.selection, event.batchId, event.resultId);
|
||||
break;
|
||||
case 'copyWithHeaders':
|
||||
this.dataService.copyResults(event.selection, event.batchId, event.resultId, true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private sendSaveRequest(format: SaveFormat) {
|
||||
let activeGrid = this.activeGrid;
|
||||
let batchId = this.renderedDataSets[activeGrid].batchId;
|
||||
let resultId = this.renderedDataSets[activeGrid].resultId;
|
||||
let selection = this.slickgrids.toArray()[activeGrid].getSelectedRanges();
|
||||
this.dataService.sendSaveRequest({ batchIndex: batchId, resultSetNumber: resultId, format: format, selection: selection });
|
||||
}
|
||||
|
||||
protected _keybindingFor(action: IAction): ResolvedKeybinding {
|
||||
var [kb] = this.keybindingService.lookupKeybindings(action.id);
|
||||
return kb;
|
||||
}
|
||||
|
||||
openContextMenu(event, batchId, resultId, index): void {
|
||||
let selection = this.slickgrids.toArray()[index].getSelectedRanges();
|
||||
|
||||
let slick: any = this.slickgrids.toArray()[index];
|
||||
let grid = slick._grid;
|
||||
let rowIndex = grid.getCellFromEvent(event).row;
|
||||
|
||||
let actionContext: IGridInfo = {
|
||||
batchIndex: batchId,
|
||||
resultSetNumber: resultId,
|
||||
selection: selection,
|
||||
gridIndex: index,
|
||||
rowIndex: rowIndex
|
||||
};
|
||||
|
||||
let anchor = { x: event.pageX + 1, y: event.pageY };
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => this.actionProvider.getGridActions(),
|
||||
getKeyBinding: (action) => this._keybindingFor(action),
|
||||
onHide: (wasCancelled?: boolean) => {
|
||||
},
|
||||
getActionsContext: () => (actionContext)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that selects all elements of a grid. This needs to
|
||||
* return a function in order to capture the scope for this component
|
||||
* @private
|
||||
* @returns {(gridIndex: number) => void}
|
||||
*
|
||||
* @memberOf QueryComponent
|
||||
*/
|
||||
protected onGridSelectAll(): (gridIndex: number) => void {
|
||||
let self = this;
|
||||
return (gridIndex: number) => {
|
||||
self.activeGrid = gridIndex;
|
||||
self.slickgrids.toArray()[this.activeGrid].selection = true;
|
||||
};
|
||||
}
|
||||
|
||||
private onSelectAllForActiveGrid(): void {
|
||||
if (this.activeGrid >= 0 && this.slickgrids.length > this.activeGrid) {
|
||||
this.slickgrids.toArray()[this.activeGrid].selection = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to convert the string to a enum compatible with SlickGrid
|
||||
*/
|
||||
protected stringToFieldType(input: string): FieldType {
|
||||
let fieldtype: FieldType;
|
||||
switch (input) {
|
||||
case 'string':
|
||||
fieldtype = FieldType.String;
|
||||
break;
|
||||
default:
|
||||
fieldtype = FieldType.String;
|
||||
break;
|
||||
}
|
||||
return fieldtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a resultset take up the full result height if this is not already true
|
||||
* Otherwise rerenders the result sets from default
|
||||
*/
|
||||
magnify(index: number): void {
|
||||
const self = this;
|
||||
if (this.renderedDataSets.length > 1) {
|
||||
this.renderedDataSets = [this.placeHolderDataSets[index]];
|
||||
} else {
|
||||
this.renderedDataSets = this.placeHolderDataSets;
|
||||
this.onScroll(0);
|
||||
}
|
||||
setTimeout(() => {
|
||||
self.resizeGrids();
|
||||
self.slickgrids.toArray()[0].setActive();
|
||||
self._cd.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
abstract onScroll(scrollTop): void;
|
||||
|
||||
protected getResultsElement(): any {
|
||||
return this._el.nativeElement.querySelector('#results');
|
||||
}
|
||||
protected getMessagesElement(): any {
|
||||
return this._el.nativeElement.querySelector('#messages');
|
||||
}
|
||||
/**
|
||||
* Force angular to re-render the results grids. Calling this upon unhide (upon focus) fixes UI
|
||||
* glitches that occur when a QueryRestulsEditor is hidden then unhidden while it is running a query.
|
||||
*/
|
||||
refreshResultsets(): void {
|
||||
let tempRenderedDataSets = this.renderedDataSets;
|
||||
this.renderedDataSets = [];
|
||||
this._cd.detectChanges();
|
||||
this.renderedDataSets = tempRenderedDataSets;
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
getSelectedRangeUnderMessages(): IRange {
|
||||
let selectedRange: IRange = undefined;
|
||||
let msgEl = this.getMessagesElement();
|
||||
if (msgEl) {
|
||||
selectedRange = this.getSelectedRangeWithin(msgEl);
|
||||
}
|
||||
return selectedRange;
|
||||
}
|
||||
|
||||
getSelectedRangeWithin(el): IRange {
|
||||
let selectedRange = undefined;
|
||||
let sel = rangy.getSelection();
|
||||
let elRange = <IRange>rangy.createRange();
|
||||
elRange.selectNodeContents(el);
|
||||
if (sel.rangeCount) {
|
||||
selectedRange = sel.getRangeAt(0).intersection(elRange);
|
||||
}
|
||||
elRange.detach();
|
||||
return selectedRange;
|
||||
}
|
||||
|
||||
selectAllMessages(): void {
|
||||
let msgEl = this._el.nativeElement.querySelector('#messages');
|
||||
this.selectElementContents(msgEl);
|
||||
}
|
||||
|
||||
selectElementContents(el): void {
|
||||
let range = rangy.createRange();
|
||||
range.selectNodeContents(el);
|
||||
let sel = rangy.getSelection();
|
||||
sel.setSingleRange(range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add handler for clicking on xml link
|
||||
*/
|
||||
xmlLinkHandler = (cellRef: string, row: number, dataContext: JSON, colDef: any) => {
|
||||
const self = this;
|
||||
self.handleLink(cellRef, row, dataContext, colDef, 'xml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add handler for clicking on json link
|
||||
*/
|
||||
jsonLinkHandler = (cellRef: string, row: number, dataContext: JSON, colDef: any) => {
|
||||
const self = this;
|
||||
self.handleLink(cellRef, row, dataContext, colDef, 'json');
|
||||
}
|
||||
|
||||
private handleLink(cellRef: string, row: number, dataContext: JSON, colDef: any, linkType: string): void {
|
||||
const self = this;
|
||||
let value = self.getCellValueString(dataContext, colDef);
|
||||
$(cellRef).children('.xmlLink').click(function (): void {
|
||||
self.dataService.openLink(value, colDef.name, linkType);
|
||||
});
|
||||
}
|
||||
|
||||
private getCellValueString(dataContext: JSON, colDef: any): string {
|
||||
let returnVal = '';
|
||||
let value = dataContext[colDef.field];
|
||||
if (Services.DBCellValue.isDBCellValue(value)) {
|
||||
returnVal = value.displayValue;
|
||||
} else if (typeof value === 'string') {
|
||||
returnVal = value;
|
||||
}
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return asyncPostRender handler based on type
|
||||
*/
|
||||
public linkHandler(type: string): Function {
|
||||
if (type === 'xml') {
|
||||
return this.xmlLinkHandler;
|
||||
} else { // default to JSON handler
|
||||
return this.jsonLinkHandler;
|
||||
}
|
||||
}
|
||||
|
||||
keyEvent(e: KeyboardEvent): void {
|
||||
let self = this;
|
||||
let handled = self.tryHandleKeyEvent(e);
|
||||
if (handled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
// Else assume that keybinding service handles routing this to a command
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by keyEvent method to give child classes a chance to
|
||||
* handle key events.
|
||||
*
|
||||
* @protected
|
||||
* @abstract
|
||||
* @param {any} e
|
||||
* @returns {boolean}
|
||||
*
|
||||
* @memberOf GridParentComponent
|
||||
*/
|
||||
protected abstract tryHandleKeyEvent(e): boolean;
|
||||
|
||||
resizeGrids(): void {
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
for (let grid of self.renderedDataSets) {
|
||||
grid.resized.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Private Helper Functions ////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
57
src/sql/parts/grid/views/query/chartViewer.component.html
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
<div #taskbarContainer></div>
|
||||
<div style="display: flex; flex-flow: row; overflow: scroll; height: 100%; width: 100%">
|
||||
<div style="flex:3 3 auto; margin: 5px">
|
||||
<div style="position: relative; width: calc(100% - 20px); height: calc(100% - 20px)">
|
||||
<ng-template component-host></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="angular-modal-body-content chart-viewer" style="flex:1 1 auto; border-left: 1px solid; margin: 5px;">
|
||||
<div style="position: relative; width: 100%">
|
||||
<div class="dialog-label">{{chartTypeLabel}}</div>
|
||||
<div class="input-divider" #chartTypesContainer></div>
|
||||
<div [hidden]="chartTypesSelectBox.value === 'count' || chartTypesSelectBox.value === 'image'">
|
||||
<div [hidden]="!showDataType">
|
||||
<div class="dialog-label">{{dataTypeLabel}}</div>
|
||||
<div class="radio-indent">
|
||||
<div class="option">
|
||||
<input type="radio" name=data-type value="number" [(ngModel)]="dataType">{{numberLabel}}
|
||||
</div>
|
||||
<div class="option">
|
||||
<input type="radio" name=data-type value="point" [(ngModel)]="dataType">{{pointLabel}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="!showDataDirection">
|
||||
<div class="dialog-label">{{dataDirectionLabel}}</div>
|
||||
<div class="radio-indent">
|
||||
<div class="option">
|
||||
<input type="radio" name=data-direction value="vertical" [(ngModel)]="dataDirection">{{verticalLabel}}
|
||||
</div>
|
||||
<div class="option">
|
||||
<input type="radio" name=data-direction value="horizontal" [(ngModel)]="dataDirection">{{horizontalLabel}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [hidden]="!showLabelFirstColumn">
|
||||
<div class="input-divider" #labelFirstColumnContainer></div>
|
||||
</div>
|
||||
|
||||
<div [hidden]="!showColumnsAsLabels">
|
||||
<div class="input-divider" #columnsAsLabelsContainer></div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-label">{{legendLabel}}</div>
|
||||
<div class="input-divider" #legendContainer></div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="right-footer">
|
||||
<div class="footer-button" #createInsightButtonContainer></div>
|
||||
<div class="footer-button" #saveChartButtonContainer></div>
|
||||
<div class="footer-button" #copyChartButtonContainer></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
347
src/sql/parts/grid/views/query/chartViewer.component.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!sql/parts/grid/views/query/chartViewer';
|
||||
|
||||
import {
|
||||
Component, Inject, ViewContainerRef, forwardRef, OnInit,
|
||||
ComponentFactoryResolver, ViewChild, OnDestroy, Input, ElementRef, ChangeDetectorRef
|
||||
} from '@angular/core';
|
||||
import { NgGridItemConfig } from 'angular2-grid';
|
||||
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
|
||||
import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive';
|
||||
import { IGridDataSet } from 'sql/parts/grid/common/interfaces';
|
||||
import * as DialogHelper from 'sql/base/browser/ui/modal/dialogHelper';
|
||||
import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { IInsightData, IInsightsView, IInsightsConfig } from 'sql/parts/dashboard/widgets/insights/interfaces';
|
||||
import { Extensions, IInsightRegistry } from 'sql/platform/dashboard/common/insightRegistry';
|
||||
import { QueryEditor } from 'sql/parts/query/editor/queryEditor';
|
||||
import { DataType, ILineConfig } from 'sql/parts/dashboard/widgets/insights/views/charts/types/lineChart.component';
|
||||
import * as PathUtilities from 'sql/common/pathUtilities';
|
||||
import { IChartViewActionContext, CopyAction, CreateInsightAction, SaveImageAction } from 'sql/parts/grid/views/query/chartViewerActions';
|
||||
|
||||
/* Insights */
|
||||
import {
|
||||
ChartInsight, DataDirection, LegendPosition
|
||||
} from 'sql/parts/dashboard/widgets/insights/views/charts/chartInsight.component';
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Builder } from 'vs/base/browser/builder';
|
||||
import { attachSelectBoxStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import * as paths from 'vs/base/common/paths';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
|
||||
const insightRegistry = Registry.as<IInsightRegistry>(Extensions.InsightContribution);
|
||||
|
||||
@Component({
|
||||
selector: 'chart-viewer',
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/grid/views/query/chartViewer.component.html'))
|
||||
})
|
||||
export class ChartViewerComponent implements OnInit, OnDestroy, IChartViewActionContext {
|
||||
public legendOptions: string[];
|
||||
private chartTypesSelectBox: SelectBox;
|
||||
private legendSelectBox: SelectBox;
|
||||
private labelFirstColumnCheckBox: Checkbox;
|
||||
private columnsAsLabelsCheckBox: Checkbox;
|
||||
|
||||
/* UI */
|
||||
/* tslint:disable:no-unused-variable */
|
||||
private chartTypeLabel: string = nls.localize('chartTypeLabel', 'Chart Type');
|
||||
private dataDirectionLabel: string = nls.localize('dataDirectionLabel', 'Data Direction');
|
||||
private verticalLabel: string = nls.localize('verticalLabel', 'Vertical');
|
||||
private horizontalLabel: string = nls.localize('horizontalLabel', 'Horizontal');
|
||||
private dataTypeLabel: string = nls.localize('dataTypeLabel', 'Data Type');
|
||||
private numberLabel: string = nls.localize('numberLabel', 'Number');
|
||||
private pointLabel: string = nls.localize('pointLabel', 'Point');
|
||||
private labelFirstColumnLabel: string = nls.localize('labelFirstColumnLabel', 'Use First Column as row label?');
|
||||
private columnsAsLabelsLabel: string = nls.localize('columnsAsLabelsLabel', 'Use Column names as labels?');
|
||||
private legendLabel: string = nls.localize('legendLabel', 'Legend Position');
|
||||
private chartNotFoundError: string = nls.localize('chartNotFound', 'Could not find chart to save');
|
||||
/* tslint:enable:no-unused-variable */
|
||||
|
||||
private _actionBar: Taskbar;
|
||||
private _createInsightAction: CreateInsightAction;
|
||||
private _copyAction: CopyAction;
|
||||
private _saveAction: SaveImageAction;
|
||||
private _chartConfig: ILineConfig;
|
||||
private _disposables: Array<IDisposable> = [];
|
||||
private _dataSet: IGridDataSet;
|
||||
private _executeResult: IInsightData;
|
||||
private _chartComponent: ChartInsight;
|
||||
|
||||
@ViewChild(ComponentHostDirective) private componentHost: ComponentHostDirective;
|
||||
@ViewChild('taskbarContainer', { read: ElementRef }) private taskbarContainer;
|
||||
@ViewChild('chartTypesContainer', { read: ElementRef }) private chartTypesElement;
|
||||
@ViewChild('legendContainer', { read: ElementRef }) private legendElement;
|
||||
@ViewChild('labelFirstColumnContainer', { read: ElementRef }) private labelFirstColumnElement;
|
||||
@ViewChild('columnsAsLabelsContainer', { read: ElementRef }) private columnsAsLabelsElement;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver,
|
||||
@Inject(forwardRef(() => ViewContainerRef)) private _viewContainerRef: ViewContainerRef,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._chartConfig = <ILineConfig>{
|
||||
dataDirection: 'vertical',
|
||||
dataType: 'number',
|
||||
legendPosition: 'none',
|
||||
labelFirstColumn: false
|
||||
};
|
||||
this.legendOptions = Object.values(LegendPosition);
|
||||
this.initializeUI();
|
||||
}
|
||||
|
||||
private initializeUI() {
|
||||
// Initialize the taskbar
|
||||
this._initActionBar();
|
||||
|
||||
// Init chart type dropdown
|
||||
this.chartTypesSelectBox = new SelectBox(insightRegistry.getAllIds(), 'horizontalBar');
|
||||
this.chartTypesSelectBox.render(this.chartTypesElement.nativeElement);
|
||||
this.chartTypesSelectBox.onDidSelect(selected => this.onChartChanged());
|
||||
this._disposables.push(attachSelectBoxStyler(this.chartTypesSelectBox, this._bootstrapService.themeService));
|
||||
|
||||
// Init label first column checkbox
|
||||
// Note: must use 'self' for callback
|
||||
this.labelFirstColumnCheckBox = DialogHelper.createCheckBox(new Builder(this.labelFirstColumnElement.nativeElement),
|
||||
this.labelFirstColumnLabel, 'chartView-checkbox', false, () => this.onLabelFirstColumnChanged());
|
||||
this._disposables.push(attachCheckboxStyler(this.labelFirstColumnCheckBox, this._bootstrapService.themeService));
|
||||
|
||||
// Init label first column checkbox
|
||||
// Note: must use 'self' for callback
|
||||
this.columnsAsLabelsCheckBox = DialogHelper.createCheckBox(new Builder(this.columnsAsLabelsElement.nativeElement),
|
||||
this.columnsAsLabelsLabel, 'chartView-checkbox', false, () => this.columnsAsLabelsChanged());
|
||||
this._disposables.push(attachCheckboxStyler(this.columnsAsLabelsCheckBox, this._bootstrapService.themeService));
|
||||
|
||||
// Init legend dropdown
|
||||
this.legendSelectBox = new SelectBox(this.legendOptions, this._chartConfig.legendPosition);
|
||||
this.legendSelectBox.render(this.legendElement.nativeElement);
|
||||
this.legendSelectBox.onDidSelect(selected => this.onLegendChanged());
|
||||
this._disposables.push(attachSelectBoxStyler(this.legendSelectBox, this._bootstrapService.themeService));
|
||||
}
|
||||
|
||||
private _initActionBar() {
|
||||
this._createInsightAction = this._bootstrapService.instantiationService.createInstance(CreateInsightAction);
|
||||
this._copyAction = this._bootstrapService.instantiationService.createInstance(CopyAction);
|
||||
this._saveAction = this._bootstrapService.instantiationService.createInstance(SaveImageAction);
|
||||
|
||||
let taskbar = <HTMLElement>this.taskbarContainer.nativeElement;
|
||||
this._actionBar = new Taskbar(taskbar, this._bootstrapService.contextMenuService);
|
||||
this._actionBar.context = this;
|
||||
this._actionBar.setContent([
|
||||
{ action: this._createInsightAction },
|
||||
{ action: this._copyAction },
|
||||
{ action: this._saveAction }
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public onChartChanged(): void {
|
||||
if (['scatter', 'timeSeries'].some(item => item === this.chartTypesSelectBox.value)) {
|
||||
this.dataType = DataType.Point;
|
||||
this.dataDirection = DataDirection.Horizontal;
|
||||
}
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
public onLabelFirstColumnChanged(): void {
|
||||
this._chartConfig.labelFirstColumn = this.labelFirstColumnCheckBox.checked;
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
public columnsAsLabelsChanged(): void {
|
||||
this._chartConfig.columnsAsLabels = this.columnsAsLabelsCheckBox.checked;
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
public onLegendChanged(): void {
|
||||
this._chartConfig.legendPosition = <LegendPosition>this.legendSelectBox.value;
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
public set dataType(type: DataType) {
|
||||
this._chartConfig.dataType = type;
|
||||
// Requires full chart refresh
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
public set dataDirection(direction: DataDirection) {
|
||||
this._chartConfig.dataDirection = direction;
|
||||
// Requires full chart refresh
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
public copyChart(): void {
|
||||
let data = this._chartComponent.getCanvasData();
|
||||
if (!data) {
|
||||
this.showError(this.chartNotFoundError);
|
||||
return;
|
||||
}
|
||||
|
||||
this._bootstrapService.clipboardService.writeImageDataUrl(data);
|
||||
}
|
||||
|
||||
public saveChart(): void {
|
||||
let filePath = this.promptForFilepath();
|
||||
let data = this._chartComponent.getCanvasData();
|
||||
if (!data) {
|
||||
this.showError(this.chartNotFoundError);
|
||||
return;
|
||||
}
|
||||
if (filePath) {
|
||||
let buffer = this.decodeBase64Image(data);
|
||||
pfs.writeFile(filePath, buffer).then(undefined, (err) => {
|
||||
if (err) {
|
||||
this.showError(err.message);
|
||||
} else {
|
||||
let fileUri = URI.from({ scheme: PathUtilities.FILE_SCHEMA, path: filePath });
|
||||
this._bootstrapService.windowsService.openExternal(fileUri.toString());
|
||||
this._bootstrapService.messageService.show(Severity.Info, nls.localize('chartSaved', 'Saved Chart to path: {0}', filePath));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private promptForFilepath(): string {
|
||||
let filepathPlaceHolder = PathUtilities.resolveCurrentDirectory(this.getActiveUriString(), PathUtilities.getRootPath(this._bootstrapService.workspaceContextService));
|
||||
filepathPlaceHolder = paths.join(filepathPlaceHolder, 'chart.png');
|
||||
|
||||
let filePath: string = this._bootstrapService.windowService.showSaveDialog({
|
||||
title: nls.localize('saveAsFileTitle', 'Choose Results File'),
|
||||
defaultPath: paths.normalize(filepathPlaceHolder, true)
|
||||
});
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private decodeBase64Image(data: string): Buffer {
|
||||
let matches = data.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
||||
return new Buffer(matches[2], 'base64');
|
||||
}
|
||||
|
||||
public createInsight(): void {
|
||||
let uriString: string = this.getActiveUriString();
|
||||
if (!uriString) {
|
||||
this.showError(nls.localize('createInsightNoEditor', 'Cannot create insight as the active editor is not a SQL Editor'));
|
||||
return;
|
||||
}
|
||||
|
||||
let uri: URI = URI.parse(uriString);
|
||||
let dataService = this._bootstrapService.queryModelService.getDataService(uriString);
|
||||
if (!dataService) {
|
||||
this.showError(nls.localize('createInsightNoDataService', 'Cannot create insight, backing data model not found'));
|
||||
return;
|
||||
}
|
||||
let queryFile: string = uri.fsPath;
|
||||
let query: string = undefined;
|
||||
let type = {};
|
||||
type[this.chartTypesSelectBox.value] = this._chartConfig;
|
||||
// create JSON
|
||||
let config: IInsightsConfig = {
|
||||
type,
|
||||
query,
|
||||
queryFile
|
||||
};
|
||||
|
||||
let widgetConfig = {
|
||||
name: nls.localize('myWidgetName', 'My-Widget'),
|
||||
gridItemConfig: this.getGridItemConfig(),
|
||||
widget: {
|
||||
'insights-widget': config
|
||||
}
|
||||
};
|
||||
|
||||
// open in new window as untitled JSON file
|
||||
dataService.openLink(JSON.stringify(widgetConfig), 'Insight', 'json');
|
||||
}
|
||||
|
||||
private showError(errorMsg: string) {
|
||||
this._bootstrapService.messageService.show(Severity.Error, errorMsg);
|
||||
}
|
||||
|
||||
private getGridItemConfig(): NgGridItemConfig {
|
||||
let config: NgGridItemConfig = {
|
||||
sizex: 2,
|
||||
sizey: 1
|
||||
};
|
||||
return config;
|
||||
}
|
||||
|
||||
private getActiveUriString(): string {
|
||||
let editorService = this._bootstrapService.editorService;
|
||||
let editor = editorService.getActiveEditor();
|
||||
if (editor && editor instanceof QueryEditor) {
|
||||
let queryEditor: QueryEditor = editor;
|
||||
return queryEditor.uri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private get showDataDirection(): boolean {
|
||||
return ['pie', 'horizontalBar', 'bar', 'doughnut'].some(item => item === this.chartTypesSelectBox.value) || (this.chartTypesSelectBox.value === 'line' && this.dataType === 'number');
|
||||
}
|
||||
|
||||
private get showLabelFirstColumn(): boolean {
|
||||
return this.dataDirection === 'horizontal' && this.dataType !== 'point';
|
||||
}
|
||||
|
||||
private get showColumnsAsLabels(): boolean {
|
||||
return this.dataDirection === 'vertical' && this.dataType !== 'point';
|
||||
}
|
||||
|
||||
private get showDataType(): boolean {
|
||||
return this.chartTypesSelectBox.value === 'line';
|
||||
}
|
||||
|
||||
public get dataDirection(): DataDirection {
|
||||
return this._chartConfig.dataDirection;
|
||||
}
|
||||
|
||||
public get dataType(): DataType {
|
||||
return this._chartConfig.dataType;
|
||||
}
|
||||
|
||||
@Input() set dataSet(dataSet: IGridDataSet) {
|
||||
// Setup the execute result
|
||||
this._dataSet = dataSet;
|
||||
this._executeResult = <IInsightData>{};
|
||||
this._executeResult.columns = dataSet.columnDefinitions.map(def => def.name);
|
||||
this._executeResult.rows = dataSet.dataRows.getRange(0, dataSet.dataRows.getLength()).map(gridRow => {
|
||||
return gridRow.values.map(cell => cell.displayValue);
|
||||
});
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
public initChart() {
|
||||
this._cd.detectChanges();
|
||||
if (this._executeResult) {
|
||||
// Reinitialize the chart component
|
||||
let componentFactory = this._componentFactoryResolver.resolveComponentFactory<IInsightsView>(insightRegistry.getCtorFromId(this.chartTypesSelectBox.value));
|
||||
this.componentHost.viewContainerRef.clear();
|
||||
let componentRef = this.componentHost.viewContainerRef.createComponent(componentFactory);
|
||||
this._chartComponent = <ChartInsight>componentRef.instance;
|
||||
this._chartComponent.config = this._chartConfig;
|
||||
this._chartComponent.data = this._executeResult;
|
||||
this._chartComponent.options = mixin(this._chartComponent.options, { animation: { duration: 0 } });
|
||||
if (this._chartComponent.init) {
|
||||
this._chartComponent.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._disposables.forEach(i => i.dispose());
|
||||
}
|
||||
}
|
||||
56
src/sql/parts/grid/views/query/chartViewer.css
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
input[type="radio"] {
|
||||
margin-top: -2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.chart-viewer {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.chart-viewer .indent {
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.chart-viewer .radio-indent {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.chart-viewer .option {
|
||||
width: 100%;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.chart-viewer .dialog-label {
|
||||
width: 100%;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.chart-viewer .input-divider {
|
||||
width: 100%;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-viewer .footer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chart-viewer .footer .right-footer {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chart-viewer .footer-button a.monaco-button.monaco-text-button {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.vs-dark.monaco-shell .chart-viewer .footer-button a.monaco-button.monaco-text-button {
|
||||
outline-color: #8e8c8c;
|
||||
}
|
||||
.chart-viewer .footer-button {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.chart-viewer .right-footer .footer-button:last-of-type {
|
||||
margin-right: none;
|
||||
}
|
||||
105
src/sql/parts/grid/views/query/chartViewerActions.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { IMessageService, Severity } from 'vs/platform/message/common/message';
|
||||
|
||||
export interface IChartViewActionContext {
|
||||
copyChart(): void;
|
||||
saveChart(): void;
|
||||
createInsight(): void;
|
||||
}
|
||||
|
||||
export class ChartViewActionBase extends Action {
|
||||
public static BaseClass = 'queryTaskbarIcon';
|
||||
private _classes: string[];
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
enabledClass: string,
|
||||
protected messageService: IMessageService
|
||||
) {
|
||||
super(id, label);
|
||||
this.enabled = true;
|
||||
this._setCssClass(enabledClass);
|
||||
}
|
||||
protected updateCssClass(enabledClass: string): void {
|
||||
// set the class, useful on change of label or icon
|
||||
this._setCssClass(enabledClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the CSS classes combining the parent and child classes.
|
||||
* Public for testing only.
|
||||
*/
|
||||
private _setCssClass(enabledClass: string): void {
|
||||
this._classes = [];
|
||||
this._classes.push(ChartViewActionBase.BaseClass);
|
||||
|
||||
if (enabledClass) {
|
||||
this._classes.push(enabledClass);
|
||||
}
|
||||
this.class = this._classes.join(' ');
|
||||
}
|
||||
|
||||
protected doRun(context: IChartViewActionContext, runAction: Function): TPromise<boolean> {
|
||||
if (!context) {
|
||||
// TODO implement support for finding chart view in active window
|
||||
this.messageService.show(Severity.Error, nls.localize('chartContextRequired', 'Chart View context is required to run this action'));
|
||||
return TPromise.as(false);
|
||||
}
|
||||
return new TPromise<boolean>((resolve, reject) => {
|
||||
runAction();
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CreateInsightAction extends ChartViewActionBase {
|
||||
public static ID = 'chartview.createInsight';
|
||||
public static LABEL = nls.localize('createInsightLabel', "Create Insight");
|
||||
|
||||
constructor(@IMessageService messageService: IMessageService
|
||||
) {
|
||||
super(CreateInsightAction.ID, CreateInsightAction.LABEL, 'createInsight', messageService);
|
||||
}
|
||||
|
||||
public run(context: IChartViewActionContext): TPromise<boolean> {
|
||||
return this.doRun(context, () => context.createInsight());
|
||||
}
|
||||
}
|
||||
|
||||
export class CopyAction extends ChartViewActionBase {
|
||||
public static ID = 'chartview.copy';
|
||||
public static LABEL = nls.localize('copyChartLabel', "Copy as image");
|
||||
|
||||
constructor(@IMessageService messageService: IMessageService
|
||||
) {
|
||||
super(CopyAction.ID, CopyAction.LABEL, 'copyImage', messageService);
|
||||
}
|
||||
|
||||
public run(context: IChartViewActionContext): TPromise<boolean> {
|
||||
return this.doRun(context, () => context.copyChart());
|
||||
}
|
||||
}
|
||||
|
||||
export class SaveImageAction extends ChartViewActionBase {
|
||||
public static ID = 'chartview.saveImage';
|
||||
public static LABEL = nls.localize('saveImageLabel', "Save as image");
|
||||
|
||||
constructor(@IMessageService messageService: IMessageService
|
||||
) {
|
||||
super(SaveImageAction.ID, SaveImageAction.LABEL, 'saveAsImage', messageService);
|
||||
}
|
||||
|
||||
public run(context: IChartViewActionContext): TPromise<boolean> {
|
||||
return this.doRun(context, () => context.saveChart());
|
||||
}
|
||||
}
|
||||
79
src/sql/parts/grid/views/query/query.component.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!--
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
|
||||
<div class="fullsize vertBox" style="position: relative">
|
||||
<div #resultsPane *ngIf="dataSets.length > 0" id="resultspane" class="boxRow resultsMessageHeader resultsViewCollapsible" [class.collapsed]="!resultActive" (click)="togglePane('results')">
|
||||
<span> {{LocalizedConstants.resultPaneLabel}} </span>
|
||||
<span class="queryResultsShortCut"> {{resultShortcut}} </span>
|
||||
</div>
|
||||
<div id="results" *ngIf="renderedDataSets.length > 0" class="results vertBox scrollable"
|
||||
(onScroll)="onScroll($event)" [scrollEnabled]="scrollEnabled" [class.hidden]="!resultActive"
|
||||
(focusin)="onGridFocus()" (focusout)="onGridFocusout()">
|
||||
<div class="boxRow content horzBox slickgrid" *ngFor="let dataSet of renderedDataSets; let i = index"
|
||||
[style.max-height]="dataSet.maxHeight" [style.min-height]="dataSet.minHeight">
|
||||
<slick-grid #slickgrid id="slickgrid_{{i}}" [columnDefinitions]="dataSet.columnDefinitions"
|
||||
[ngClass]="i === activeGrid ? 'active' : ''"
|
||||
[dataRows]="dataSet.dataRows"
|
||||
(contextMenu)="openContextMenu($event, dataSet.batchId, dataSet.resultId, i)"
|
||||
enableAsyncPostRender="true"
|
||||
showDataTypeIcon="false"
|
||||
showHeader="true"
|
||||
[resized]="dataSet.resized"
|
||||
(mousedown)="navigateToGrid(i)"
|
||||
[selectionModel]="selectionModel"
|
||||
[plugins]="slickgridPlugins"
|
||||
class="boxCol content vertBox slickgrid">
|
||||
</slick-grid>
|
||||
<span class="boxCol content vertBox">
|
||||
<div class="boxRow content maxHeight" *ngFor="let icon of dataIcons">
|
||||
<div *ngIf="icon.showCondition()" class="gridIconContainer">
|
||||
<a class="gridIcon" style="cursor: pointer;"
|
||||
(click)="icon.functionality(dataSet.batchId, dataSet.resultId, i)"
|
||||
[title]="icon.hoverText()" [ngClass]="icon.icon()">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="messagepane" class="boxRow resultsMessageHeader resultsViewCollapsible" [class.collapsed]="!messageActive && dataSets.length !== 0" (click)="togglePane('messages')" style="position: relative">
|
||||
<div id="messageResizeHandle" [class.hidden]="!_resultsPane || !_messageActive || !resultActive" class="resizableHandle"></div>
|
||||
<span> {{LocalizedConstants.messagePaneLabel}} </span>
|
||||
<span class="queryResultsShortCut"> {{messageShortcut}} </span>
|
||||
</div>
|
||||
<div id="messages" class="scrollable messages" [class.hidden]="!messageActive && dataSets.length !== 0"
|
||||
(contextmenu)="openMessagesContextMenu($event)" (focusin)="onMessagesFocus()" (focusout)="onMessagesFocusout()"
|
||||
tabindex=0>
|
||||
<div class="messagesTopSpacing"></div>
|
||||
<table id="messageTable" class="resultsMessageTable">
|
||||
<colgroup>
|
||||
<col span="1" class="wideResultsMessage">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<ng-template ngFor let-message [ngForOf]="messages">
|
||||
<tr class='messageRow'>
|
||||
<td><span *ngIf="message.link">[{{message.time}}]</span></td>
|
||||
<td class="resultsMessageValue" [class.errorMessage]="message.isError" [class.batchMessage]="!message.link">{{message.message}} <a class="queryLink" *ngIf="message.link" (click)="onSelectionLinkClicked(message.batchId)">{{message.link.text}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr id='executionSpinner' *ngIf="!complete">
|
||||
<td><span *ngIf="messages.length === 0">[{{startString}}]</span></td>
|
||||
<td>
|
||||
<img class="icon in-progress" height="18px" />
|
||||
<span style="vertical-align: bottom">{{LocalizedConstants.executeQueryLabel}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="complete">
|
||||
<td></td>
|
||||
<td>{{stringsFormat(LocalizedConstants.elapsedTimeLabel, totalElapsedTimeSpan)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="resizeHandle" [class.hidden]="!resizing" [style.top]="resizeHandleTop"></div>
|
||||
</div>
|
||||
603
src/sql/parts/grid/views/query/query.component.ts
Normal file
@@ -0,0 +1,603 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import 'vs/css!sql/media/icons/common-icons';
|
||||
import 'vs/css!sql/parts/grid/media/slickColorTheme';
|
||||
import 'vs/css!sql/parts/grid/media/flexbox';
|
||||
import 'vs/css!sql/parts/grid/media/styles';
|
||||
import 'vs/css!sql/parts/grid/media/slick.grid';
|
||||
import 'vs/css!sql/parts/grid/media/slickGrid';
|
||||
|
||||
import {
|
||||
ElementRef, QueryList, ChangeDetectorRef, OnInit, OnDestroy, Component, Inject,
|
||||
ViewChildren, forwardRef, EventEmitter, Input, ViewChild
|
||||
} from '@angular/core';
|
||||
import { IGridDataRow, SlickGrid, VirtualizedCollection } from 'angular2-slickgrid';
|
||||
import * as rangy from 'sql/base/node/rangy';
|
||||
|
||||
import * as LocalizedConstants from 'sql/parts/query/common/localizedConstants';
|
||||
import * as Services from 'sql/parts/grid/services/sharedServices';
|
||||
import { IGridIcon, IMessage, IRange, IGridDataSet } from 'sql/parts/grid/common/interfaces';
|
||||
import { GridParentComponent } from 'sql/parts/grid/views/gridParentComponent';
|
||||
import { GridActionProvider } from 'sql/parts/grid/views/gridActions';
|
||||
import { IBootstrapService, BOOTSTRAP_SERVICE_ID } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { QueryComponentParams } from 'sql/services/bootstrap/bootstrapParams';
|
||||
import * as WorkbenchUtils from 'sql/workbench/common/sqlWorkbenchUtils';
|
||||
import { error } from 'sql/base/common/log';
|
||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
|
||||
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { clone } from 'vs/base/common/objects';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
|
||||
export const QUERY_SELECTOR: string = 'query-component';
|
||||
|
||||
declare type PaneType = 'messages' | 'results';
|
||||
|
||||
@Component({
|
||||
selector: QUERY_SELECTOR,
|
||||
host: { '(window:keydown)': 'keyEvent($event)', '(window:gridnav)': 'keyEvent($event)' },
|
||||
templateUrl: decodeURI(require.toUrl('sql/parts/grid/views/query/query.component.html')),
|
||||
providers: [{ provide: TabChild, useExisting: forwardRef(() => QueryComponent) }]
|
||||
})
|
||||
export class QueryComponent extends GridParentComponent implements OnInit, OnDestroy {
|
||||
// CONSTANTS
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private scrollTimeOutTime: number = 200;
|
||||
private windowSize: number = 50;
|
||||
private messagePaneHeight: number = 22;
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private maxScrollGrids: number = 8;
|
||||
|
||||
// create a function alias to use inside query.component
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private stringsFormat: any = strings.format;
|
||||
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private dataIcons: IGridIcon[] = [
|
||||
{
|
||||
showCondition: () => { return this.dataSets.length > 1; },
|
||||
icon: () => {
|
||||
return this.renderedDataSets.length === 1
|
||||
? 'exitFullScreen'
|
||||
: 'extendFullScreen';
|
||||
},
|
||||
hoverText: () => {
|
||||
return this.renderedDataSets.length === 1
|
||||
? LocalizedConstants.restoreLabel
|
||||
: LocalizedConstants.maximizeLabel;
|
||||
},
|
||||
functionality: (batchId, resultId, index) => {
|
||||
this.magnify(index);
|
||||
}
|
||||
},
|
||||
{
|
||||
showCondition: () => { return true; },
|
||||
icon: () => { return 'saveCsv'; },
|
||||
hoverText: () => { return LocalizedConstants.saveCSVLabel; },
|
||||
functionality: (batchId, resultId, index) => {
|
||||
let selection = this.slickgrids.toArray()[index].getSelectedRanges();
|
||||
if (selection.length <= 1) {
|
||||
this.handleContextClick({ type: 'savecsv', batchId: batchId, resultId: resultId, index: index, selection: selection });
|
||||
} else {
|
||||
this.dataService.showWarning(LocalizedConstants.msgCannotSaveMultipleSelections);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
showCondition: () => { return true; },
|
||||
icon: () => { return 'saveJson'; },
|
||||
hoverText: () => { return LocalizedConstants.saveJSONLabel; },
|
||||
functionality: (batchId, resultId, index) => {
|
||||
let selection = this.slickgrids.toArray()[index].getSelectedRanges();
|
||||
if (selection.length <= 1) {
|
||||
this.handleContextClick({ type: 'savejson', batchId: batchId, resultId: resultId, index: index, selection: selection });
|
||||
} else {
|
||||
this.dataService.showWarning(LocalizedConstants.msgCannotSaveMultipleSelections);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
showCondition: () => { return true; },
|
||||
icon: () => { return 'saveExcel'; },
|
||||
hoverText: () => { return LocalizedConstants.saveExcelLabel; },
|
||||
functionality: (batchId, resultId, index) => {
|
||||
let selection = this.slickgrids.toArray()[index].getSelectedRanges();
|
||||
if (selection.length <= 1) {
|
||||
this.handleContextClick({ type: 'saveexcel', batchId: batchId, resultId: resultId, index: index, selection: selection });
|
||||
} else {
|
||||
this.dataService.showWarning(LocalizedConstants.msgCannotSaveMultipleSelections);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
showCondition: () => { return true; },
|
||||
icon: () => { return 'viewChart'; },
|
||||
hoverText: () => { return LocalizedConstants.viewChartLabel; },
|
||||
functionality: (batchId, resultId, index) => {
|
||||
this.showChartForGrid(index);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// FIELDS
|
||||
// Service for interaction with the IQueryModel
|
||||
|
||||
// All datasets
|
||||
private dataSets: IGridDataSet[] = [];
|
||||
private messages: IMessage[] = [];
|
||||
private messageStore: IMessage[] = [];
|
||||
private messageTimeout: number;
|
||||
private scrollTimeOut: number;
|
||||
private resizing = false;
|
||||
private resizeHandleTop: string = '0';
|
||||
private scrollEnabled = true;
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private firstRender = true;
|
||||
private totalElapsedTimeSpan: number;
|
||||
private complete = false;
|
||||
private sentPlans: Map<number, string> = new Map<number, string>();
|
||||
private hasQueryPlan: boolean = false;
|
||||
public queryExecutionStatus: EventEmitter<string> = new EventEmitter<string>();
|
||||
public queryPlanAvailable: EventEmitter<string> = new EventEmitter<string>();
|
||||
public showChartRequested: EventEmitter<IGridDataSet> = new EventEmitter<IGridDataSet>();
|
||||
|
||||
@Input() public queryParameters: QueryComponentParams;
|
||||
|
||||
@ViewChildren('slickgrid') slickgrids: QueryList<SlickGrid>;
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
@ViewChild('resultsPane', { read: ElementRef }) private _resultsPane: ElementRef;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) cd: ChangeDetectorRef,
|
||||
@Inject(BOOTSTRAP_SERVICE_ID) bootstrapService: IBootstrapService
|
||||
) {
|
||||
super(el, cd, bootstrapService);
|
||||
this._el.nativeElement.className = 'slickgridContainer';
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by Angular when the object is initialized
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const self = this;
|
||||
|
||||
this.dataService = this.queryParameters.dataService;
|
||||
this.actionProvider = new GridActionProvider(this.dataService, this.onGridSelectAll());
|
||||
|
||||
this.baseInit();
|
||||
this.setupResizeBind();
|
||||
|
||||
this.subscribeWithDispose(this.dataService.queryEventObserver, (event) => {
|
||||
switch (event.type) {
|
||||
case 'start':
|
||||
self.handleStart(self, event);
|
||||
break;
|
||||
case 'complete':
|
||||
self.handleComplete(self, event);
|
||||
break;
|
||||
case 'message':
|
||||
self.handleMessage(self, event);
|
||||
break;
|
||||
case 'resultSet':
|
||||
self.handleResultSet(self, event);
|
||||
break;
|
||||
default:
|
||||
error('Unexpected query event type "' + event.type + '" sent');
|
||||
break;
|
||||
}
|
||||
self._cd.detectChanges();
|
||||
});
|
||||
this.dataService.onAngularLoaded();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.baseDestroy();
|
||||
}
|
||||
|
||||
protected initShortcuts(shortcuts: { [name: string]: Function }): void {
|
||||
shortcuts['event.nextGrid'] = () => {
|
||||
this.navigateToGrid(this.activeGrid + 1);
|
||||
};
|
||||
shortcuts['event.prevGrid'] = () => {
|
||||
this.navigateToGrid(this.activeGrid - 1);
|
||||
};
|
||||
shortcuts['event.maximizeGrid'] = () => {
|
||||
this.magnify(this.activeGrid);
|
||||
};
|
||||
}
|
||||
|
||||
handleStart(self: QueryComponent, event: any): void {
|
||||
self.messages = [];
|
||||
self.dataSets = [];
|
||||
self.placeHolderDataSets = [];
|
||||
self.renderedDataSets = self.placeHolderDataSets;
|
||||
self.totalElapsedTimeSpan = undefined;
|
||||
self.complete = false;
|
||||
self.activeGrid = 0;
|
||||
|
||||
// reset query plan info and send notification to subscribers
|
||||
self.hasQueryPlan = false;
|
||||
self.sentPlans = new Map<number, string>();
|
||||
self.queryExecutionStatus.emit('start');
|
||||
self.firstRender = true;
|
||||
}
|
||||
|
||||
handleComplete(self: QueryComponent, event: any): void {
|
||||
self.totalElapsedTimeSpan = event.data;
|
||||
self.complete = true;
|
||||
}
|
||||
|
||||
handleMessage(self: QueryComponent, event: any): void {
|
||||
self.messageStore.push(event.data);
|
||||
clearTimeout(self.messageTimeout);
|
||||
self.messageTimeout = setTimeout(() => {
|
||||
self.messages = self.messages.concat(self.messageStore);
|
||||
self.messageStore = [];
|
||||
self._cd.detectChanges();
|
||||
self.scrollMessages();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
handleResultSet(self: QueryComponent, event: any): void {
|
||||
let resultSet = event.data;
|
||||
|
||||
// No column info found, so define a column of no name by default
|
||||
if (!resultSet.columnInfo) {
|
||||
resultSet.columnInfo = [];
|
||||
resultSet.columnInfo[0] = { columnName: '' };
|
||||
}
|
||||
// Setup a function for generating a promise to lookup result subsets
|
||||
let loadDataFunction = (offset: number, count: number): Promise<IGridDataRow[]> => {
|
||||
return new Promise<IGridDataRow[]>((resolve, reject) => {
|
||||
self.dataService.getQueryRows(offset, count, resultSet.batchId, resultSet.id).subscribe(rows => {
|
||||
let gridData: IGridDataRow[] = [];
|
||||
for (let row = 0; row < rows.rows.length; row++) {
|
||||
// Push row values onto end of gridData for slickgrid
|
||||
gridData.push({
|
||||
values: rows.rows[row]
|
||||
});
|
||||
}
|
||||
|
||||
// if this is a query plan resultset we haven't processed yet then forward to subscribers
|
||||
if (self.hasQueryPlan && !self.sentPlans[resultSet.batchId]) {
|
||||
self.sentPlans[resultSet.batchId] = rows.rows[0][0].displayValue;
|
||||
self.queryPlanAvailable.emit(rows.rows[0][0].displayValue);
|
||||
}
|
||||
resolve(gridData);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Precalculate the max height and min height
|
||||
let maxHeight: string = 'inherit';
|
||||
if (resultSet.rowCount < self._defaultNumShowingRows) {
|
||||
let maxHeightNumber: number = Math.max((resultSet.rowCount + 1) * self._rowHeight, self.dataIcons.length * 30) + 10;
|
||||
maxHeight = maxHeightNumber.toString() + 'px';
|
||||
}
|
||||
|
||||
let minHeight: string = maxHeight;
|
||||
if (resultSet.rowCount >= self._defaultNumShowingRows) {
|
||||
let minHeightNumber: number = (self._defaultNumShowingRows + 1) * self._rowHeight + 10;
|
||||
minHeight = minHeightNumber.toString() + 'px';
|
||||
}
|
||||
|
||||
// Store the result set from the event
|
||||
let dataSet: IGridDataSet = {
|
||||
resized: undefined,
|
||||
batchId: resultSet.batchId,
|
||||
resultId: resultSet.id,
|
||||
totalRows: resultSet.rowCount,
|
||||
maxHeight: maxHeight,
|
||||
minHeight: minHeight,
|
||||
dataRows: new VirtualizedCollection(
|
||||
self.windowSize,
|
||||
resultSet.rowCount,
|
||||
loadDataFunction,
|
||||
index => { return { values: [] }; }
|
||||
),
|
||||
columnDefinitions: resultSet.columnInfo.map((c, i) => {
|
||||
let isLinked = c.isXml || c.isJson;
|
||||
let linkType = c.isXml ? 'xml' : 'json';
|
||||
return {
|
||||
id: i.toString(),
|
||||
name: c.columnName === 'Microsoft SQL Server 2005 XML Showplan'
|
||||
? 'XML Showplan'
|
||||
: c.columnName,
|
||||
type: self.stringToFieldType('string'),
|
||||
formatter: isLinked ? Services.hyperLinkFormatter : Services.textFormatter,
|
||||
asyncPostRender: isLinked ? self.linkHandler(linkType) : undefined
|
||||
};
|
||||
})
|
||||
};
|
||||
self.dataSets.push(dataSet);
|
||||
|
||||
// check if the resultset is for a query plan
|
||||
for (let i = 0; i < resultSet.columnInfo.length; ++i) {
|
||||
let column = resultSet.columnInfo[i];
|
||||
if (column.columnName === 'Microsoft SQL Server 2005 XML Showplan') {
|
||||
this.hasQueryPlan = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a dataSet to render without rows to reduce DOM size
|
||||
let undefinedDataSet = clone(dataSet);
|
||||
undefinedDataSet.columnDefinitions = dataSet.columnDefinitions;
|
||||
undefinedDataSet.dataRows = undefined;
|
||||
undefinedDataSet.resized = new EventEmitter();
|
||||
self.placeHolderDataSets.push(undefinedDataSet);
|
||||
self.onScroll(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform copy and do other actions for context menu on the messages component
|
||||
*/
|
||||
handleMessagesContextClick(event: { type: string, selectedRange: IRange }): void {
|
||||
switch (event.type) {
|
||||
case 'copySelection':
|
||||
let selectedText = event.selectedRange.text();
|
||||
WorkbenchUtils.executeCopy(selectedText);
|
||||
break;
|
||||
case 'selectall':
|
||||
document.execCommand('selectAll');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
openMessagesContextMenu(event: any): void {
|
||||
let self = this;
|
||||
event.preventDefault();
|
||||
let selectedRange: IRange = this.getSelectedRangeUnderMessages();
|
||||
let selectAllFunc = () => self.selectAllMessages();
|
||||
let anchor = { x: event.x + 1, y: event.y };
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => this.actionProvider.getMessagesActions(this.dataService, selectAllFunc),
|
||||
getKeyBinding: (action) => this._keybindingFor(action),
|
||||
onHide: (wasCancelled?: boolean) => {
|
||||
},
|
||||
getActionsContext: () => (selectedRange)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles rendering the results to the DOM that are currently being shown
|
||||
* and destroying any results that have moved out of view
|
||||
* @param scrollTop The scrolltop value, if not called by the scroll event should be 0
|
||||
*/
|
||||
onScroll(scrollTop): void {
|
||||
const self = this;
|
||||
clearTimeout(self.scrollTimeOut);
|
||||
this.scrollTimeOut = setTimeout(() => {
|
||||
if (self.dataSets.length < self.maxScrollGrids) {
|
||||
self.scrollEnabled = false;
|
||||
for (let i = 0; i < self.placeHolderDataSets.length; i++) {
|
||||
self.placeHolderDataSets[i].dataRows = self.dataSets[i].dataRows;
|
||||
self.placeHolderDataSets[i].resized.emit();
|
||||
}
|
||||
} else {
|
||||
let gridHeight = self._el.nativeElement.getElementsByTagName('slick-grid')[0].offsetHeight;
|
||||
let tabHeight = self.getResultsElement().offsetHeight;
|
||||
let numOfVisibleGrids = Math.ceil((tabHeight / gridHeight)
|
||||
+ ((scrollTop % gridHeight) / gridHeight));
|
||||
let min = Math.floor(scrollTop / gridHeight);
|
||||
let max = min + numOfVisibleGrids;
|
||||
for (let i = 0; i < self.placeHolderDataSets.length; i++) {
|
||||
if (i >= min && i < max) {
|
||||
if (self.placeHolderDataSets[i].dataRows === undefined) {
|
||||
self.placeHolderDataSets[i].dataRows = self.dataSets[i].dataRows;
|
||||
self.placeHolderDataSets[i].resized.emit();
|
||||
}
|
||||
} else if (self.placeHolderDataSets[i].dataRows !== undefined) {
|
||||
self.placeHolderDataSets[i].dataRows = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self._cd.detectChanges();
|
||||
|
||||
if (self.firstRender) {
|
||||
let setActive = () => {
|
||||
if (self.firstRender && self.slickgrids.toArray().length > 0) {
|
||||
self.slickgrids.toArray()[0].setActive();
|
||||
self.firstRender = false;
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setActive();
|
||||
});
|
||||
}
|
||||
}, self.scrollTimeOutTime);
|
||||
}
|
||||
|
||||
onSelectionLinkClicked(index: number): void {
|
||||
this.dataService.setEditorSelection(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the resize for the messages/results panes bar
|
||||
*/
|
||||
setupResizeBind(): void {
|
||||
const self = this;
|
||||
|
||||
let resizeHandleElement: HTMLElement = self._el.nativeElement.querySelector('#messageResizeHandle');
|
||||
let $resizeHandle = $(resizeHandleElement);
|
||||
let $messages = $(self.getMessagesElement());
|
||||
|
||||
$resizeHandle.bind('dragstart', (e) => {
|
||||
self.resizing = true;
|
||||
self.resizeHandleTop = self.calculateResizeHandleTop(e.pageY);
|
||||
self._cd.detectChanges();
|
||||
return true;
|
||||
});
|
||||
|
||||
$resizeHandle.bind('drag', (e) => {
|
||||
// Update the animation if the drag is within the allowed range.
|
||||
if (self.isDragWithinAllowedRange(e.pageY, resizeHandleElement)) {
|
||||
self.resizeHandleTop = self.calculateResizeHandleTop(e.pageY);
|
||||
self.resizing = true;
|
||||
self._cd.detectChanges();
|
||||
|
||||
// Stop the animation if the drag is out of the allowed range.
|
||||
// The animation is resumed when the drag comes back into the allowed range.
|
||||
} else {
|
||||
self.resizing = false;
|
||||
}
|
||||
});
|
||||
|
||||
$resizeHandle.bind('dragend', (e) => {
|
||||
self.resizing = false;
|
||||
// Redefine the min size for the messages based on the final position
|
||||
// if the drag is within the allowed rang
|
||||
if (self.isDragWithinAllowedRange(e.pageY, resizeHandleElement)) {
|
||||
let minHeightNumber = this.getMessagePaneHeightFromDrag(e.pageY);
|
||||
$messages.css('min-height', minHeightNumber + 'px');
|
||||
self._cd.detectChanges();
|
||||
self.resizeGrids();
|
||||
|
||||
// Otherwise just update the UI to show that the drag is complete
|
||||
} else {
|
||||
self._cd.detectChanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the resize of the messagepane given by the drag at top=eventPageY is valid,
|
||||
* false otherwise. A drag is valid if it is below the bottom of the resultspane and
|
||||
* this.messagePaneHeight pixels above the bottom of the entire angular component.
|
||||
*/
|
||||
isDragWithinAllowedRange(eventPageY: number, resizeHandle: HTMLElement): boolean {
|
||||
let resultspaneElement: HTMLElement = this._el.nativeElement.querySelector('#resultspane');
|
||||
let minHeight = this.getMessagePaneHeightFromDrag(eventPageY);
|
||||
|
||||
if (resultspaneElement &&
|
||||
minHeight > 0 &&
|
||||
resultspaneElement.getBoundingClientRect().bottom < eventPageY
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the position of the top of the resize handle given the Y-axis drag
|
||||
* coordinate as eventPageY.
|
||||
*/
|
||||
calculateResizeHandleTop(eventPageY: number): string {
|
||||
let resultsWindowTop: number = this._el.nativeElement.getBoundingClientRect().top;
|
||||
let relativeTop: number = eventPageY - resultsWindowTop;
|
||||
return relativeTop + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the height the message pane would be if it were resized so that its top would be set to eventPageY.
|
||||
* This will return a negative value if eventPageY is below the bottom limit.
|
||||
*/
|
||||
getMessagePaneHeightFromDrag(eventPageY: number): number {
|
||||
let bottomDragLimit: number = this._el.nativeElement.getBoundingClientRect().bottom - this.messagePaneHeight;
|
||||
return bottomDragLimit - eventPageY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the messages tab is scrolled to the bottom
|
||||
*/
|
||||
scrollMessages(): void {
|
||||
let messagesDiv = this.getMessagesElement();
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected tryHandleKeyEvent(e): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles rendering and unrendering necessary resources in order to properly
|
||||
* navigate from one grid another. Should be called any time grid navigation is performed
|
||||
* @param targetIndex The index in the renderedDataSets to navigate to
|
||||
* @returns A boolean representing if the navigation was successful
|
||||
*/
|
||||
navigateToGrid(targetIndex: number): boolean {
|
||||
// check if the target index is valid
|
||||
if (targetIndex >= this.renderedDataSets.length || targetIndex < 0 || !this.hasFocus()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deselect any text since we are navigating to a new grid
|
||||
// Do this even if not switching grids, since this covers clicking on the grid after message selection
|
||||
rangy.getSelection().removeAllRanges();
|
||||
|
||||
// check if you are actually trying to change navigation
|
||||
if (this.activeGrid === targetIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.slickgrids.toArray()[this.activeGrid].selection = false;
|
||||
this.slickgrids.toArray()[targetIndex].setActive();
|
||||
this.activeGrid = targetIndex;
|
||||
|
||||
// scrolling logic
|
||||
let resultsWindow = $('#results');
|
||||
let scrollTop = resultsWindow.scrollTop();
|
||||
let scrollBottom = scrollTop + resultsWindow.height();
|
||||
let gridHeight = $(this._el.nativeElement).find('slick-grid').height();
|
||||
if (scrollBottom < gridHeight * (targetIndex + 1)) {
|
||||
scrollTop += (gridHeight * (targetIndex + 1)) - scrollBottom;
|
||||
resultsWindow.scrollTop(scrollTop);
|
||||
}
|
||||
if (scrollTop > gridHeight * targetIndex) {
|
||||
scrollTop = (gridHeight * targetIndex);
|
||||
resultsWindow.scrollTop(scrollTop);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public hasFocus(): boolean {
|
||||
return DOM.isAncestor(document.activeElement, this._el.nativeElement);
|
||||
}
|
||||
|
||||
resizeGrids(): void {
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
for (let grid of self.renderedDataSets) {
|
||||
grid.resized.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private showChartForGrid(index: number) {
|
||||
if (this.renderedDataSets.length > index) {
|
||||
this.showChartRequested.emit(this.renderedDataSets[index]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper function to toggle messages and results panes */
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private togglePane(pane: PaneType): void {
|
||||
if (pane === 'messages') {
|
||||
this._messageActive = !this._messageActive;
|
||||
} else if (pane === 'results') {
|
||||
this.resultActive = !this.resultActive;
|
||||
}
|
||||
this._cd.detectChanges();
|
||||
this.resizeGrids();
|
||||
}
|
||||
|
||||
layout() {
|
||||
this.resizeGrids();
|
||||
}
|
||||
}
|
||||