diff --git a/src/lib/Database/Query.ts b/src/lib/Database/Query.ts index cc8d9ee3..500989e5 100644 --- a/src/lib/Database/Query.ts +++ b/src/lib/Database/Query.ts @@ -30,12 +30,17 @@ export type DatabaseQueryType = Omit[]> { - const results = await connection.all("select id, title, body, createdAt from queries order by createdAt desc"); + static async getAll(): Promise[]> { + const results = await connection.all( + "select id, title, body, createdAt, updatedAt from queries order by createdAt desc" + ); return results.map((query) => { if (query.createdAt) { query.createdAt = moment.utc(query.createdAt, "YYYY-MM-DD HH:mm:ss", true).local(); } + if (query.updatedAt) { + query.updatedAt = moment.utc(query.updatedAt, "YYYY-MM-DD HH:mm:ss", true).local(); + } return query; }); } diff --git a/src/renderer/components/QueryList/QueryList.css b/src/renderer/components/QueryList/QueryList.css index 4c6f4c95..4d74c11f 100644 --- a/src/renderer/components/QueryList/QueryList.css +++ b/src/renderer/components/QueryList/QueryList.css @@ -106,10 +106,31 @@ margin-top: 2px; } +.QueryList-item-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; + margin-bottom: 5px; +} + +.QueryList-item-id { + font-size: 11px; + color: var(--color-text-muted); + display: flex; + align-items: center; + gap: 6px; +} + +.QueryList-item-update-mark { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--color-accent); +} + .QueryList-item-time { font-size: 11px; color: var(--color-text-muted); - align-self: flex-end; - margin-top: 10px; - margin-bottom: 5px; } diff --git a/src/renderer/components/QueryList/QueryList.tsx b/src/renderer/components/QueryList/QueryList.tsx index 09cbc061..719ca964 100644 --- a/src/renderer/components/QueryList/QueryList.tsx +++ b/src/renderer/components/QueryList/QueryList.tsx @@ -9,6 +9,7 @@ const MENU_ID = "QUERY_LIST_MENU"; type Props = { readonly queries: QueryType[]; readonly selectedQueryId: number | null; + readonly markedQueryIds: number[]; readonly onAddQuery: () => void; readonly onRefresh: () => void; readonly onSelectQuery: (queryId: number) => void; @@ -19,6 +20,7 @@ type Props = { const QueryList: React.FC = ({ queries, selectedQueryId, + markedQueryIds, onAddQuery, onRefresh, onSelectQuery, @@ -95,7 +97,15 @@ const QueryList: React.FC = ({
{query.title}
{query.body.replace(/\s{2,}/g, " ").substring(0, 50)}
-
{query.createdAt.format("YYYY-MM-DD")}
+
+
+ ID: {query.id} + {markedQueryIds.includes(query.id) && ( + + )} +
+
{query.createdAt.format("YYYY-MM-DD")}
+
))} diff --git a/src/renderer/pages/Query/Query.tsx b/src/renderer/pages/Query/Query.tsx index 0318de2a..1676fc54 100644 --- a/src/renderer/pages/Query/Query.tsx +++ b/src/renderer/pages/Query/Query.tsx @@ -17,16 +17,22 @@ class Query extends React.Component { override componentDidMount(): void { Action.initialize(); window.addEventListener("focus", this.handleWindowFocus); + window.addEventListener("blur", this.handleWindowBlur); } override componentWillUnmount(): void { window.removeEventListener("focus", this.handleWindowFocus); + window.removeEventListener("blur", this.handleWindowBlur); } private handleWindowFocus = (): void => { Action.initialize(); }; + private handleWindowBlur = (): void => { + Action.markWindowBlurred(); + }; + handleAddQuery(): void { const defaultDataSourceId = this.state.setting.defaultDataSourceId; const ds = defaultDataSourceId !== null ? this.findDataSourceById(defaultDataSourceId) : this.state.dataSources[0]; diff --git a/src/renderer/pages/Query/QueryAction.ts b/src/renderer/pages/Query/QueryAction.ts index 657065fe..83648316 100644 --- a/src/renderer/pages/Query/QueryAction.ts +++ b/src/renderer/pages/Query/QueryAction.ts @@ -105,6 +105,7 @@ const QueryAction = { id, params: Object.assign({ executor: null }, params), }); + dispatch("markQueryAfterExecution", { id }); Database.Query.update(id, params); ipcRenderer.send("queryCompleted", { @@ -129,6 +130,7 @@ const QueryAction = { id, params: Object.assign({ executor: null }, params), }); + dispatch("markQueryAfterExecution", { id }); Database.Query.update( id, Object.assign(params, { @@ -156,6 +158,10 @@ const QueryAction = { dispatch("updateEditor", params); }, + markWindowBlurred(): void { + dispatch("windowBlurred", { timestamp: Date.now() }); + }, + async selectResultTab(query: QueryType, name: string): Promise { dispatch("selectResultTab", { id: query.id, name }); diff --git a/src/renderer/pages/Query/QueryStore.ts b/src/renderer/pages/Query/QueryStore.ts index 1ea15f46..58c45ee0 100644 --- a/src/renderer/pages/Query/QueryStore.ts +++ b/src/renderer/pages/Query/QueryStore.ts @@ -10,6 +10,8 @@ export interface QueryState { dataSources: DataSourceType[]; charts: ChartType[]; selectedQueryId: number | null; + markedQueryIds: number[]; + inactiveSinceAt: number | null; editor: { height: number | null; line: number | null; @@ -25,6 +27,8 @@ export default class QueryStore extends Store { dataSources: [], charts: [], selectedQueryId: null, + markedQueryIds: [], + inactiveSinceAt: null, editor: { height: null, line: null, @@ -35,10 +39,33 @@ export default class QueryStore extends Store { override reduce(type: string, payload: any): StateBuilder { switch (type) { case "initialize": { + const { inactiveSinceAt } = this.state; + const existingIds = new Set(payload.queries.map((q: QueryType) => q.id)); + const markedIds = new Set(this.state.markedQueryIds.filter((id) => existingIds.has(id))); + (payload.queries as QueryType[]) + .filter((q) => inactiveSinceAt !== null && q.updatedAt && q.updatedAt.valueOf() > inactiveSinceAt) + .forEach((q) => markedIds.add(q.id)); return this.merge("setting", payload.setting) .mergeList("queries", payload.queries) .mergeList("charts", payload.charts) - .mergeList("dataSources", payload.dataSources); + .mergeList("dataSources", payload.dataSources) + .set("markedQueryIds", [...markedIds]) + .set("inactiveSinceAt", null); + } + case "windowBlurred": { + if (this.state.inactiveSinceAt !== null) { + return this.set("inactiveSinceAt", this.state.inactiveSinceAt); + } + return this.set("inactiveSinceAt", payload.timestamp); + } + case "markQueryAfterExecution": { + if (this.state.selectedQueryId === payload.id) { + return this.set("markedQueryIds", this.state.markedQueryIds); + } + if (this.state.markedQueryIds.includes(payload.id)) { + return this.set("markedQueryIds", this.state.markedQueryIds); + } + return this.set("markedQueryIds", [...this.state.markedQueryIds, payload.id]); } case "selectQuery": { const idx = this.findQueryIndex(payload.id); @@ -49,7 +76,17 @@ export default class QueryStore extends Store { status: currentQuery.status || payload.query.status, executor: currentQuery.executor, }; - return this.set("selectedQueryId", payload.id).set("editor.line", null).merge(`queries.${idx}`, updatedQuery); + const idsToClear = new Set([payload.id]); + if (this.state.selectedQueryId !== null) { + idsToClear.add(this.state.selectedQueryId); + } + return this.set("selectedQueryId", payload.id) + .set("editor.line", null) + .merge(`queries.${idx}`, updatedQuery) + .set( + "markedQueryIds", + this.state.markedQueryIds.filter((id) => !idsToClear.has(id)) + ); } case "addNewQuery": { return this.set("selectedQueryId", payload.query.id).set("editor.line", null).prepend("queries", payload.query); @@ -60,7 +97,13 @@ export default class QueryStore extends Store { } case "deleteQuery": { const idx = this.findQueryIndex(payload.id); - return this.set("selectedQueryId", null).set("editor.line", null).del(`queries.${idx}`); + return this.set("selectedQueryId", null) + .set("editor.line", null) + .del(`queries.${idx}`) + .set( + "markedQueryIds", + this.state.markedQueryIds.filter((id) => id !== payload.id) + ); } case "updateEditor": { return this.merge("editor", payload); diff --git a/test/unit/lib/Database/Query.test.ts b/test/unit/lib/Database/Query.test.ts index b4790a87..f0f421dc 100644 --- a/test/unit/lib/Database/Query.test.ts +++ b/test/unit/lib/Database/Query.test.ts @@ -21,12 +21,14 @@ suite("Database/Query", () => { id: 1, title: "title 1", createdAt: moment.utc("2017-01-01 00:00:00", "YYYY-MM-DD HH:mm:ss", true).local(), + updatedAt: moment.utc("2017-01-02 00:00:00", "YYYY-MM-DD HH:mm:ss", true).local(), body: "select 1;", }, { id: 2, title: "title 2", createdAt: moment.utc("2017-01-01 00:00:00", "YYYY-MM-DD HH:mm:ss", true).local(), + updatedAt: moment.utc("2017-01-02 00:00:00", "YYYY-MM-DD HH:mm:ss", true).local(), body: "select 1;", }, ]);