Skip to content

feat(web-console): add loading indicator and notification for a single query when running multiple queries #456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
27 changes: 14 additions & 13 deletions packages/browser-tests/cypress/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const tableSchemas = {
const materializedViewSchemas = {
btc_trades_mv:
"CREATE MATERIALIZED VIEW IF NOT EXISTS btc_trades_mv WITH BASE btc_trades as (" +
"SELECT timestamp, avg(amount) FROM btc_trades SAMPLE BY 1m) PARTITION BY week;",
"SELECT timestamp, avg(amount) avg FROM btc_trades SAMPLE BY 1m) PARTITION BY week;",
};

before(() => {
Expand Down Expand Up @@ -209,18 +209,7 @@ Cypress.Commands.add("getCursorQueryGlyph", () => cy.get(".cursorQueryGlyph"));
Cypress.Commands.add("getRunIconInLine", (lineNumber) => {
cy.getCursorQueryGlyph().should("be.visible");
const selector = `.cursorQueryGlyph-line-${lineNumber}`;
cy.get("body").then(() => {
let element = null;

if (Cypress.$(selector).length > 0) {
element = cy.get(selector).first();
}

if (!element) {
throw new Error(`No run icon found for line ${lineNumber}.`);
}
return element;
});
return cy.get(selector).first();
});

Cypress.Commands.add("openRunDropdownInLine", (lineNumber) => {
Expand Down Expand Up @@ -249,6 +238,18 @@ Cypress.Commands.add("clickRunQuery", () => {
.wait("@exec");
});

Cypress.Commands.add("clickRunScript", (continueOnFailure = false) => {
cy.getEditor().should("be.visible");
cy.getByDataHook("button-run-query-dropdown").click();
cy.getByDataHook("button-run-script").click();

cy.getByRole("dialog").should("be.visible");
if (continueOnFailure) {
cy.getByDataHook("stop-after-failure-checkbox").uncheck();
}
cy.getByDataHook("run-all-queries-confirm").click();
});

const numberRangeRegexp = (n, width = 3) => {
const [min, max] = [n - width, n + width];
const numbers = Array.from(
Expand Down
99 changes: 78 additions & 21 deletions packages/browser-tests/cypress/integration/console/editor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,8 @@ describe("run all queries in tab", () => {
cy.typeQuery("select 1;\nselect a;\nselect 3;");

// When
cy.getByDataHook("button-run-query-dropdown").click();
cy.getByDataHook("button-run-script").click();
// Then
cy.getByRole("dialog").should("be.visible");
cy.getByDataHook("stop-after-failure-checkbox").should("be.checked");
cy.clickRunScript();

// When
cy.getByDataHook("run-all-queries-confirm").click();
// Then
cy.getByDataHook("error-notification")
.invoke("text")
Expand All @@ -321,14 +315,8 @@ describe("run all queries in tab", () => {
cy.typeQuery("select 1;\nselect a;\nselect 3;");

// When
cy.getByDataHook("button-run-query-dropdown").click();
cy.getByDataHook("button-run-script").click();
// Then
cy.getByRole("dialog").should("be.visible");
cy.getByDataHook("stop-after-failure-checkbox").uncheck();
cy.clickRunScript(true);

// When
cy.getByDataHook("run-all-queries-confirm").click();
// Then
cy.getByDataHook("success-notification")
.invoke("text")
Expand All @@ -340,6 +328,81 @@ describe("run all queries in tab", () => {
cy.get(".error-glyph").should("have.length", 1);
cy.get(".cursorQueryGlyph").should("have.length", 3);
});

it("should scroll to the running query and show the loading notification", () => {
// Given
cy.intercept("/exec*", (req) => {
req.on("response", (res) => {
res.setDelay(1000);
});
});
cy.typeQuery(
"select 1;\n\n\n\n\n\n\n\n\nselect 2;\n\n\n\n\n\n\n\n\n\nselect 3;"
);

// When
cy.clickRunScript();

// Then
cy.getByDataHook("loading-notification").should(
"contain",
`Running query "select 1"`
);
cy.getRunIconInLine(1).should("be.visible");
// Then
cy.getByDataHook("loading-notification").should(
"contain",
`Running query "select 2"`
);
cy.getRunIconInLine(10).should("be.visible");
// Then
cy.getByDataHook("loading-notification").should(
"contain",
`Running query "select 3"`
);
cy.getRunIconInLine(20).should("be.visible");
});

it("should disable editing when running script", () => {
// Given
cy.intercept("/exec*", (req) => {
req.on("response", (res) => {
res.setDelay(1000);
});
});
cy.typeQuery("select 1;\nselect 2;\nselect 3;");

// When
cy.clickRunScript();

// Then
cy.typeQuery("should not be visible");
cy.getEditorContent().should("not.contain", "should not be visible");
});

it("should move cursor to the failed query after running script", () => {
// Given
cy.typeQuery("select 1;\nselect a;\nselect 2;");
cy.clickLine(1);

// When
cy.clickRunScript();

// Then
cy.get(".active-line-number").should("contain", "2");
});

it("should keep the cursor position if all queries are successful", () => {
// Given
cy.typeQuery("select 1;\nselect 2;\nselect 3;");
cy.clickLine(1);

// When
cy.clickRunScript();

// Then
cy.get(".active-line-number").should("contain", "1");
});
});

describe("appendQuery", () => {
Expand Down Expand Up @@ -1076,14 +1139,8 @@ describe("multiple run buttons with dynamic query log", () => {
it("should keep execution info per tab", () => {
// When
cy.typeQuery("select 1;\nselect a;\nselect 3;");
cy.getByDataHook("button-run-query-dropdown").click();
cy.getByDataHook("button-run-script").click();
// Then
cy.getByRole("dialog").should("be.visible");
cy.getByDataHook("stop-after-failure-checkbox").uncheck();
cy.clickRunScript(true);

// When
cy.getByDataHook("run-all-queries-confirm").click();
// Then
cy.getByDataHook("success-notification")
.invoke("text")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,13 @@ describe("questdb schema with suspended tables with Linux OS error codes", () =>
cy.typeQuery(
"ALTER TABLE btc_trades SUSPEND WAL WITH 24, 'Too many open files';"
)
.clickRunIconInLine(1)
.clickRunQuery()
.clearEditor();

cy.typeQuery(
"ALTER TABLE ecommerce_stats SUSPEND WAL WITH 12, 'Out of memory';"
)
.clickRunIconInLine(1)
.clickRunQuery()
.clearEditor();
});
beforeEach(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-tests/questdb
Submodule questdb updated 154 files
95 changes: 61 additions & 34 deletions packages/web-console/src/scenes/Editor/Monaco/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ const Content = styled(PaneContent)`
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGhlaWdodD0iMjJweCIgd2lkdGg9IjIycHgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8ZGVmcz4KICAgICAgICA8Y2xpcFBhdGggaWQ9ImNsaXAwIj48cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiLz48L2NsaXBQYXRoPgogICAgPC9kZWZzPgogICAgPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwKSI+CiAgICAgICAgPHBhdGggZD0iTTggNC45MzR2MTQuMTMyYzAgLjQzMy40NjYuNzAyLjgxMi40ODRsMTAuNTYzLTcuMDY2YS41LjUgMCAwIDAgMC0uODMyTDguODEyIDQuNjE2QS41LjUgMCAwIDAgOCA0LjkzNFoiIGZpbGw9IiM1MGZhN2IiLz4KICAgICAgICA8Y2lyY2xlIGN4PSIxOCIgY3k9IjgiIHI9IjYiIGZpbGw9IiNmZjU1NTUiLz4KICAgICAgICA8cmVjdCB4PSIxNyIgeT0iNCIgd2lkdGg9IjIiIGhlaWdodD0iNSIgZmlsbD0id2hpdGUiIHJ4PSIwLjUiLz4KICAgICAgICA8Y2lyY2xlIGN4PSIxOCIgY3k9IjExIiByPSIxIiBmaWxsPSJ3aGl0ZSIvPgogICAgPC9nPgo8L3N2Zz4=");
}

.cursorQueryGlyph.loading-glyph:after {
height: 22px;
width: 22px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPgogIDxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wIDBoMjR2MjRIMHoiIC8+CiAgPHBhdGggZD0iTTEyIDJhMSAxIDAgMCAxIDEgMXYzYTEgMSAwIDAgMS0yIDBWM2ExIDEgMCAwIDEgMS0xem0wIDE1YTEgMSAwIDAgMSAxIDF2M2ExIDEgMCAwIDEtMiAwdi0zYTEgMSAwIDAgMSAxLTF6bTguNjYtMTBhMSAxIDAgMCAxLS4zNjYgMS4zNjZsLTIuNTk4IDEuNWExIDEgMCAxIDEtMS0xLjczMmwyLjU5OC0xLjVBMSAxIDAgMCAxIDIwLjY2IDd6TTcuNjcgMTQuNWExIDEgMCAwIDEtLjM2NiAxLjM2NmwtMi41OTggMS41YTEgMSAwIDEgMS0xLTEuNzMybDIuNTk4LTEuNWExIDEgMCAwIDEgMS4zNjYuMzY2ek0yMC42NiAxN2ExIDEgMCAwIDEtMS4zNjYuMzY2bC0yLjU5OC0xLjVhMSAxIDAgMCAxIDEtMS43MzJsMi41OTggMS41QTEgMSAwIDAgMSAyMC42NiAxN3pNNy42NyA5LjVhMSAxIDAgMCAxLTEuMzY2LjM2NmwtMi41OTgtMS41YTEgMSAwIDEgMSAxLTEuNzMybDIuNTk4IDEuNUExIDEgMCAwIDEgNy42NyA5LjV6IiAvPgo8L3N2Zz4=");
animation: loading-glyph-spin 3s linear infinite;
}

@keyframes loading-glyph-spin {
from { transform: rotate(0); }
to { transform: rotate(360deg); }
}

.cancelQueryGlyph {
&:after {
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGhlaWdodD0iMjJweCIgd2lkdGg9IjIycHgiIGFyaWEtaGlkZGVuPSJ0cnVlIiBmb2N1c2FibGU9ImZhbHNlIiBmaWxsPSIjZmY1NTU1IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJTdHlsZWRJY29uQmFzZS1zYy1lYTl1bGotMCBqQ2hkR0siPjxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wIDBoMjR2MjRIMHoiPjwvcGF0aD48cGF0aCBkPSJNNyA3djEwaDEwVjdIN3pNNiA1aDEyYTEgMSAwIDAgMSAxIDF2MTJhMSAxIDAgMCAxLTEgMUg2YTEgMSAwIDAgMS0xLTFWNmExIDEgMCAwIDEgMS0xeiI+PC9wYXRoPjwvc3ZnPgo=");
Expand Down Expand Up @@ -801,6 +813,17 @@ const MonacoEditor = ({ executionRefs }: { executionRefs: React.MutableRefObject
let notification: Partial<NotificationShape> & { content: ReactNode, query: QueryKey } | null = null
dispatch(actions.query.setResult(undefined))

dispatch(actions.query.setActiveNotification({
type: NotificationType.LOADING,
query: `${activeBufferRef.current.label}@${0}-${0}`,
content: (
<Box gap="1rem" align="center">
<Text color="foreground">Running query "{effectiveQueryText.length > 30 ? `${effectiveQueryText.slice(0, 30)}...` : effectiveQueryText}"</Text>
</Box>
),
createdAt: new Date(),
}))

try {
const result = await quest.queryRaw(normalizeQueryText(effectiveQueryText), { limit: "0,1000", explain: true })

Expand Down Expand Up @@ -930,7 +953,9 @@ const MonacoEditor = ({ executionRefs }: { executionRefs: React.MutableRefObject
const handleRunScript = async () => {
let successfulQueries = 0
let failedQueries = 0
if (!editorRef.current) return
const editor = editorRef.current
const monaco = monacoRef.current
if (!editor || !monaco) return
const queriesToRun = queriesToRunRef.current && queriesToRunRef.current.length > 1 ? queriesToRunRef.current : undefined
const runningAllQueries = !queriesToRun
// Clear all notifications & execution refs for the buffer
Expand All @@ -944,30 +969,36 @@ const MonacoEditor = ({ executionRefs }: { executionRefs: React.MutableRefObject

isRunningScriptRef.current = true

notificationTimeoutRef.current = window.setTimeout(() => {
dispatch(
actions.query.setActiveNotification({
type: NotificationType.LOADING,
query: `${activeBufferRef.current.label}@${0}-${0}`,
content: (
<Box gap="1rem" align="center">
<Text color="foreground">Running queries...</Text>
</Box>
),
createdAt: new Date(),
}),
)
notificationTimeoutRef.current = null
}, 1000)
const startTime = Date.now()

const queries = queriesToRun ?? getAllQueries(editorRef.current)
const queries = queriesToRun ?? getAllQueries(editor)
let lastQuery: Request | undefined
let individualQueryResults: Array<IndividualQueryResult> = []

editor.updateOptions({ readOnly: true })

const startTime = Date.now()
for (let i = 0; i < queries.length; i++) {
const query = queries[i]
editor.revealPositionInCenterIfOutsideViewport({ lineNumber: query.row + 1, column: query.column })
const queryGlyph = editor.getLineDecorations(query.row + 1)?.find(d => d.options.glyphMarginClassName?.includes("cursorQueryGlyph"))
let delta = null
if (queryGlyph) {
delta = editor.createDecorationsCollection([{
range: new monaco.Range(
query.row + 1,
1,
query.row + 1,
1
),
options: {
isWholeLine: false,
glyphMarginClassName: 'cursorQueryGlyph loading-glyph',
},
}])
}
const result = await runIndividualQuery(query, i === queries.length - 1)
if (delta) {
delta.clear()
}
individualQueryResults.push(result)
if (result.success) {
successfulQueries++
Expand All @@ -982,11 +1013,6 @@ const MonacoEditor = ({ executionRefs }: { executionRefs: React.MutableRefObject

const duration = (Date.now() - startTime) * 1e6

if (notificationTimeoutRef.current) {
window.clearTimeout(notificationTimeoutRef.current)
notificationTimeoutRef.current = null
}

const lastResult = individualQueryResults[individualQueryResults.length - 1]
if (lastResult && lastResult.result?.type === QuestDB.Type.DQL) {
dispatch(actions.query.setResult(lastResult.result))
Expand All @@ -1009,17 +1035,15 @@ const MonacoEditor = ({ executionRefs }: { executionRefs: React.MutableRefObject
dispatch(actions.query.addNotification({ ...notification!, updateActiveNotification: false }, activeBuffer.id as number))
})

if (editorRef.current && lastQuery) {
const position = {
lineNumber: lastQuery.row + 1,
column: lastQuery.column
}
editorRef.current.focus()
editorRef.current.revealPosition(position)
if (runningAllQueries) {
editorRef.current.setPosition(position, "script")
}
const lastFailureIndex = individualQueryResults.reduce((acc, result, index) => result.success ? acc : index, -1)
if (lastFailureIndex !== -1) {
editor.setPosition({
lineNumber: queries[lastFailureIndex].row + 1,
column: queries[lastFailureIndex].column,
}, "script")
}
editor.focus()
editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!)

const completedGracefully = queries.length === individualQueryResults.length
if (completedGracefully || (failedQueries > 0 && stopAfterFailureRef.current && runningAllQueries)) {
Expand All @@ -1046,6 +1070,7 @@ const MonacoEditor = ({ executionRefs }: { executionRefs: React.MutableRefObject
isRunningScriptRef.current = false
scriptStopRef.current = false
stopAfterFailureRef.current = true
editor.updateOptions({ readOnly: false })
}

useEffect(() => {
Expand Down Expand Up @@ -1099,6 +1124,7 @@ const MonacoEditor = ({ executionRefs }: { executionRefs: React.MutableRefObject
const isRunningExplain = running === RunningType.EXPLAIN

if (request?.query) {
editorRef.current?.updateOptions({ readOnly: true })
const parentQuery = request.query
const parentQueryKey = createQueryKeyFromRequest(editorRef.current, request)
const originalQueryText = request.selection ? request.selection.queryText : request.query
Expand Down Expand Up @@ -1310,6 +1336,7 @@ const MonacoEditor = ({ executionRefs }: { executionRefs: React.MutableRefObject
if (monacoRef?.current && editorRef?.current) {
applyGlyphsAndLineMarkings(monacoRef.current, editorRef.current)
}
editorRef.current?.updateOptions({ readOnly: !!request })
}, [request])

const setCompletionProvider = async () => {
Expand Down