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()