Skip to content

Commit

Permalink
Merge pull request #420 from HarperDB/stage
Browse files Browse the repository at this point in the history
[v4.8.0] Create Dynamic Columns from Dynamic Data & Form Field Fixes
  • Loading branch information
BboyAkers authored Nov 20, 2024
2 parents 244afa3 + cdf4075 commit 3add1b7
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 35 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "harperdb-studio",
"version": "4.7.1",
"version": "4.8.0",
"description": "A UI for HarperDB",
"deploymentUrl": "studio.harperdb.io",
"private": true,
Expand Down
7 changes: 5 additions & 2 deletions src/assets/styles/components/_react-table.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
min-width: 100px;
white-space: nowrap;

&.disabled-column {
background-color: $lighter-grey-overlay !important;
cursor: not-allowed;
}

&.sorted {
&.desc {
box-shadow: inset 0 -2px 0 0 $lighter-grey-overlay;
Expand All @@ -26,7 +31,6 @@
&.asc {
box-shadow: inset 0 2px 0 0 $lighter-grey-overlay;
}

}
}
}
Expand Down Expand Up @@ -129,7 +133,6 @@
}
}


.paginator {
align-items: center;
display: flex;
Expand Down
3 changes: 3 additions & 0 deletions src/components/auth/SignUp.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function SignUp() {
<Input
id="firstname"
name="fname"
maxLength={40}
required
autoComplete="given-name"
type="text"
Expand All @@ -59,6 +60,7 @@ function SignUp() {
<Input
id="lastname"
name="lname"
maxLength={40}
required
autoComplete="family-name"
type="text"
Expand All @@ -74,6 +76,7 @@ function SignUp() {
<Input
id="email"
autoComplete="email"
maxLength={40}
name="email"
required
className="mb-2"
Expand Down
7 changes: 5 additions & 2 deletions src/components/instance/browse/BrowseDatatable.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ let controller;
let controller2;
let controller3;

function BrowseDatatable({ tableState, setTableState, activeTable }) {
function BrowseDatatable({ tableState, setTableState, activeTable, tableDescriptionAttributes }) {
const navigate = useNavigate();
const { compute_stack_id, schema, table, customer_id } = useParams();
const auth = useStoreState(instanceState, (s) => s.auth);
Expand Down Expand Up @@ -65,7 +65,7 @@ function BrowseDatatable({ tableState, setTableState, activeTable }) {
}
controller = new AbortController();
controller2 = new AbortController();
const { newData, newTotalRecords, newTotalPages, newEntityAttributes, hashAttribute, dataTableColumns, error } = await getTableData({
const { newData, newTotalRecords, newTotalPages, newEntityAttributes, hashAttribute, dataTableColumns, dynamicAttributesFromDataTable, error } = await getTableData({
schema,
table,
filtered: tableState.filtered,
Expand Down Expand Up @@ -103,6 +103,7 @@ function BrowseDatatable({ tableState, setTableState, activeTable }) {
newEntityAttributes,
hashAttribute,
dataTableColumns,
dynamicAttributesFromDataTable,
error,
});
}
Expand Down Expand Up @@ -146,6 +147,7 @@ function BrowseDatatable({ tableState, setTableState, activeTable }) {
<DataTable
manual
columns={tableState.dataTableColumns || []}
tableDescriptionAttributes={tableDescriptionAttributes}
data={tableState.tableData || []}
error={tableState.error}
currentPage={tableState.page}
Expand All @@ -154,6 +156,7 @@ function BrowseDatatable({ tableState, setTableState, activeTable }) {
showFilter={tableState.showFilter}
sorted={tableState.sorted.length ? tableState.sorted : [{ id: tableState.hashAttribute, desc: false }]}
loading={loading && !tableState.autoRefresh}
dynamicAttributesFromDataTable={tableState.dynamicAttributesFromDataTable}
onFilteredChange={(value) => {
setTableState({ ...tableState, page: 0, filtered: value });
}}
Expand Down
30 changes: 19 additions & 11 deletions src/components/instance/browse/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,32 +64,40 @@ function BrowseIndex() {
const structure = useStoreState(instanceState, (s) => s.structure);
const [entities, setEntities] = useState({ schemas: [], tables: [], activeTable: false });
const [tableState, setTableState] = useState(defaultTableState);
const [tableDescription, setTableDescription] = useState(null);
const baseUrl = `/o/${customer_id}/i/${compute_stack_id}/browse`;
const showForm = instanceAuths[compute_stack_id]?.super || instanceAuths[compute_stack_id]?.structure === true;
const showTableForm = showForm || (instanceAuths[compute_stack_id]?.structure && instanceAuths[compute_stack_id]?.structure?.includes(schema));
const emptyPromptMessage = showForm
? `Please ${(schema && entities.tables && !entities.tables.length) || !entities.schemas.length ? 'create' : 'choose'} a ${
schema ? 'table' : `${versionAsFloat >= 4.2 ? 'database' : 'schema'}`
}`
? `Please ${(schema && entities.tables && !entities.tables.length) || !entities.schemas.length ? 'create' : 'choose'} a
${schema ? 'table' : `${versionAsFloat >= 4.2 ? 'database' : 'schema'}`}`
: "This user has not been granted access to any tables. A super-user must update this user's role.";
const [hasHashAttr, setHasHashAttr] = useState(true);

const syncInstanceStructure = () => {
buildInstanceStructure({ auth, url });
};

const checkForHashAttribute = () => {
async function check() {
if (table) {
const fetchDescribeTable = async () => {
if (table) {
try {
const result = await describeTable({ auth, url, schema, table });
setHasHashAttr(Boolean(result.hash_attribute));
setTableDescription(result);
} catch (e) {
addError(e);
}
}

check();
};

useEffect(checkForHashAttribute, [auth, url, schema, table]);
useEffect(() => {
fetchDescribeTable();
}, [auth, url, schema, table]);

Check warning on line 94 in src/components/instance/browse/index.js

View workflow job for this annotation

GitHub Actions / publishProductionStudio

React Hook useEffect has a missing dependency: 'fetchDescribeTable'. Either include it or remove the dependency array

useEffect(() => {
if (tableDescription) {
setHasHashAttr(Boolean(tableDescription.hash_attribute));
}
}, [tableDescription]);

const validate = () => {
if (structure) {
Expand Down Expand Up @@ -153,7 +161,7 @@ function BrowseIndex() {
) : schema && table && action && entities.activeTable ? (
<JSONEditor newEntityAttributes={tableState.newEntityAttributes} hashAttribute={tableState.hashAttribute} />
) : schema && table && entities.activeTable ? (
<DataTable activeTable={entities.activeTable} tableState={tableState} setTableState={setTableState} />
<DataTable activeTable={entities.activeTable} tableDescriptionAttributes={tableDescription?.attributes} tableState={tableState} setTableState={setTableState} />
) : schema && table && !hasHashAttr ? (
<NoPrimaryKeyMessage />
) : (
Expand Down
6 changes: 4 additions & 2 deletions src/components/shared/DataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ function DataTable({
onRowClick,
sorted,
loading,
dynamicAttributesFromDataTable,
tableDescriptionAttributes,
manual = false,
}) {
const { headerGroups, page, rows, prepareRow, state, setAllFilters, canPreviousPage, canNextPage, pageOptions, pageCount, gotoPage, nextPage, previousPage, setPageSize } =
Expand Down Expand Up @@ -110,8 +112,8 @@ function DataTable({

return (
<ErrorBoundary onError={(err, componentStack) => addError({ error: { message: err.message, componentStack } })} FallbackComponent={ErrorFallback}>
<div className="react-table-scroller">
<DataTableHeader headerGroups={headerGroups} onSortedChange={onSortedChange} sorted={sorted} showFilter={showFilter} />
<div role="table" aria-label="Table Data" className="react-table-scroller">
<DataTableHeader headerGroups={headerGroups} tableDescriptionAttributes={tableDescriptionAttributes} onSortedChange={onSortedChange} sorted={sorted} showFilter={showFilter} dynamicAttributesFromDataTable={dynamicAttributesFromDataTable} />
{loading || localLoading ? (
<div className="centered text-center">
<i className="fa fa-spinner fa-spin" />
Expand Down
47 changes: 32 additions & 15 deletions src/components/shared/DataTableHeader.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
import React from 'react';
import { Row, Col } from 'reactstrap';

const DataTableHeader = ({ headerGroups, onSortedChange, sorted, showFilter }) =>
headerGroups.map((headerGroup) => {
const DataTableHeader = ({ headerGroups, onSortedChange, sorted, showFilter, dynamicAttributesFromDataTable, tableDescriptionAttributes }) => {

const isIndexedAttribute = (columnId) => {
let isIndexed = false;
tableDescriptionAttributes.forEach((attr) => {
if (attr.attribute === columnId && (attr.is_primary_key || attr.indexed)) {
isIndexed = true;
}
});
return isIndexed;
}

return headerGroups.map((headerGroup) => {
const { key, ...rest } = headerGroup.getHeaderGroupProps();
return (
<div key={key} {...rest}>
<Row className="header g-0">
{headerGroup.headers.map((column) => (
<Col
key={column.id}
onClick={() => onSortedChange([{ id: column.id, desc: sorted[0]?.id === column.id ? !sorted[0]?.desc : false }])}
className={`${sorted[0]?.id === column.id ? 'sorted' : ''} ${sorted[0]?.desc ? 'desc' : 'asc'} ${column.id.indexOf('hdb-narrow') !== -1 ? 'action' : ''} px-1`}
onClick={() => {
if (!dynamicAttributesFromDataTable.includes(column.id) && isIndexedAttribute(column.id)) {
onSortedChange([{ id: column.id, desc: sorted[0]?.id === column.id ? !sorted[0]?.desc : false }])
}
}}
className={`${sorted[0]?.id === column.id ? 'sorted' : ''} ${sorted[0]?.desc ? 'desc' : 'asc'} ${column.id.indexOf('hdb-narrow') !== -1 ? 'action' : ''} px-1 ${!dynamicAttributesFromDataTable.includes(column.id) && isIndexedAttribute(column.id) ? '' : 'disabled-column'}`}
>
<div className="text-renderer">{column.render('Header')}</div>
</Col>
))}
</Row>
{showFilter && (
<Row className="filter g-0">
{headerGroup.headers.map((column) => (
<Col key={column.id} className={column.id.indexOf('hdb-narrow') !== -1 ? 'action' : ''}>
{column.render('Filter')}
</Col>
))}
</Row>
)}
</div>
{
showFilter && (
<Row className="filter g-0">
{headerGroup.headers.map((column) => (
<Col key={column.id} className={column.id.indexOf('hdb-narrow') !== -1 ? 'action' : ''}>
{column.render('Filter')}
</Col>
))}
</Row>
)
}
</div >
);
});

}
export default DataTableHeader;
24 changes: 22 additions & 2 deletions src/functions/instance/getTableData.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ import describeTable from '../api/instance/describeTable';
import searchByValue from '../api/instance/searchByValue';
import searchByConditions from '../api/instance/searchByConditions';

const getAttributesFromTableData = (tableData, existingAttributes) => {
if (existingAttributes.length >= 8) return [];
const existing = new Map(existingAttributes.map((value, index) => [value, index]));
const extra = new Map();
for (const dataRow of tableData) {
for (const key of Object.keys(dataRow)) {
if (!existing.has(key)) {
const count = extra.get(key) || 0;
extra.set(key, count + 1);
}
}
}
return Array.from(extra).sort(([, a], [, b]) => b - a).map(([key]) => key).slice(0, 8 - existingAttributes.length);
}

export default async ({ schema, table, filtered, pageSize, onlyCached, sorted, page, auth, url, signal, signal2 }) => {
let fetchError = false;
let newTotalRecords = 0;
Expand All @@ -10,6 +25,7 @@ export default async ({ schema, table, filtered, pageSize, onlyCached, sorted, p
let allAttributes = false;
let hashAttribute = false;
let get_attributes = ['*'];
let dynamicAttributesFromDataTable = [];
const offset = page * pageSize;

try {
Expand Down Expand Up @@ -76,9 +92,12 @@ export default async ({ schema, table, filtered, pageSize, onlyCached, sorted, p
}
}

// sort columns, but keep primary key / hash attribute first, and created and updated last.
dynamicAttributesFromDataTable = getAttributesFromTableData(newData, allAttributes)
allAttributes.push(...dynamicAttributesFromDataTable);

// Keeps primary key / hash attribute first, and created and updated last.
// NOTE: __created__ and __updated__ might not exist in the schema, only include if they exist.
const orderedColumns = allAttributes.filter((a) => ![hashAttribute, '__createdtime__', '__updatedtime__'].includes(a)).sort();
const orderedColumns = allAttributes.filter((a) => ![hashAttribute, '__createdtime__', '__updatedtime__'].includes(a))
const newEntityAttributes = orderedColumns.reduce((ac, a) => ({ ...ac, [a]: null }), {});

if (allAttributes.includes('__createdtime__')) orderedColumns.push('__createdtime__');
Expand All @@ -97,5 +116,6 @@ export default async ({ schema, table, filtered, pageSize, onlyCached, sorted, p
hashAttribute,
dataTableColumns,
error: fetchError === 'table' ? `You are not authorized to view ${schema}:${table}` : fetchError,
dynamicAttributesFromDataTable
};
};

0 comments on commit 3add1b7

Please sign in to comment.