Skip to content

Commit

Permalink
feat: SQL query editor
Browse files Browse the repository at this point in the history
  • Loading branch information
nextchamp-saqib committed Nov 14, 2024
1 parent 784d572 commit df84790
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 57 deletions.
30 changes: 30 additions & 0 deletions frontend/src2/query/Query.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { useMagicKeys, whenever } from '@vueuse/core'
import { onBeforeUnmount, provide } from 'vue'
import { WorkbookQuery } from '../types/workbook.types'
import NativeQueryEditor from './components/NativeQueryEditor.vue'
import useQuery from './query'
import QueryBuilder from './components/QueryBuilder.vue'
const props = defineProps<{ query: WorkbookQuery }>()
const query = useQuery(props.query)
provide('query', query)
window.query = query
query.execute()
const keys = useMagicKeys()
const cmdZ = keys['Meta+Z']
const cmdShiftZ = keys['Meta+Shift+Z']
const stopUndoWatcher = whenever(cmdZ, () => query.canUndo() && query.history.undo())
const stopRedoWatcher = whenever(cmdShiftZ, () => query.canRedo() && query.history.redo())
onBeforeUnmount(() => {
stopUndoWatcher()
stopRedoWatcher()
})
</script>

<template>
<QueryBuilder v-if="query.doc.is_builder_query" />
<NativeQueryEditor v-else-if="query.doc.is_native_query" />
</template>
44 changes: 0 additions & 44 deletions frontend/src2/query/QueryBuilder.vue

This file was deleted.

81 changes: 81 additions & 0 deletions frontend/src2/query/components/NativeQueryEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { useTimeAgo } from '@vueuse/core'
import { LoadingIndicator } from 'frappe-ui'
import { Play, RefreshCw } from 'lucide-vue-next'
import { computed, inject, ref } from 'vue'
import Code from '../../components/Code.vue'
import DataTable from '../../components/DataTable.vue'
import { Query } from '../query'
const query = inject<Query>('query')!
const sql = ref<string>(query.getSQLQuery())
function execute() {
query.setSQLQuery(sql.value, 'demo_data')
}
const columns = computed(() => query.result.columns)
const rows = computed(() => query.result.formattedRows)
const previewRowCount = computed(() => query.result.rows.length.toLocaleString())
const totalRowCount = computed(() =>
query.result.totalRowCount ? query.result.totalRowCount.toLocaleString() : ''
)
</script>

<template>
<div class="flex flex-1 flex-col gap-4 overflow-hidden p-4">
<div class="relative flex h-[55%] w-full flex-col rounded border">
<div class="flex-1">
<Code v-model="sql" />
</div>

<div class="absolute bottom-2 left-10">
<Button class="h-8 w-8 bg-white shadow" variant="ghost" @click="execute">
<template #icon>
<Play class="h-4 w-4 text-gray-700" stroke-width="1.5" />
</template>
</Button>
</div>
</div>
<div
v-show="query.result.executedSQL"
class="tnum flex flex-shrink-0 items-center gap-2 text-sm text-gray-600"
>
<div class="h-2 w-2 rounded-full bg-green-500"></div>
<div>
<span v-if="query.result.timeTaken == -1"> Fetched from cache </span>
<span v-else> Fetched in {{ query.result.timeTaken }}s </span>
<span> {{ useTimeAgo(query.result.lastExecutedAt).value }} </span>
</div>
</div>
<div class="relative flex w-full flex-1 flex-col overflow-hidden rounded border">
<div
v-if="query.executing"
class="absolute top-10 z-10 flex w-full items-center justify-center rounded bg-gray-50/30 backdrop-blur-sm"
>
<LoadingIndicator class="h-8 w-8 text-gray-700" />
</div>

