diff --git a/daiquiri/core/assets/js/components/Table.js b/daiquiri/core/assets/js/components/Table.js
new file mode 100644
index 00000000..92025ea2
--- /dev/null
+++ b/daiquiri/core/assets/js/components/Table.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import TableHeader from './TableHeader'
+import TablePane from './TablePane'
+
+const Table = ({ columns, rows, params, setParams }) => {
+ return (
+
+ )
+}
+
+Table.defaultProps = {
+ columns: [],
+ rows: {},
+}
+
+Table.propTypes = {
+ columns: PropTypes.array,
+ rows: PropTypes.object,
+ params: PropTypes.object.isRequired,
+ setParams: PropTypes.func.isRequired
+}
+
+export default Table
diff --git a/daiquiri/core/assets/js/components/TableBody.js b/daiquiri/core/assets/js/components/TableBody.js
new file mode 100644
index 00000000..ae583ed9
--- /dev/null
+++ b/daiquiri/core/assets/js/components/TableBody.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+const TableBody = ({ columns, rows }) => {
+ return (
+
+ {
+ rows.results && rows.results.map((row, rowIndex) => (
+
+ {
+ columns.map((column, columnIndex) => (
+
+
+ {row[columnIndex]}
+
+ |
+ ))
+ }
+
+ ))
+ }
+
+ )
+}
+
+TableBody.propTypes = {
+ columns: PropTypes.array.isRequired,
+ rows: PropTypes.object.isRequired
+}
+
+export default TableBody
diff --git a/daiquiri/core/assets/js/components/TableHead.js b/daiquiri/core/assets/js/components/TableHead.js
new file mode 100644
index 00000000..942f4867
--- /dev/null
+++ b/daiquiri/core/assets/js/components/TableHead.js
@@ -0,0 +1,69 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classNames from 'classnames'
+
+const TableHead = ({ columns, params, setParams }) => {
+ const tooltips = true
+ const ordering = params.ordering || ''
+
+ const handleOrdering = (column) => {
+ setParams({...params, ordering: (ordering == column.name) ? '-' + column.name : column.name})
+ }
+
+ return (
+
+
+ {
+ columns.map((column, columnIndex) => (
+
+
+
+ {
+ column.label ? (
+ {column.label}
+ ) : (
+ {column.name}
+ )
+ }
+
+ {
+ tooltips && (
+
+ help
+
+ )
+ }
+ handleOrdering(column)}>
+
+ {
+ ordering == '-' + column.name ? (
+ 'expand_less'
+ ) : (
+ 'expand_more'
+ )
+ }
+
+
+ {
+ columnIndex < columns.length -1 && (
+
+ )
+ }
+
+ |
+ ))
+ }
+
+
+ )
+}
+
+TableHead.propTypes = {
+ columns: PropTypes.array.isRequired,
+ params: PropTypes.object.isRequired,
+ setParams: PropTypes.func.isRequired
+}
+
+export default TableHead
diff --git a/daiquiri/core/assets/js/components/TableHeader.js b/daiquiri/core/assets/js/components/TableHeader.js
new file mode 100644
index 00000000..1affb45f
--- /dev/null
+++ b/daiquiri/core/assets/js/components/TableHeader.js
@@ -0,0 +1,47 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+const TableHeader = ({ count, params, setParams }) => {
+
+ const lastPage = count / params.page_size
+ const isFirstPage = params.page == 1
+ const isLastPage = params.page == lastPage
+
+ const handleFirst = () => setParams({...params, page: 1})
+ const handlePrevious = () => setParams({...params, page: params.page - 1})
+ const handleNext = () => setParams({...params, page: params.page + 1})
+ const handleLast = () => setParams({...params, page: lastPage })
+ const handleReset = () => setParams({page: 1, page_size: 10})
+
+ // eslint-disable-next-line react/prop-types
+ const PageItem = ({label, disabled, onClick}) => (
+
+
+ {label}
+
+
+ )
+
+ return (
+
+ )
+}
+
+TableHeader.propTypes = {
+ count: PropTypes.number,
+ params: PropTypes.object.isRequired,
+ setParams: PropTypes.func.isRequired
+}
+
+export default TableHeader
diff --git a/daiquiri/core/assets/js/components/TablePane.js b/daiquiri/core/assets/js/components/TablePane.js
new file mode 100644
index 00000000..0886cf43
--- /dev/null
+++ b/daiquiri/core/assets/js/components/TablePane.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import TableHead from './TableHead'
+import TableBody from './TableBody'
+
+const TablePane = ({ columns, rows, params, setParams }) => {
+
+ return (
+
+ )
+}
+
+TablePane.propTypes = {
+ columns: PropTypes.array.isRequired,
+ rows: PropTypes.object.isRequired,
+ params: PropTypes.object.isRequired,
+ setParams: PropTypes.func.isRequired
+}
+
+export default TablePane
diff --git a/daiquiri/core/assets/scss/base.scss b/daiquiri/core/assets/scss/base.scss
index cfb46970..bb16f85a 100644
--- a/daiquiri/core/assets/scss/base.scss
+++ b/daiquiri/core/assets/scss/base.scss
@@ -10,5 +10,6 @@ $material-symbols-font-path: '~material-symbols/';
@import 'fonts';
@import 'footer';
@import 'layout';
+@import 'table';
@import 'typography';
@import 'variables';
diff --git a/daiquiri/core/assets/scss/table.scss b/daiquiri/core/assets/scss/table.scss
new file mode 100644
index 00000000..beb28ba3
--- /dev/null
+++ b/daiquiri/core/assets/scss/table.scss
@@ -0,0 +1,91 @@
+.dq-table {
+ .dq-table-header {
+ display: flex;
+ gap: 5px;
+
+ .page-link {
+ cursor: pointer;
+ }
+ }
+
+ .dq-table-pane {
+ border: 1px solid var(--bs-border-color);
+ border-radius: 4px;
+ width: auto;
+ overflow-x: auto;
+ table-layout: fixed;
+ width: 100%;
+
+ table.table {
+ margin: 0;
+ table-layout: fixed !important;
+
+ th,
+ td {
+ width: 300px;
+ padding: 0;
+ border-right: 1px solid var(--bs-border-color);
+
+ &:last-child,
+ &:last-child {
+ border-right: none;
+ }
+ }
+ tr {
+ &.selected {
+ background-color: var(--dq-table-active-bg);
+ }
+ }
+ th.dq-table-check {
+ width: 32px;
+
+ @include media-breakpoint-up(md) {
+ width: 40px;
+ }
+ }
+ .dq-table-cell,
+ .dq-table-check {
+ white-space: nowrap;
+ padding: 8px;
+ position: relative;
+ overflow: hidden;
+ }
+ .dq-table-check {
+ white-space: nowrap;
+ padding: 9px 4px 7px 6px;
+ text-align: center;
+ }
+
+ th {
+ .dq-table-cell {
+ display: flex;
+ padding-right: 0;
+
+ .name {
+ overflow: hidden;
+ flex-grow: 1;
+ }
+ .info {
+ .material-symbols-rounded {
+ font-size: 20px !important;
+ }
+ }
+ .order {
+ cursor: pointer;
+
+ .material-symbols-rounded {
+ line-height: 20px !important;
+ }
+ }
+ .handle {
+ width: 10px;
+
+ &:hover {
+ cursor: move;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/daiquiri/query/assets/js/query/api/QueryApi.js b/daiquiri/query/assets/js/query/api/QueryApi.js
index fdfe2a0f..9cd618b1 100644
--- a/daiquiri/query/assets/js/query/api/QueryApi.js
+++ b/daiquiri/query/assets/js/query/api/QueryApi.js
@@ -35,6 +35,14 @@ class QueryApi extends BaseApi {
return this.get(`/query/api/jobs/${id}/`)
}
+ static fetchJobColumns(id, params) {
+ return this.get(`/query/api/jobs/${id}/columns/?${encodeParams(params)}`)
+ }
+
+ static fetchJobRows(id, params) {
+ return this.get(`/query/api/jobs/${id}/rows/?${encodeParams(params)}`)
+ }
+
static fetchUserSchema(params) {
return this.get(`/query/api/jobs/tables/?${encodeParams(params)}`)
}
diff --git a/daiquiri/query/assets/js/query/components/JobResults.js b/daiquiri/query/assets/js/query/components/JobResults.js
index 5ca7e095..31ea3cfe 100644
--- a/daiquiri/query/assets/js/query/components/JobResults.js
+++ b/daiquiri/query/assets/js/query/components/JobResults.js
@@ -1,8 +1,27 @@
-import React from 'react'
+import React, { useState } from 'react'
import PropTypes from 'prop-types'
+import Table from '../../../../../core/assets/js/components/Table'
+
+import { useJobColumnsQuery, useJobRowsQuery } from '../hooks/query'
+
const JobResults = ({ job }) => {
- return Results {job.id}
+ const [params, setParams] = useState({
+ page: 1,
+ page_size: 10
+ })
+
+ const { data: columns } = useJobColumnsQuery(job.id, params)
+ const { data: rows } = useJobRowsQuery(job.id, params)
+
+ return (
+
+ )
}
JobResults.propTypes = {
diff --git a/daiquiri/query/assets/js/query/hooks/query.js b/daiquiri/query/assets/js/query/hooks/query.js
index df55b4b4..f1f65217 100644
--- a/daiquiri/query/assets/js/query/hooks/query.js
+++ b/daiquiri/query/assets/js/query/hooks/query.js
@@ -1,5 +1,4 @@
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
-
+import { keepPreviousData, useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import QueryApi from '../api/QueryApi'
const refetchInterval = 4000
@@ -42,6 +41,22 @@ export const useJobQuery = (jobId) => {
})
}
+export const useJobColumnsQuery = (jobId, params) => {
+ return useQuery({
+ queryKey: ['jobColumns', jobId, params],
+ queryFn: () => QueryApi.fetchJobColumns(jobId, params),
+ placeholderData: keepPreviousData
+ })
+}
+
+export const useJobRowsQuery = (jobId, params) => {
+ return useQuery({
+ queryKey: ['jobRows', jobId, params],
+ queryFn: () => QueryApi.fetchJobRows(jobId, params),
+ placeholderData: keepPreviousData
+ })
+}
+
export const useUpdateJobMutation = () => {
const queryClient = useQueryClient()