diff --git a/package.json b/package.json index 13298287..eb61aef4 100644 --- a/package.json +++ b/package.json @@ -215,7 +215,7 @@ }, { "id": "queryHistory", - "name": "Query History", + "name": "Statement History", "visibility": "visible", "when": "code-for-ibmi:connected == true" }, @@ -240,7 +240,7 @@ "viewsWelcome": [ { "view": "queryHistory", - "contents": "Query history will appear here." + "contents": "Statement history will appear here." }, { "view": "jobManager", @@ -413,13 +413,13 @@ }, { "command": "vscode-db2i.queryHistory.remove", - "title": "Remove query from history", + "title": "Remove statement from history", "category": "Db2 for i", "icon": "$(trash)" }, { "command": "vscode-db2i.queryHistory.clear", - "title": "Clear query history", + "title": "Clear statement history", "category": "Db2 for i", "icon": "$(trash)" }, diff --git a/src/Storage.ts b/src/Storage.ts index b9578a57..c2abc677 100644 --- a/src/Storage.ts +++ b/src/Storage.ts @@ -3,7 +3,12 @@ import vscode from 'vscode'; const QUERIES_KEY = `queries`; const SERVERCOMPONENT_KEY = `serverVersion` -export type QueryList = string[]; +export interface QueryHistoryItem { + query: string; + unix: number; +} + +export type QueryList = QueryHistoryItem[]; abstract class Storage { protected readonly globalState; @@ -54,6 +59,24 @@ export class ConnectionStorage extends Storage { return this.set(SERVERCOMPONENT_KEY, name); } + /** + * Eventually we will want to remove this function, but for now we need to fix the past queries + */ + fixPastQueries() { + const currentList = this.getPastQueries() as (string|QueryHistoryItem)[]; + const hasOldFormat = currentList.some(item => typeof item === `string`); + if (hasOldFormat) { + const newList = currentList.map(item => { + if (typeof item === `string`) { + return { query: item, unix: Math.floor(Date.now() / 1000) - 86400 }; + } else { + return item; + } + }); + return this.setPastQueries(newList); + } + } + getPastQueries() { return this.get(QUERIES_KEY) || []; } diff --git a/src/config.ts b/src/config.ts index 12527063..085658ea 100644 --- a/src/config.ts +++ b/src/config.ts @@ -33,6 +33,8 @@ export async function onConnectOrServerInstall(): Promise { Config.setConnectionName(instance.getConnection().currentConnectionName); determineFeatures(); + await Config.fixPastQueries(); + await ServerComponent.initialise().then(installed => { if (installed) { JobManagerView.setVisible(true); diff --git a/src/views/html.ts b/src/views/html.ts index 912bdf27..1f6799ea 100644 --- a/src/views/html.ts +++ b/src/views/html.ts @@ -20,6 +20,13 @@ export function getHeader(options: {withCollapsed?: boolean} = {}): string { top: 0; /* Don't forget this, required for the stickiness */ } + tfoot tr { + background-color: var(--vscode-multiDiffEditor-headerBackground); + text-align: left; + position: sticky; /* Lock the footer row to the bottom so it's always visible as rows are scrolled */ + bottom: 0; /* Don't forget this, required for the stickiness */ + } + #resultset th, #resultset td { padding: 5px 15px; diff --git a/src/views/queryHistoryView.ts b/src/views/queryHistoryView.ts index 87d77375..4fb8151f 100644 --- a/src/views/queryHistoryView.ts +++ b/src/views/queryHistoryView.ts @@ -1,6 +1,7 @@ import vscode, { MarkdownString, ThemeIcon, TreeItem, window, workspace } from "vscode"; import { TreeDataProvider } from "vscode"; import { Config } from "../config"; +import { QueryHistoryItem } from "../Storage"; const openSqlDocumentCommand = `vscode-db2i.openSqlDocument`; @@ -22,7 +23,7 @@ export class queryHistory implements TreeDataProvider { vscode.commands.registerCommand(`vscode-db2i.queryHistory.prepend`, async (newQuery?: string) => { if (newQuery && Config.ready) { let currentList = Config.getPastQueries(); - const existingQuery = currentList.findIndex(query => query.trim() === newQuery.trim()); + const existingQuery = currentList.findIndex(queryItem => queryItem.query.trim() === newQuery.trim()); // If it exists, remove it if (existingQuery > 0) { @@ -31,7 +32,10 @@ export class queryHistory implements TreeDataProvider { // If it's at the top, don't add it, it's already at the top if (existingQuery !== 0) { - currentList.splice(0, 0, newQuery); + currentList.splice(0, 0, { + query: newQuery, + unix: Math.floor(Date.now() / 1000) + }); } await Config.setPastQueries(currentList); @@ -40,11 +44,13 @@ export class queryHistory implements TreeDataProvider { } }), - vscode.commands.registerCommand(`vscode-db2i.queryHistory.remove`, async (node: PastQuery) => { + vscode.commands.registerCommand(`vscode-db2i.queryHistory.remove`, async (node: PastQueryNode) => { if (node && Config.ready) { let currentList = Config.getPastQueries(); const chosenQuery = node.query; - const existingQuery = currentList.findIndex(query => query.trim() === chosenQuery.trim()); + const existingQuery = currentList.findIndex(queryItem => + queryItem.query.trim() === chosenQuery.trim() + ); // If it exists, remove it if (existingQuery >= 0) { @@ -72,9 +78,58 @@ export class queryHistory implements TreeDataProvider { return element; } - async getChildren(): Promise { + async getChildren(timePeriod?: TimePeriodNode): Promise { if (Config.ready) { - return Config.getPastQueries().map(query => new PastQuery(query)); + if (timePeriod) { + return timePeriod.getChildren(); + + } else { + const currentList = Config.getPastQueries(); + + const day = 60 * 60 * 24; + const week = day * 7; + const month = day * 30; + + const now = Math.floor(Date.now() / 1000); + const dayAgo = now - day; + const weekAgo = now - week; + const monthAgo = now - month; + + let pastDayQueries: PastQueryNode[] = []; + let pastWeekQueries: PastQueryNode[] = []; + let pastMonthQueries: PastQueryNode[] = []; + let olderQueries: PastQueryNode[] = []; + + currentList.forEach(queryItem => { + // The smaller the unix value, the older it is + if (queryItem.unix < monthAgo) { + olderQueries.push(new PastQueryNode(queryItem.query)); + } else if (queryItem.unix < weekAgo) { + pastMonthQueries.push(new PastQueryNode(queryItem.query)); + } else if (queryItem.unix < dayAgo) { + pastWeekQueries.push(new PastQueryNode(queryItem.query)); + } else { + pastDayQueries.push(new PastQueryNode(queryItem.query)); + } + }); + + let nodes: TimePeriodNode[] = []; + + if (pastDayQueries.length > 0) { + nodes.push(new TimePeriodNode(`Past day`, pastDayQueries, true)); + } + if (pastWeekQueries.length > 0) { + nodes.push(new TimePeriodNode(`Past week`, pastWeekQueries)); + } + if (pastMonthQueries.length > 0) { + nodes.push(new TimePeriodNode(`Past month`, pastMonthQueries)); + } + if (olderQueries.length > 0) { + nodes.push(new TimePeriodNode(`Older`, olderQueries)); + } + + return nodes; + } } else { return [new TreeItem(`A connection is required for query history`)]; @@ -82,7 +137,21 @@ export class queryHistory implements TreeDataProvider { } } -class PastQuery extends vscode.TreeItem { +class TimePeriodNode extends vscode.TreeItem { + constructor(public period: string, private nodes: PastQueryNode[], expanded = false) { + super(period, expanded ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed); + + this.contextValue = `timePeriod`; + + this.iconPath = new ThemeIcon(`calendar`); + } + + getChildren() { + return this.nodes; + } +} + +class PastQueryNode extends vscode.TreeItem { constructor(public query: string) { super(query.length > 63 ? query.substring(0, 60) + `...` : query); diff --git a/src/views/results/html.ts b/src/views/results/html.ts index 07b3a83e..4c3ef5eb 100644 --- a/src/views/results/html.ts +++ b/src/views/results/html.ts @@ -52,16 +52,19 @@ export function generateScroller(basicSelect: string, isCL: boolean): string { */ const vscode = acquireVsCodeApi(); const basicSelect = ${JSON.stringify(basicSelect)}; + const htmlTableId = 'resultset'; + const statusId = 'status'; + const messageSpanId = 'messageSpan'; + let myQueryId = ''; - - let mustLoadHeaders = true; + let needToInitializeTable = true; let totalRows = 0; let noMoreRows = false; let isFetching = false; window.addEventListener("load", main); function main() { - let Observer = new IntersectionObserver(function(entries) { + new IntersectionObserver(function(entries) { // isIntersecting is true when element and viewport are overlapping // isIntersecting is false when element and viewport don't overlap if(entries[0].isIntersecting === true) { @@ -69,9 +72,7 @@ export function generateScroller(basicSelect: string, isCL: boolean): string { fetchNextPage(); } } - }, { threshold: [0] }); - - Observer.observe(document.getElementById("nextButton")); + }, { threshold: [0] }).observe(document.getElementById(messageSpanId)); window.addEventListener('message', event => { const data = event.data; @@ -85,21 +86,21 @@ export function generateScroller(basicSelect: string, isCL: boolean): string { isFetching = false; noMoreRows = data.isDone; - if (mustLoadHeaders && event.data.columnList) { - setHeaders('resultset', event.data.columnList); - mustLoadHeaders = false; + if (needToInitializeTable && event.data.columnList) { + initializeTable(event.data.columnList); + needToInitializeTable = false; } if (data.rows && data.rows.length > 0) { totalRows += data.rows.length; - appendRows('resultset', data.rows); + appendRows(data.rows); } - const nextButton = document.getElementById("nextButton"); if (data.rows === undefined && totalRows === 0) { - nextButton.innerText = 'Query executed with no result set returned. Rows affected: ' + data.update_count; + document.getElementById(messageSpanId).innerText = 'Statement executed with no result set returned. Rows affected: ' + data.update_count; } else { - nextButton.innerText = noMoreRows ? ('Loaded ' + totalRows + '. End of data') : ('Loaded ' + totalRows + '. Fetching more...'); + document.getElementById(statusId).innerText = noMoreRows ? ('Loaded ' + totalRows + '. End of data') : ('Loaded ' + totalRows + '. More available.'); + document.getElementById(messageSpanId).style.visibility = "hidden"; } break; @@ -124,27 +125,26 @@ export function generateScroller(basicSelect: string, isCL: boolean): string { document.getElementById("spinnerContent").style.display = 'none'; } - function setHeaders(tableId, columns) { - var tHeadRef = document.getElementById(tableId).getElementsByTagName('thead')[0]; - tHeadRef.innerHTML = ''; - - // Insert a row at the end of table - var newRow = tHeadRef.insertRow(); - - columns.forEach(colName => { - // Insert a cell at the end of the row - var newCell = newRow.insertCell(); - - // Append a text node to the cell - var newText = document.createTextNode(colName); - newCell.appendChild(newText); - }); + function initializeTable(columns) { + // Initialize the header + var header = document.getElementById(htmlTableId).getElementsByTagName('thead')[0]; + header.innerHTML = ''; + var headerRow = header.insertRow(); + columns.forEach(colName => headerRow.insertCell().appendChild(document.createTextNode(colName))); + + // Initialize the footer + var footer = document.getElementById(htmlTableId).getElementsByTagName('tfoot')[0]; + footer.innerHTML = ''; + var newCell = footer.insertRow().insertCell(); + newCell.colSpan = columns.length; + newCell.id = statusId; + newCell.appendChild(document.createTextNode(' ')); } - function appendRows(tableId, arrayOfObjects) { - var tBodyRef = document.getElementById(tableId).getElementsByTagName('tbody')[0]; + function appendRows(rows) { + var tBodyRef = document.getElementById(htmlTableId).getElementsByTagName('tbody')[0]; - for (const row of arrayOfObjects) { + for (const row of rows) { // Insert a row at the end of table var newRow = tBodyRef.insertRow() @@ -170,8 +170,9 @@ export function generateScroller(basicSelect: string, isCL: boolean): string { -
-

+ + +

Running statement