Skip to content
This repository was archived by the owner on Aug 23, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"mdi-react": "^7.4.0",
"mitt": "^2.1.0",
"parse-link-header": "^1.0.1",
"prism-react-renderer": "^1.2.0",
"prop-types": "^15.7.2",
"query-string": "^6.13.8",
"react": "^17.0.1",
Expand Down
4 changes: 2 additions & 2 deletions client/src/common/Code.module.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
pre {
.pre {
font-family: 'Courier 10 Pitch', Courier, monospace;
font-size: 95%;
line-height: 140%;
display: inline-block;
word-wrap: break-word;
}

code {
.code {
font-family: Monaco, Consolas, 'Andale Mono', 'DejaVu Sans Mono', monospace;
font-size: 95%;
line-height: 140%;
Expand Down
4 changes: 2 additions & 2 deletions client/src/common/Code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface Props extends React.HTMLAttributes<HTMLElement> {
}

const Code = ({ children, className, type, ...rest }: Props) => {
const cs = [];
const cs = [styles.code];

if (className) {
cs.push(className);
Expand All @@ -17,7 +17,7 @@ const Code = ({ children, className, type, ...rest }: Props) => {
}

return (
<pre>
<pre className={styles.pre}>
<code className={cs.join(' ')} {...rest}>
{children}
</code>
Expand Down
24 changes: 23 additions & 1 deletion client/src/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,28 @@ input {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}

.sp-error {
display: flex;
justify-content: center;
align-items: center;
overflow: auto;

font-size: 1.3rem;
padding: 16px;
text-align: center;
color: hsl(323, 100%, 42%);
}

.sp-info {
display: flex;
justify-content: center;
align-items: center;

font-size: 1.3rem;
padding: 16px;
text-align: center;
}

/* Might be trying out more of just plain css, less of css.modules */
.sp-error-block {
height: 100%;
Expand All @@ -163,7 +185,7 @@ input {
align-items: center;

font-size: 1.3rem;
padding: 24px;
padding: 16px;
text-align: center;
}

Expand Down
194 changes: 194 additions & 0 deletions client/src/queryEditor/HistoryDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import humanizeDuration from 'humanize-duration';
import capitalize from 'lodash/capitalize';
import Highlight, { defaultProps } from 'prism-react-renderer';
import theme from 'prism-react-renderer/themes/vsLight';
import React, { useEffect, useRef } from 'react';
import Button from '../common/Button';
import Drawer from '../common/Drawer';
import ErrorBlock from '../common/ErrorBlock';
import InfoBlock from '../common/InfoBlock';
import SpinKitCube from '../common/SpinKitCube';
import { setEditorBatchHistoryItem } from '../stores/editor-actions';
import { useSessionQueryId, useSessionQueryName } from '../stores/editor-store';
import { api } from '../utilities/api';
import styles from './StatementsTable.module.css';

type Props = {
visible?: boolean;
onClose: (...args: any[]) => any;
};

function HistoryDrawer({ onClose, visible }: Props) {
const bottomEl = useRef<HTMLDivElement>(null);
const containerEl = useRef<HTMLDivElement>(null);

const queryId = useSessionQueryId() || 'null';
const queryName = useSessionQueryName() || 'unsaved queries';

const {
data: queryBatches,
error: queryBatchHistoryError,
} = api.useQueryBatchHistory(queryId);

const fetching = !queryBatches;

const batchLength = queryBatches ? queryBatches.length : 0;
useEffect(() => {
if (visible && batchLength > 0) {
// Without setTimeout this fires too soon? This is sort of hacky and could be fragile
setTimeout(() => {
bottomEl && bottomEl.current && bottomEl.current.scrollIntoView(false);
}, 50);
}
}, [batchLength, visible]);

let content = null;

if (fetching) {
content = (
<div className="h-100 w-100 flex-center">
<SpinKitCube />
</div>
);
} else if (queryBatchHistoryError) {
content = <ErrorBlock>Error getting execution history</ErrorBlock>;
} else if (queryBatches) {
if (queryBatches.length === 0) {
content = (
<div style={{ height: 150, width: '100%' }}>
<InfoBlock>No run history found for this query.</InfoBlock>
</div>
);
} else {
content = queryBatches
.map((batch) => {
return (
<div
key={batch.id}
style={{
border: 'var(--border)',
padding: 8,
marginTop: 16,
marginBottom: 16,
position: 'relative',
}}
>
<h2 style={{ fontSize: '1rem' }}>{batch.createdAtCalendar}</h2>
<div style={{ marginBottom: 16 }}>
{capitalize(batch.status)} in{' '}
{humanizeDuration(batch.durationMs)}
</div>
<Button
style={{ position: 'absolute', top: 8, right: 8 }}
onClick={() => {
setEditorBatchHistoryItem(batch);
onClose();
}}
>
Open in editor
</Button>
<Highlight
{...defaultProps}
theme={theme}
code={batch.selectedText || batch.batchText}
language="sql"
>
{({
className,
style,
tokens,
getLineProps,
getTokenProps,
}) => (
<pre className={className} style={style}>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
{!batch.statements &&
(batch.status === 'finished' || batch.status === 'error') ? (
<div className="sp-info" style={{ fontSize: '1rem' }}>
Query results purged from storage
</div>
) : null}
{(batch.statements || []).map((statement) => {
if (statement.error) {
return (
<div
key={statement.id}
className="sp-error"
style={{ fontSize: '1rem' }}
>
{statement.error.title}
</div>
);
}

if (statement.status !== 'finished') {
return null;
}

return (
<table
key={statement.id}
className={styles.table}
style={{ border: 'var(--border)', marginTop: 8 }}
>
<thead>
<tr>
{(statement?.columns || []).map((column) => (
<th key={column.name}>{column.name}</th>
))}
</tr>
<tr>
<td
style={{ textAlign: 'center' }}
colSpan={(statement?.columns || []).length}
>
<em>
{statement.rowCount}{' '}
{statement.rowCount === 1 ? 'row' : 'rows'}
{statement.incomplete ? '(incomplete)' : ''}
</em>
</td>
</tr>
</thead>
</table>
);
})}
</div>
);
})
.concat([<div key="bottom" ref={bottomEl} />]);
}
}

return (
<Drawer
title={`Run history for ${queryName}`}
visible={visible}
width={'70vw'}
onClose={onClose}
placement="right"
>
<div
ref={containerEl}
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
{content}
</div>
</Drawer>
);
}

export default React.memo(HistoryDrawer);
2 changes: 2 additions & 0 deletions client/src/queryEditor/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import ConnectionDropDown from './ConnectionDropdown';
import ToolbarChartButton from './ToolbarChartButton';
import ToolbarConnectionClientButton from './ToolbarConnectionClientButton';
import ToolbarHistoryButton from './ToolbarHistoryButton';
import ToolbarQueryName from './ToolbarQueryName';
import ToolbarRunButton from './ToolbarRunButton';
import ToolbarSpacer from './ToolbarSpacer';
Expand All @@ -27,6 +28,7 @@ function Toolbar() {
<ToolbarSpacer grow />
<ToolbarRunButton />
<ToolbarSpacer />
<ToolbarHistoryButton />
<ToolbarChartButton />
</div>
</div>
Expand Down
22 changes: 22 additions & 0 deletions client/src/queryEditor/ToolbarHistoryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import HistoryIcon from 'mdi-react/HistoryIcon';
import React, { useState } from 'react';
import IconButton from '../common/IconButton';
import HistoryDrawer from './HistoryDrawer';

function ToolbarHistoryButton() {
const [show, setShow] = useState(false);

return (
<>
<IconButton
tooltip="View query run history"
onClick={() => setShow(true)}
>
<HistoryIcon />
</IconButton>
{show && <HistoryDrawer visible={show} onClose={() => setShow(false)} />}
</>
);
}

export default React.memo(ToolbarHistoryButton);
30 changes: 30 additions & 0 deletions client/src/stores/editor-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ACLRecord,
AppInfo,
Batch,
BatchHistoryItem,
ChartFields,
Connection,
ConnectionClient,
Expand Down Expand Up @@ -463,6 +464,35 @@ export const runQuery = async () => {
});
};

export const setEditorBatchHistoryItem = async (
batchHistoryItem: BatchHistoryItem
) => {
const { focusedSessionId } = getState();

// Statements might not exist if query result data is purged
// In that case, just restore the SQL and chart config and similar
// clear out batchId/selectedStatementId
const hasStatements = batchHistoryItem.statements;

setSession(focusedSessionId, {
queryName: batchHistoryItem.name,
queryText: batchHistoryItem.batchText,
chartType: batchHistoryItem.chart?.chartType,
chartFields: batchHistoryItem.chart?.fields,
connectionId: batchHistoryItem.connectionId,
connectionClient: undefined,
selectedText: '',
batchId: hasStatements ? batchHistoryItem.id : undefined,
selectedStatementId: undefined,
isRunning: false,
runQueryStartTime: batchHistoryItem.startTime,
});

if (hasStatements) {
setBatch(focusedSessionId, batchHistoryItem.id, batchHistoryItem);
}
};

export const saveQuery = async (additionalUpdates?: Partial<EditorSession>) => {
const { focusedSessionId } = getState();
const session = getState().getFocusedSession();
Expand Down
Loading