<DataTable :columns="columns" :rows="rows" :on-export="query.downloadResults">
<template #footer-left>
<div class="tnum flex items-center gap-2 text-sm text-gray-600">
<span> Showing {{ previewRowCount }} of </span>
<span v-if="!totalRowCount" class="inline-block">
<Tooltip text="Load Count">
<RefreshCw
v-if="!query.fetchingCount"
class="h-3.5 w-3.5 cursor-pointer transition-all hover:text-gray-800"
stroke-width="1.5"
@click="query.fetchResultCount"
/>
<LoadingIndicator v-else class="h-3.5 w-3.5 text-gray-600" />
</Tooltip>
</span>
<span v-else> {{ totalRowCount }} </span>
rows
</div>
</template>
</DataTable>
</div>
</div>
</template>
29 changes: 29 additions & 0 deletions frontend/src2/query/components/QueryBuilder.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { inject } from 'vue'
import { Query } from '../query'
import QueryBuilderSourceSelector from './QueryBuilderSourceSelector.vue'
import QueryBuilderTable from './QueryBuilderTable.vue'
import QueryBuilderToolbar from './QueryBuilderToolbar.vue'
import QueryInfo from './QueryInfo.vue'
import QueryOperations from './QueryOperations.vue'
const query = inject<Query>('query')!
</script>

<template>
<div class="flex flex-1 overflow-hidden">
<div class="relative flex h-full flex-1 flex-col gap-3 overflow-hidden p-4">
<QueryBuilderSourceSelector v-if="!query.doc.operations.length" />
<template v-else>
<QueryBuilderToolbar></QueryBuilderToolbar>
<QueryBuilderTable></QueryBuilderTable>
</template>
</div>
<div
class="relative z-[1] flex h-full w-[19rem] flex-shrink-0 flex-col overflow-y-auto bg-white"
>
<QueryInfo />
<QueryOperations />
</div>
</div>
</template>
17 changes: 11 additions & 6 deletions frontend/src2/query/components/QueryBuilderSourceSelector.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { DatabaseZap, Table } from 'lucide-vue-next'
import { DatabaseZap } from 'lucide-vue-next'
import { inject, ref } from 'vue'
import { Query } from '../query'
import SourceSelectorDialog from './source_selector/SourceSelectorDialog.vue'
Expand All @@ -9,14 +9,19 @@ const showSourceSelectorDialog = ref(true)
</script>

<template>
<div class="flex h-full w-full items-center justify-center bg-gray-50">
<div class="flex h-full w-full items-center justify-center">
<div class="flex items-center gap-4">
<div
class="flex h-[18rem] w-[24rem] flex-col items-center justify-center rounded bg-white px-8 text-center shadow-sm"
class="flex flex-col items-center justify-center gap-2 rounded border border-dashed border-gray-300 p-8 text-center"
>
<DatabaseZap class="mb-2 h-12 w-12 text-gray-400" stroke-width="1.5" />
<span class="px-12 text-sm text-gray-600">
Select a source table to start building your query
<div class="rounded-full bg-orange-50 p-3">
<DatabaseZap class="h-5 w-5 text-orange-500/70" stroke-width="1.5" />
</div>
<p class="font-medium">No Table Selected</p>
<span class="text-sm leading-4 text-gray-600">
Select a source table to start building your query.
<br />
You can also select a query as a source.
</span>
<Button class="mt-2" variant="outline" @click="showSourceSelectorDialog = true">
Open Selector
Expand Down
15 changes: 14 additions & 1 deletion frontend/src2/query/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,15 @@ import {
SelectArgs,
Source,
SourceArgs,
SQL,
SQLArgs,
Summarize,
SummarizeArgs,
Table,
TableArgs,
Union,
UnionArgs,
} from '../types/query.types'
import { Query } from './query'

export const table = (args: Partial<TableArgs>): Table => ({
type: 'table',
Expand Down Expand Up @@ -324,6 +325,17 @@ export const query_operation_types = {
return `${op.expression.expression}`
},
},
sql: {
label: 'SQL',
type: 'sql',
icon: Braces,
color: 'gray',
class: 'text-gray-600 bg-gray-100',
init: (args: SQLArgs): SQL => ({ type: 'sql', ...args }),
getDescription: (op: SQL) => {
return `${op.raw_sql}`
},
},
}

export const source = query_operation_types.source.init
Expand All @@ -341,3 +353,4 @@ export const pivot_wider = query_operation_types.pivot_wider.init
export const order_by = query_operation_types.order_by.init
export const limit = query_operation_types.limit.init
export const custom_operation = query_operation_types.custom_operation.init
export const sql = query_operation_types.sql.init
24 changes: 23 additions & 1 deletion frontend/src2/query/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
SelectArgs,
SourceArgs,
SummarizeArgs,
UnionArgs
UnionArgs,
} from '../types/query.types'
import { WorkbookQuery } from '../types/workbook.types'
import {
Expand All @@ -43,6 +43,7 @@ import {
rename,
select,
source,
sql,
summarize,
union,
} from './helpers'
Expand Down Expand Up @@ -105,6 +106,9 @@ export function makeQuery(workbookQuery: WorkbookQuery) {
getColumnsForSelection,
downloadResults,

getSQLQuery,
setSQLQuery,

dimensions: computed(() => ({} as Dimension[])),
measures: computed(() => ({} as Measure[])),
getDimension,
Expand Down Expand Up @@ -696,6 +700,24 @@ export function makeQuery(workbookQuery: WorkbookQuery) {
delete query.doc.calculated_measures[column_name]
}

function getSQLQuery() {
if (!query.doc.is_native_query) return ''
const op = query.doc.operations.find((op) => op.type === 'sql')
if (!op) return ''
return op.raw_sql
}

function setSQLQuery(raw_sql: string, data_source: string) {
query.doc.operations = []
const op = sql({ raw_sql, data_source })
if (raw_sql.trim().length) {
query.doc.operations.push(op)
query.activeOperationIdx = 0
} else {
query.activeOperationIdx = -1
}
}

const originalQuery = copy(workbookQuery)
function reset() {
query.doc = copy(originalQuery)
Expand Down
4 changes: 4 additions & 0 deletions frontend/src2/types/query.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ export type PivotWider = { type: 'pivot_wider' } & PivotWiderArgs
export type CustomOperationArgs = { expression: Expression }
export type CustomOperation = { type: 'custom_operation' } & CustomOperationArgs

export type SQLArgs = { raw_sql: string, data_source: string }
export type SQL = { type: 'sql' } & SQLArgs

export type Operation =
| Source
| Filter
Expand All @@ -156,6 +159,7 @@ export type Operation =
| Limit
| PivotWider
| CustomOperation
| SQL

export type QueryResultRow = Record<string, any>
export type QueryResultColumn = {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src2/types/workbook.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export type WorkbookQuery = {
operations: Operation[]
use_live_connection?: boolean
calculated_measures?: Record<string, Measure>
is_native_query?: boolean
is_script_query?: boolean
is_builder_query?: boolean
}

export type WorkbookChart = {
Expand Down
20 changes: 18 additions & 2 deletions frontend/src2/workbook/WorkbookQuery.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import QueryBuilder from '../query/QueryBuilder.vue'
import Query from '../query/Query.vue'
import { Workbook, workbookKey } from './workbook'
import WorkbookQueryEmptyState from './WorkbookQueryEmptyState.vue'
const props = defineProps<{ name?: string; index: number | string }>()
const workbook = inject<Workbook>(workbookKey)
const activeQuery = computed(() => workbook?.doc.queries[Number(props.index)])
const typeIsSet = computed(
() =>
activeQuery.value?.is_native_query ||
activeQuery.value?.is_script_query ||
activeQuery.value?.is_builder_query
)
function setQueryType(interfaceType: 'query-builder' | 'sql-editor' | 'script-editor') {
if (!activeQuery.value) return
activeQuery.value.is_native_query = interfaceType === 'sql-editor'
activeQuery.value.is_script_query = interfaceType === 'script-editor'
activeQuery.value.is_builder_query = interfaceType === 'query-builder'
}
</script>

<template>
<QueryBuilder v-if="activeQuery" :key="activeQuery.name" :query="activeQuery" />
<WorkbookQueryEmptyState v-if="activeQuery && !typeIsSet" @select="setQueryType" />
<Query v-if="activeQuery && typeIsSet" :key="activeQuery.name" :query="activeQuery" />
</template>
Loading

0 comments on commit df84790

Please sign in to comment.