mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Make details visualization configurable (#7535)
- Added possibility to select visible columns and reordering - Added formatting options as in Table visualization - Set default alignment to left
This commit is contained in:
@@ -1,64 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { map, mapValues, keyBy } from "lodash";
|
|
||||||
import moment from "moment";
|
|
||||||
import { RendererPropTypes } from "@/visualizations/prop-types";
|
|
||||||
import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
|
|
||||||
import Descriptions from "antd/lib/descriptions";
|
|
||||||
import Pagination from "antd/lib/pagination";
|
|
||||||
|
|
||||||
import "./details.less";
|
|
||||||
|
|
||||||
function renderValue(value: any, type: any) {
|
|
||||||
const formats = {
|
|
||||||
date: visualizationsSettings.dateFormat,
|
|
||||||
datetime: visualizationsSettings.dateTimeFormat,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type === "date" || type === "datetime") {
|
|
||||||
if (moment.isMoment(value)) {
|
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
return value.format(formats[type]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "" + value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DetailsRenderer({ data }: any) {
|
|
||||||
const [page, setPage] = useState(0);
|
|
||||||
|
|
||||||
if (!data || !data.rows || data.rows.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const types = mapValues(keyBy(data.columns, "name"), "type");
|
|
||||||
|
|
||||||
// We use columsn to maintain order of columns in the view.
|
|
||||||
const columns = data.columns.map((column: any) => column.name);
|
|
||||||
const row = data.rows[page];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="details-viz">
|
|
||||||
<Descriptions size="small" column={1} bordered>
|
|
||||||
{map(columns, key => (
|
|
||||||
<Descriptions.Item key={key} label={key}>
|
|
||||||
{renderValue(row[key], types[key])}
|
|
||||||
</Descriptions.Item>
|
|
||||||
))}
|
|
||||||
</Descriptions>
|
|
||||||
{data.rows.length > 1 && (
|
|
||||||
<div className="paginator-container">
|
|
||||||
<Pagination
|
|
||||||
showSizeChanger={false}
|
|
||||||
current={page + 1}
|
|
||||||
defaultPageSize={1}
|
|
||||||
total={data.rows.length}
|
|
||||||
onChange={p => setPage(p - 1)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DetailsRenderer.propTypes = RendererPropTypes;
|
|
||||||
31
viz-lib/src/visualizations/details/Editor/ColumnEditor.tsx
Normal file
31
viz-lib/src/visualizations/details/Editor/ColumnEditor.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import SharedColumnEditor from "../../shared/components/ColumnEditor";
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
column: {
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
alignContent?: "left" | "center" | "right";
|
||||||
|
displayAs?: any;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
onChange?: (...args: any[]) => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = OwnProps & typeof ColumnEditor.defaultProps;
|
||||||
|
|
||||||
|
export default function ColumnEditor({ column, onChange }: Props) {
|
||||||
|
return (
|
||||||
|
<SharedColumnEditor
|
||||||
|
column={column}
|
||||||
|
onChange={onChange}
|
||||||
|
variant="details"
|
||||||
|
showSearch={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnEditor.defaultProps = {
|
||||||
|
onChange: (...args: any[]) => {},
|
||||||
|
};
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import React from "react";
|
||||||
|
import enzyme from "enzyme";
|
||||||
|
|
||||||
|
import getOptions from "../getOptions";
|
||||||
|
import ColumnsSettings from "./ColumnsSettings";
|
||||||
|
|
||||||
|
function findByTestID(wrapper: any, testId: any) {
|
||||||
|
return wrapper.find(`[data-test="${testId}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mount(options: any, done: any) {
|
||||||
|
const data = {
|
||||||
|
columns: [
|
||||||
|
{ name: "id", type: "integer" },
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "created_at", type: "datetime" },
|
||||||
|
],
|
||||||
|
rows: [{ id: 1, name: "test", created_at: "2023-01-01T00:00:00Z" }],
|
||||||
|
};
|
||||||
|
options = getOptions(options, data);
|
||||||
|
return enzyme.mount(
|
||||||
|
<ColumnsSettings
|
||||||
|
visualizationName="Details"
|
||||||
|
data={data}
|
||||||
|
options={options}
|
||||||
|
onOptionsChange={changedOptions => {
|
||||||
|
expect(changedOptions).toMatchSnapshot();
|
||||||
|
done();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Visualizations -> Details -> Editor -> Columns Settings", () => {
|
||||||
|
test("Toggles column visibility", done => {
|
||||||
|
const el = mount({}, done);
|
||||||
|
|
||||||
|
findByTestID(el, "Details.Column.id.Visibility")
|
||||||
|
.last()
|
||||||
|
.simulate("click");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Changes column title", done => {
|
||||||
|
const el = mount({}, done);
|
||||||
|
findByTestID(el, "Details.Column.name.Name")
|
||||||
|
.last()
|
||||||
|
.simulate("click"); // expand settings
|
||||||
|
|
||||||
|
findByTestID(el, "Details.Column.name.Title")
|
||||||
|
.last()
|
||||||
|
.simulate("change", { target: { value: "Full Name" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Changes column alignment", done => {
|
||||||
|
const el = mount({}, done);
|
||||||
|
findByTestID(el, "Details.Column.id.Name")
|
||||||
|
.last()
|
||||||
|
.simulate("click"); // expand settings
|
||||||
|
|
||||||
|
findByTestID(el, "Details.Column.id.TextAlignment")
|
||||||
|
.last()
|
||||||
|
.find('[data-test="TextAlignmentSelect.Center"] input')
|
||||||
|
.simulate("change", { target: { checked: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Changes column description", done => {
|
||||||
|
const el = mount({}, done);
|
||||||
|
findByTestID(el, "Details.Column.name.Name")
|
||||||
|
.last()
|
||||||
|
.simulate("click"); // expand settings
|
||||||
|
|
||||||
|
findByTestID(el, "Details.Column.name.Description")
|
||||||
|
.last()
|
||||||
|
.simulate("change", { target: { value: "User full name" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Changes column display type", done => {
|
||||||
|
const el = mount({}, done);
|
||||||
|
findByTestID(el, "Details.Column.created_at.Name")
|
||||||
|
.last()
|
||||||
|
.simulate("click"); // expand settings
|
||||||
|
|
||||||
|
findByTestID(el, "Details.Column.created_at.DisplayAs")
|
||||||
|
.last()
|
||||||
|
.simulate("mouseDown");
|
||||||
|
findByTestID(el, "Details.Column.created_at.DisplayAs.string")
|
||||||
|
.last()
|
||||||
|
.simulate("click");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Hides multiple columns", done => {
|
||||||
|
const el = mount({}, done);
|
||||||
|
|
||||||
|
findByTestID(el, "Details.Column.id.Visibility")
|
||||||
|
.last()
|
||||||
|
.simulate("click");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from "react";
|
||||||
|
import SharedColumnsSettings from "../../shared/components/ColumnsSettings";
|
||||||
|
import { EditorPropTypes } from "@/visualizations/prop-types";
|
||||||
|
|
||||||
|
export default function ColumnsSettings({ options, onOptionsChange, data }: any) {
|
||||||
|
return (
|
||||||
|
<SharedColumnsSettings
|
||||||
|
options={options}
|
||||||
|
onOptionsChange={onOptionsChange}
|
||||||
|
variant="details"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnsSettings.propTypes = EditorPropTypes;
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Visualizations -> Details -> Editor -> Columns Settings Changes column alignment 1`] = `
|
||||||
|
Object {
|
||||||
|
"columns": Array [
|
||||||
|
Object {
|
||||||
|
"alignContent": "center",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "number",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "id",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": "0,0",
|
||||||
|
"order": 100000,
|
||||||
|
"title": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "string",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "name",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100001,
|
||||||
|
"title": "name",
|
||||||
|
"type": "string",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": "DD/MM/YYYY HH:mm",
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "datetime",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "created_at",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100002,
|
||||||
|
"title": "created_at",
|
||||||
|
"type": "datetime",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Visualizations -> Details -> Editor -> Columns Settings Changes column description 1`] = `
|
||||||
|
Object {
|
||||||
|
"columns": Array [
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "number",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "id",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": "0,0",
|
||||||
|
"order": 100000,
|
||||||
|
"title": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "User full name",
|
||||||
|
"displayAs": "string",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "name",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100001,
|
||||||
|
"title": "name",
|
||||||
|
"type": "string",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": "DD/MM/YYYY HH:mm",
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "datetime",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "created_at",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100002,
|
||||||
|
"title": "created_at",
|
||||||
|
"type": "datetime",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Visualizations -> Details -> Editor -> Columns Settings Changes column display type 1`] = `
|
||||||
|
Object {
|
||||||
|
"columns": Array [
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "number",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "id",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": "0,0",
|
||||||
|
"order": 100000,
|
||||||
|
"title": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "string",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "name",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100001,
|
||||||
|
"title": "name",
|
||||||
|
"type": "string",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": "DD/MM/YYYY HH:mm",
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "string",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "created_at",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100002,
|
||||||
|
"title": "created_at",
|
||||||
|
"type": "datetime",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Visualizations -> Details -> Editor -> Columns Settings Changes column title 1`] = `
|
||||||
|
Object {
|
||||||
|
"columns": Array [
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "number",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "id",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": "0,0",
|
||||||
|
"order": 100000,
|
||||||
|
"title": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "string",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "name",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100001,
|
||||||
|
"title": "Full Name",
|
||||||
|
"type": "string",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": "DD/MM/YYYY HH:mm",
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "datetime",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "created_at",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100002,
|
||||||
|
"title": "created_at",
|
||||||
|
"type": "datetime",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Visualizations -> Details -> Editor -> Columns Settings Hides multiple columns 1`] = `
|
||||||
|
Object {
|
||||||
|
"columns": Array [
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "number",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "id",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": "0,0",
|
||||||
|
"order": 100000,
|
||||||
|
"title": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"visible": false,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "string",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "name",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100001,
|
||||||
|
"title": "name",
|
||||||
|
"type": "string",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": "DD/MM/YYYY HH:mm",
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "datetime",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "created_at",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100002,
|
||||||
|
"title": "created_at",
|
||||||
|
"type": "datetime",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Visualizations -> Details -> Editor -> Columns Settings Toggles column visibility 1`] = `
|
||||||
|
Object {
|
||||||
|
"columns": Array [
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "number",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "id",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": "0,0",
|
||||||
|
"order": 100000,
|
||||||
|
"title": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"visible": false,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "string",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "name",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100001,
|
||||||
|
"title": "name",
|
||||||
|
"type": "string",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"alignContent": "left",
|
||||||
|
"allowHTML": false,
|
||||||
|
"booleanValues": Array [
|
||||||
|
"false",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
"dateTimeFormat": "DD/MM/YYYY HH:mm",
|
||||||
|
"description": "",
|
||||||
|
"displayAs": "datetime",
|
||||||
|
"highlightLinks": false,
|
||||||
|
"imageHeight": "",
|
||||||
|
"imageTitleTemplate": "{{ @ }}",
|
||||||
|
"imageUrlTemplate": "{{ @ }}",
|
||||||
|
"imageWidth": "",
|
||||||
|
"linkOpenInNewTab": true,
|
||||||
|
"linkTextTemplate": "{{ @ }}",
|
||||||
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
|
"name": "created_at",
|
||||||
|
"nullValue": "null",
|
||||||
|
"numberFormat": undefined,
|
||||||
|
"order": 100002,
|
||||||
|
"title": "created_at",
|
||||||
|
"type": "datetime",
|
||||||
|
"visible": true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
33
viz-lib/src/visualizations/details/Editor/editor.less
Normal file
33
viz-lib/src/visualizations/details/Editor/editor.less
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
.details-visualization-editor-columns {
|
||||||
|
.ant-collapse {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-collapse-item {
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
height: 20px;
|
||||||
|
margin-left: -16px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-editor-columns-dragged-item {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-visualization-editor-column {
|
||||||
|
padding-left: 6px;
|
||||||
|
|
||||||
|
.image-dimension-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.image-dimension-selector-spacer {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
viz-lib/src/visualizations/details/Editor/index.tsx
Normal file
9
viz-lib/src/visualizations/details/Editor/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import createTabbedEditor from "@/components/visualizations/editor/createTabbedEditor";
|
||||||
|
|
||||||
|
import ColumnsSettings from "./ColumnsSettings";
|
||||||
|
|
||||||
|
import "./editor.less";
|
||||||
|
|
||||||
|
export default createTabbedEditor([
|
||||||
|
{ key: "Columns", title: "Columns", component: ColumnsSettings },
|
||||||
|
]);
|
||||||
179
viz-lib/src/visualizations/details/Renderer.test.tsx
Normal file
179
viz-lib/src/visualizations/details/Renderer.test.tsx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import React from "react";
|
||||||
|
import enzyme from "enzyme";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
import Renderer from "./Renderer";
|
||||||
|
import getOptions from "./getOptions";
|
||||||
|
|
||||||
|
function mount(data: any, options: any = {}) {
|
||||||
|
options = getOptions(options, data);
|
||||||
|
return enzyme.mount(<Renderer data={data} options={options} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Visualizations -> Details -> Renderer", () => {
|
||||||
|
const sampleData = {
|
||||||
|
columns: [
|
||||||
|
{ name: "id", type: "integer" },
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "created_at", type: "datetime" },
|
||||||
|
{ name: "active", type: "boolean" },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "John Doe",
|
||||||
|
created_at: moment("2023-01-01T12:00:00Z"),
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Jane Smith",
|
||||||
|
created_at: moment("2023-02-01T12:00:00Z"),
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test("Renders all columns when no options provided", () => {
|
||||||
|
const el = mount(sampleData);
|
||||||
|
|
||||||
|
// Check that the component renders with expected data
|
||||||
|
expect(el.text()).toContain("id");
|
||||||
|
expect(el.text()).toContain("name");
|
||||||
|
expect(el.text()).toContain("created_at");
|
||||||
|
expect(el.text()).toContain("active");
|
||||||
|
expect(el.text()).toContain("1"); // id value
|
||||||
|
expect(el.text()).toContain("John Doe"); // name value
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Renders only visible columns", () => {
|
||||||
|
const options = {
|
||||||
|
columns: [
|
||||||
|
{ name: "id", visible: true, order: 0 },
|
||||||
|
{ name: "name", visible: false, order: 1 },
|
||||||
|
{ name: "created_at", visible: true, order: 2 },
|
||||||
|
{ name: "active", visible: false, order: 3 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = mount(sampleData, options);
|
||||||
|
|
||||||
|
// Should show id and created_at, but not name and active
|
||||||
|
expect(el.text()).toContain("id");
|
||||||
|
expect(el.text()).toContain("created_at");
|
||||||
|
expect(el.text()).not.toContain("name");
|
||||||
|
expect(el.text()).not.toContain("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Respects column order", () => {
|
||||||
|
const options = {
|
||||||
|
columns: [
|
||||||
|
{ name: "active", visible: true, order: 0 },
|
||||||
|
{ name: "name", visible: true, order: 1 },
|
||||||
|
{ name: "created_at", visible: true, order: 2 },
|
||||||
|
{ name: "id", visible: true, order: 3 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = mount(sampleData, options);
|
||||||
|
|
||||||
|
// Get all description item labels in order
|
||||||
|
const labels = el.find('.ant-descriptions-item-label').map(node => node.text());
|
||||||
|
|
||||||
|
// Should appear in order: active (0), name (1), created_at (2), id (3)
|
||||||
|
expect(labels).toEqual(['active', 'name', 'created_at', 'id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Uses custom column titles", () => {
|
||||||
|
const options = {
|
||||||
|
columns: [
|
||||||
|
{ name: "id", visible: true, title: "User ID", order: 0 },
|
||||||
|
{ name: "name", visible: true, title: "Full Name", order: 1 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = mount(sampleData, options);
|
||||||
|
|
||||||
|
expect(el.text()).toContain("User ID");
|
||||||
|
expect(el.text()).toContain("Full Name");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Applies text alignment", () => {
|
||||||
|
const options = {
|
||||||
|
columns: [
|
||||||
|
{ name: "id", visible: true, alignContent: "center", order: 0 },
|
||||||
|
{ name: "name", visible: true, alignContent: "right", order: 1 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = mount(sampleData, options);
|
||||||
|
|
||||||
|
// Check that alignment styles are applied
|
||||||
|
const alignedDivs = el.find('div[style]');
|
||||||
|
expect(alignedDivs.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Shows pagination for multiple rows", () => {
|
||||||
|
const el = mount(sampleData);
|
||||||
|
|
||||||
|
// Check that pagination is present - look for pagination elements
|
||||||
|
const paginationElements = el.find('[className*="paginator"]');
|
||||||
|
expect(paginationElements.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Hides pagination for single row", () => {
|
||||||
|
const singleRowData = {
|
||||||
|
...sampleData,
|
||||||
|
rows: [sampleData.rows[0]],
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = mount(singleRowData);
|
||||||
|
|
||||||
|
// Check that pagination is not present for single row
|
||||||
|
const paginationElements = el.find('[className*="paginator"]');
|
||||||
|
expect(paginationElements.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Handles empty data", () => {
|
||||||
|
const emptyData = {
|
||||||
|
columns: [],
|
||||||
|
rows: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const el = mount(emptyData);
|
||||||
|
|
||||||
|
expect(el.html()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Handles null data", () => {
|
||||||
|
// Suppress PropTypes warning for this test
|
||||||
|
const originalError = console.error;
|
||||||
|
console.error = jest.fn();
|
||||||
|
|
||||||
|
// Test the component directly with null data instead of using mount helper
|
||||||
|
const el = enzyme.mount(<Renderer data={null as any} options={{}} />);
|
||||||
|
|
||||||
|
expect(el.html()).toBeNull();
|
||||||
|
|
||||||
|
// Restore console.error
|
||||||
|
console.error = originalError;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Navigates between rows with pagination", () => {
|
||||||
|
const el = mount(sampleData);
|
||||||
|
|
||||||
|
// Check first row is displayed
|
||||||
|
expect(el.text()).toContain("John Doe");
|
||||||
|
expect(el.text()).not.toContain("Jane Smith");
|
||||||
|
|
||||||
|
// Find and click next button
|
||||||
|
const nextButton = el.find('button').filterWhere(n => n.text().includes('Next') || n.prop('aria-label') === 'Next Page');
|
||||||
|
if (nextButton.length > 0) {
|
||||||
|
nextButton.first().simulate("click");
|
||||||
|
|
||||||
|
// Check second row is displayed after state update
|
||||||
|
el.update();
|
||||||
|
expect(el.text()).toContain("Jane Smith");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
82
viz-lib/src/visualizations/details/Renderer.tsx
Normal file
82
viz-lib/src/visualizations/details/Renderer.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState, useMemo } from "react";
|
||||||
|
import { map, filter, sortBy } from "lodash";
|
||||||
|
import { RendererPropTypes } from "@/visualizations/prop-types";
|
||||||
|
import Descriptions from "antd/lib/descriptions";
|
||||||
|
import Pagination from "antd/lib/pagination";
|
||||||
|
import Tooltip from "antd/lib/tooltip";
|
||||||
|
|
||||||
|
import ColumnTypes from "../shared/columns";
|
||||||
|
import "./details.less";
|
||||||
|
|
||||||
|
|
||||||
|
export default function Renderer({ data, options }: any) {
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
|
||||||
|
const visibleColumns = useMemo(() => {
|
||||||
|
if (!options?.columns) return [];
|
||||||
|
|
||||||
|
const columns = sortBy(filter(options.columns, "visible"), "order");
|
||||||
|
|
||||||
|
return columns.map((column: any) => {
|
||||||
|
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||||
|
const ColumnType = ColumnTypes[column.displayAs] || ColumnTypes.string;
|
||||||
|
const Component = ColumnType(column);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
Component,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [options?.columns]);
|
||||||
|
|
||||||
|
if (!data || !data.rows || data.rows.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = data.rows[page];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="details-viz">
|
||||||
|
<Descriptions size="small" column={1} bordered>
|
||||||
|
{map(visibleColumns, column => {
|
||||||
|
const { Component } = column;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Descriptions.Item
|
||||||
|
key={column.name}
|
||||||
|
label={
|
||||||
|
<React.Fragment>
|
||||||
|
{column.description && (
|
||||||
|
<span style={{ paddingRight: 5 }}>
|
||||||
|
<Tooltip placement="top" title={column.description}>
|
||||||
|
<i className="fa fa-info-circle" aria-hidden="true"></i>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{column.title || column.name}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: column.alignContent || "left" }}>
|
||||||
|
<Component row={row} />
|
||||||
|
</div>
|
||||||
|
</Descriptions.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Descriptions>
|
||||||
|
{data.rows.length > 1 && (
|
||||||
|
<div className="paginator-container">
|
||||||
|
<Pagination
|
||||||
|
showSizeChanger={false}
|
||||||
|
current={page + 1}
|
||||||
|
defaultPageSize={1}
|
||||||
|
total={data.rows.length}
|
||||||
|
onChange={p => setPage(p - 1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Renderer.propTypes = RendererPropTypes;
|
||||||
160
viz-lib/src/visualizations/details/getOptions.test.ts
Normal file
160
viz-lib/src/visualizations/details/getOptions.test.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import getOptions from "./getOptions";
|
||||||
|
|
||||||
|
describe("Visualizations -> Details -> getOptions", () => {
|
||||||
|
const sampleData = {
|
||||||
|
columns: [
|
||||||
|
{ name: "id", type: "integer" },
|
||||||
|
{ name: "name", type: "string" },
|
||||||
|
{ name: "created_at", type: "datetime" },
|
||||||
|
{ name: "is_active", type: "boolean" },
|
||||||
|
{ name: "score", type: "float" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
test("Returns default options when no options provided", () => {
|
||||||
|
const result = getOptions({}, sampleData);
|
||||||
|
|
||||||
|
expect(result.columns).toHaveLength(5);
|
||||||
|
expect(result.columns[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "id",
|
||||||
|
type: "integer",
|
||||||
|
displayAs: "number",
|
||||||
|
visible: true,
|
||||||
|
alignContent: "left",
|
||||||
|
title: "id",
|
||||||
|
description: "",
|
||||||
|
allowHTML: false,
|
||||||
|
highlightLinks: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Preserves existing column options", () => {
|
||||||
|
const existingOptions = {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
visible: false,
|
||||||
|
title: "User ID",
|
||||||
|
alignContent: "center",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOptions(existingOptions, sampleData);
|
||||||
|
|
||||||
|
const idColumn = result.columns.find((col: any) => col.name === "id");
|
||||||
|
expect(idColumn).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
visible: false,
|
||||||
|
title: "User ID",
|
||||||
|
alignContent: "center",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sets correct default display types", () => {
|
||||||
|
const result = getOptions({}, sampleData);
|
||||||
|
|
||||||
|
const columnsByName = result.columns.reduce((acc: any, col: any) => {
|
||||||
|
acc[col.name] = col;
|
||||||
|
return acc;
|
||||||
|
}, {} as any);
|
||||||
|
|
||||||
|
expect(columnsByName.id.displayAs).toBe("number");
|
||||||
|
expect(columnsByName.name.displayAs).toBe("string");
|
||||||
|
expect(columnsByName.created_at.displayAs).toBe("datetime");
|
||||||
|
expect(columnsByName.is_active.displayAs).toBe("boolean");
|
||||||
|
expect(columnsByName.score.displayAs).toBe("number");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sets correct default alignments", () => {
|
||||||
|
const result = getOptions({}, sampleData);
|
||||||
|
|
||||||
|
const columnsByName = result.columns.reduce((acc: any, col: any) => {
|
||||||
|
acc[col.name] = col;
|
||||||
|
return acc;
|
||||||
|
}, {} as any);
|
||||||
|
|
||||||
|
expect(columnsByName.id.alignContent).toBe("left");
|
||||||
|
expect(columnsByName.name.alignContent).toBe("left");
|
||||||
|
expect(columnsByName.created_at.alignContent).toBe("left");
|
||||||
|
expect(columnsByName.is_active.alignContent).toBe("left");
|
||||||
|
expect(columnsByName.score.alignContent).toBe("left");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Handles column name type suffixes", () => {
|
||||||
|
const dataWithTypeSuffixes = {
|
||||||
|
columns: [
|
||||||
|
{ name: "user::filter", type: "string" },
|
||||||
|
{ name: "amount__multiFilter", type: "float" },
|
||||||
|
{ name: "::date_field", type: "date" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOptions({}, dataWithTypeSuffixes);
|
||||||
|
|
||||||
|
expect(result.columns[0].title).toBe("user");
|
||||||
|
expect(result.columns[1].title).toBe("amount");
|
||||||
|
expect(result.columns[2].title).toBe("date_field");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Maintains column order from existing options", () => {
|
||||||
|
const existingOptions = {
|
||||||
|
columns: [
|
||||||
|
{ name: "name", order: 0 },
|
||||||
|
{ name: "id", order: 1 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOptions(existingOptions, sampleData);
|
||||||
|
|
||||||
|
expect(result.columns[0].name).toBe("name");
|
||||||
|
expect(result.columns[1].name).toBe("id");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Handles missing columns in existing options", () => {
|
||||||
|
const existingOptions = {
|
||||||
|
columns: [
|
||||||
|
{ name: "id", visible: false },
|
||||||
|
{ name: "nonexistent", visible: true },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getOptions(existingOptions, sampleData);
|
||||||
|
|
||||||
|
// Should include all data columns
|
||||||
|
expect(result.columns).toHaveLength(5);
|
||||||
|
|
||||||
|
// Should preserve settings for existing columns
|
||||||
|
const idColumn = result.columns.find((col: any) => col.name === "id");
|
||||||
|
expect(idColumn.visible).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Includes default format options", () => {
|
||||||
|
const result = getOptions({}, sampleData);
|
||||||
|
|
||||||
|
const column = result.columns[0];
|
||||||
|
expect(column).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
booleanValues: ["false", "true"],
|
||||||
|
imageUrlTemplate: "{{ @ }}",
|
||||||
|
imageTitleTemplate: "{{ @ }}",
|
||||||
|
imageWidth: "",
|
||||||
|
imageHeight: "",
|
||||||
|
linkUrlTemplate: "{{ @ }}",
|
||||||
|
linkTextTemplate: "{{ @ }}",
|
||||||
|
linkTitleTemplate: "{{ @ }}",
|
||||||
|
linkOpenInNewTab: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Handles empty data", () => {
|
||||||
|
const emptyData = { columns: [] };
|
||||||
|
const result = getOptions({}, emptyData);
|
||||||
|
|
||||||
|
expect(result.columns).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
17
viz-lib/src/visualizations/details/getOptions.ts
Normal file
17
viz-lib/src/visualizations/details/getOptions.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import {
|
||||||
|
getDefaultFormatOptions,
|
||||||
|
getColumnsOptions,
|
||||||
|
} from "@/visualizations/shared/columnUtils";
|
||||||
|
|
||||||
|
const DEFAULT_OPTIONS = {};
|
||||||
|
|
||||||
|
|
||||||
|
export default function getOptions(options: any, { columns }: any) {
|
||||||
|
options = { ...DEFAULT_OPTIONS, ...options };
|
||||||
|
options.columns = _.map(getColumnsOptions(columns, options.columns, { alignContent: "left" }), col => ({
|
||||||
|
...getDefaultFormatOptions(col),
|
||||||
|
...col,
|
||||||
|
}));
|
||||||
|
return options;
|
||||||
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import DetailsRenderer from "./DetailsRenderer";
|
import getOptions from "./getOptions";
|
||||||
|
import Renderer from "./Renderer";
|
||||||
const DEFAULT_OPTIONS = {};
|
import Editor from "./Editor";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
type: "DETAILS",
|
type: "DETAILS",
|
||||||
name: "Details View",
|
name: "Details View",
|
||||||
getOptions: (options: any) => ({
|
getOptions,
|
||||||
...DEFAULT_OPTIONS,
|
Renderer,
|
||||||
...options,
|
Editor,
|
||||||
}),
|
|
||||||
Renderer: DetailsRenderer,
|
|
||||||
defaultColumns: 4,
|
defaultColumns: 4,
|
||||||
defaultRows: 2,
|
defaultRows: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
126
viz-lib/src/visualizations/shared/columnUtils.ts
Normal file
126
viz-lib/src/visualizations/shared/columnUtils.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
|
||||||
|
|
||||||
|
const filterTypes = ["filter", "multi-filter", "multiFilter"];
|
||||||
|
|
||||||
|
export function getColumnNameWithoutType(column: any) {
|
||||||
|
let typeSplit;
|
||||||
|
if (column.indexOf("::") !== -1) {
|
||||||
|
typeSplit = "::";
|
||||||
|
} else if (column.indexOf("__") !== -1) {
|
||||||
|
typeSplit = "__";
|
||||||
|
} else {
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = column.split(typeSplit);
|
||||||
|
if (parts[0] === "" && parts.length === 2) {
|
||||||
|
return parts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.includes(filterTypes, parts[1])) {
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColumnContentAlignment(type: any) {
|
||||||
|
return ["integer", "float", "boolean", "date", "datetime"].indexOf(type) >= 0 ? "right" : "left";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultColumnsOptions(columns: any, extraFields = {}) {
|
||||||
|
const displayAs = {
|
||||||
|
integer: "number",
|
||||||
|
float: "number",
|
||||||
|
boolean: "boolean",
|
||||||
|
date: "datetime",
|
||||||
|
datetime: "datetime",
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultFields = {
|
||||||
|
// `string` cell options
|
||||||
|
allowHTML: false,
|
||||||
|
highlightLinks: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return _.map(columns, (col, index) => ({
|
||||||
|
name: col.name,
|
||||||
|
type: col.type,
|
||||||
|
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||||
|
displayAs: displayAs[col.type] || "string",
|
||||||
|
visible: true,
|
||||||
|
order: 100000 + index,
|
||||||
|
title: getColumnNameWithoutType(col.name),
|
||||||
|
alignContent: getColumnContentAlignment(col.type),
|
||||||
|
description: "",
|
||||||
|
...defaultFields,
|
||||||
|
...extraFields,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultFormatOptions(column: any) {
|
||||||
|
const dateTimeFormat = {
|
||||||
|
date: visualizationsSettings.dateFormat || "DD/MM/YYYY",
|
||||||
|
datetime: visualizationsSettings.dateTimeFormat || "DD/MM/YYYY HH:mm",
|
||||||
|
};
|
||||||
|
const numberFormat = {
|
||||||
|
integer: visualizationsSettings.integerFormat || "0,0",
|
||||||
|
float: visualizationsSettings.floatFormat || "0,0.00",
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||||
|
dateTimeFormat: dateTimeFormat[column.type],
|
||||||
|
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||||
|
numberFormat: numberFormat[column.type],
|
||||||
|
nullValue: visualizationsSettings.nullValue,
|
||||||
|
booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
|
||||||
|
// `image` cell options
|
||||||
|
imageUrlTemplate: "{{ @ }}",
|
||||||
|
imageTitleTemplate: "{{ @ }}",
|
||||||
|
imageWidth: "",
|
||||||
|
imageHeight: "",
|
||||||
|
// `link` cell options
|
||||||
|
linkUrlTemplate: "{{ @ }}",
|
||||||
|
linkTextTemplate: "{{ @ }}",
|
||||||
|
linkTitleTemplate: "{{ @ }}",
|
||||||
|
linkOpenInNewTab: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wereColumnsReordered(queryColumns: any, visualizationColumns: any) {
|
||||||
|
queryColumns = _.map(queryColumns, col => col.name);
|
||||||
|
visualizationColumns = _.map(visualizationColumns, col => col.name);
|
||||||
|
|
||||||
|
// Some columns may be removed - so skip them (but keep original order)
|
||||||
|
visualizationColumns = _.filter(visualizationColumns, col => _.includes(queryColumns, col));
|
||||||
|
// Pick query columns that were previously saved with viz (but keep order too)
|
||||||
|
queryColumns = _.filter(queryColumns, col => _.includes(visualizationColumns, col));
|
||||||
|
|
||||||
|
// Both array now have the same size as they both contains only common columns
|
||||||
|
// (in fact, it was an intersection, that kept order of items on both arrays).
|
||||||
|
// Now check for equality item-by-item; if common columns are in the same order -
|
||||||
|
// they were not reordered in editor
|
||||||
|
for (let i = 0; i < queryColumns.length; i += 1) {
|
||||||
|
if (visualizationColumns[i] !== queryColumns[i]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColumnsOptions(columns: any, visualizationColumns: any, extraFields = {}) {
|
||||||
|
const options = getDefaultColumnsOptions(columns, extraFields);
|
||||||
|
|
||||||
|
if (wereColumnsReordered(columns, visualizationColumns)) {
|
||||||
|
visualizationColumns = _.fromPairs(
|
||||||
|
_.map(visualizationColumns, (col, index) => [col.name, _.extend({}, col, { order: index })])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
visualizationColumns = _.fromPairs(_.map(visualizationColumns, col => [col.name, _.omit(col, "order")]));
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(options, col => _.extend(col, visualizationColumns[col.name]));
|
||||||
|
|
||||||
|
return _.sortBy(options, "order");
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import React from "react";
|
||||||
|
import enzyme from "enzyme";
|
||||||
|
|
||||||
|
import ColumnEditor from "./ColumnEditor";
|
||||||
|
|
||||||
|
function findByTestID(wrapper: any, testId: any) {
|
||||||
|
return wrapper.find(`[data-test="${testId}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mount(column: any, variant: "table" | "details", onChange: any = jest.fn()) {
|
||||||
|
return enzyme.mount(
|
||||||
|
<ColumnEditor
|
||||||
|
column={column}
|
||||||
|
variant={variant}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockColumn = {
|
||||||
|
name: "user_id",
|
||||||
|
title: "user_id",
|
||||||
|
visible: true,
|
||||||
|
alignContent: "left" as const,
|
||||||
|
displayAs: "string",
|
||||||
|
description: "",
|
||||||
|
allowSearch: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Shared ColumnEditor", () => {
|
||||||
|
describe("Common functionality", () => {
|
||||||
|
test.each(["table", "details"] as const)("Changes column title - %s variant", async (variant) => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const onChange = jest.fn((changes) => {
|
||||||
|
expect(changes).toEqual({
|
||||||
|
...mockColumn,
|
||||||
|
title: "User ID",
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
const el = mount(mockColumn, variant, onChange);
|
||||||
|
|
||||||
|
const testPrefix = variant === "table" ? "Table" : "Details";
|
||||||
|
findByTestID(el, `${testPrefix}.Column.user_id.Title`)
|
||||||
|
.find("input")
|
||||||
|
.simulate("change", { target: { value: "User ID" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(["table", "details"] as const)("Changes column alignment - %s variant", (variant) => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const el = mount({
|
||||||
|
...mockColumn,
|
||||||
|
name: "amount",
|
||||||
|
displayAs: "number",
|
||||||
|
}, variant, onChange);
|
||||||
|
|
||||||
|
const testPrefix = variant === "table" ? "Table" : "Details";
|
||||||
|
findByTestID(el, `${testPrefix}.Column.amount.TextAlignment`)
|
||||||
|
.find('input[value="right"]')
|
||||||
|
.simulate("change", { target: { value: "right" } });
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith({
|
||||||
|
...mockColumn,
|
||||||
|
name: "amount",
|
||||||
|
displayAs: "number",
|
||||||
|
alignContent: "right",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(["table", "details"] as const)("Changes column description - %s variant", async (variant) => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const onChange = jest.fn((changes) => {
|
||||||
|
expect(changes).toEqual({
|
||||||
|
...mockColumn,
|
||||||
|
name: "status",
|
||||||
|
title: "Status",
|
||||||
|
description: "Current order status",
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
const el = mount({
|
||||||
|
...mockColumn,
|
||||||
|
name: "status",
|
||||||
|
title: "Status",
|
||||||
|
}, variant, onChange);
|
||||||
|
|
||||||
|
const testPrefix = variant === "table" ? "Table" : "Details";
|
||||||
|
findByTestID(el, `${testPrefix}.Column.status.Description`)
|
||||||
|
.find("input")
|
||||||
|
.simulate("change", { target: { value: "Current order status" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(["table", "details"] as const)("Changes display type - %s variant", (variant) => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const el = mount({
|
||||||
|
...mockColumn,
|
||||||
|
name: "created_at",
|
||||||
|
title: "Created At",
|
||||||
|
displayAs: "datetime",
|
||||||
|
}, variant, onChange);
|
||||||
|
|
||||||
|
const testPrefix = variant === "table" ? "Table" : "Details";
|
||||||
|
findByTestID(el, `${testPrefix}.Column.created_at.DisplayAs`)
|
||||||
|
.find(".ant-select-selector")
|
||||||
|
.simulate("mouseDown");
|
||||||
|
findByTestID(el, `${testPrefix}.Column.created_at.DisplayAs.string`)
|
||||||
|
.simulate("click");
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith({
|
||||||
|
...mockColumn,
|
||||||
|
name: "created_at",
|
||||||
|
title: "Created At",
|
||||||
|
displayAs: "string",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Table variant specific", () => {
|
||||||
|
test("Shows search checkbox", () => {
|
||||||
|
const el = mount(mockColumn, "table");
|
||||||
|
|
||||||
|
const searchCheckbox = findByTestID(el, "Table.Column.user_id.UseForSearch");
|
||||||
|
expect(searchCheckbox.find("input[type='checkbox']")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Changes search setting", () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const el = mount({
|
||||||
|
...mockColumn,
|
||||||
|
allowSearch: false,
|
||||||
|
}, "table", onChange);
|
||||||
|
|
||||||
|
findByTestID(el, "Table.Column.user_id.UseForSearch")
|
||||||
|
.find("input[type='checkbox']")
|
||||||
|
.simulate("change", { target: { checked: true } });
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith({
|
||||||
|
...mockColumn,
|
||||||
|
allowSearch: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Uses correct CSS class", () => {
|
||||||
|
const el = mount(mockColumn, "table");
|
||||||
|
expect(el.find(".table-visualization-editor-column")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Details variant specific", () => {
|
||||||
|
test("Hides search checkbox", () => {
|
||||||
|
const el = mount(mockColumn, "details");
|
||||||
|
|
||||||
|
const searchCheckbox = findByTestID(el, "Details.Column.user_id.UseForSearch");
|
||||||
|
expect(searchCheckbox).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Uses correct CSS class", () => {
|
||||||
|
const el = mount(mockColumn, "details");
|
||||||
|
expect(el.find(".details-visualization-editor-column")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Props and defaults", () => {
|
||||||
|
test("Uses default showSearch based on variant", () => {
|
||||||
|
const tableEl = mount(mockColumn, "table");
|
||||||
|
const detailsEl = mount(mockColumn, "details");
|
||||||
|
|
||||||
|
expect(findByTestID(tableEl, "Table.Column.user_id.UseForSearch").find("input[type='checkbox']")).toHaveLength(1);
|
||||||
|
expect(findByTestID(detailsEl, "Details.Column.user_id.UseForSearch")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Allows custom testPrefix", () => {
|
||||||
|
const el = mount(mockColumn, "table");
|
||||||
|
el.setProps({ testPrefix: "Custom.Prefix" });
|
||||||
|
el.update();
|
||||||
|
|
||||||
|
expect(findByTestID(el, "Custom.Prefix.Title").find("input")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Handles missing onChange gracefully", () => {
|
||||||
|
const el = mount(mockColumn, "table", undefined);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
findByTestID(el, "Table.Column.user_id.Title")
|
||||||
|
.find("input")
|
||||||
|
.simulate("change", { target: { value: "New Title" } });
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Rendering", () => {
|
||||||
|
test("Table variant renders with correct structure", () => {
|
||||||
|
const el = mount({
|
||||||
|
...mockColumn,
|
||||||
|
allowSearch: true,
|
||||||
|
description: "Sample description",
|
||||||
|
}, "table");
|
||||||
|
|
||||||
|
// Verify key elements are present
|
||||||
|
expect(el.find('.table-visualization-editor-column')).toHaveLength(1);
|
||||||
|
expect(findByTestID(el, "Table.Column.user_id.Title").find("input")).toHaveLength(1);
|
||||||
|
expect(findByTestID(el, "Table.Column.user_id.TextAlignment").find("input[type='radio']")).toHaveLength(3);
|
||||||
|
expect(findByTestID(el, "Table.Column.user_id.UseForSearch").find("input[type='checkbox']")).toHaveLength(1);
|
||||||
|
expect(findByTestID(el, "Table.Column.user_id.Description").find("input")).toHaveLength(1);
|
||||||
|
expect(findByTestID(el, "Table.Column.user_id.DisplayAs")).toHaveLength(7); // Expected count based on current behavior
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Details variant renders with correct structure", () => {
|
||||||
|
const el = mount({
|
||||||
|
...mockColumn,
|
||||||
|
description: "Sample description",
|
||||||
|
}, "details");
|
||||||
|
|
||||||
|
// Verify key elements are present
|
||||||
|
expect(el.find('.details-visualization-editor-column')).toHaveLength(1);
|
||||||
|
expect(findByTestID(el, "Details.Column.user_id.Title").find("input")).toHaveLength(1);
|
||||||
|
expect(findByTestID(el, "Details.Column.user_id.TextAlignment").find("input[type='radio']")).toHaveLength(3);
|
||||||
|
expect(findByTestID(el, "Details.Column.user_id.UseForSearch")).toHaveLength(0); // Should not exist
|
||||||
|
expect(findByTestID(el, "Details.Column.user_id.Description").find("input")).toHaveLength(1);
|
||||||
|
expect(findByTestID(el, "Details.Column.user_id.DisplayAs")).toHaveLength(7); // Expected count based on current behavior
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
117
viz-lib/src/visualizations/shared/components/ColumnEditor.tsx
Normal file
117
viz-lib/src/visualizations/shared/components/ColumnEditor.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { map } from "lodash";
|
||||||
|
import React from "react";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
import * as Grid from "antd/lib/grid";
|
||||||
|
import { Section, Select, Input, Checkbox, TextAlignmentSelect } from "@/components/visualizations/editor";
|
||||||
|
|
||||||
|
import ColumnTypes from "../columns";
|
||||||
|
|
||||||
|
type Column = {
|
||||||
|
name: string;
|
||||||
|
title?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
alignContent?: "left" | "center" | "right";
|
||||||
|
displayAs?: any;
|
||||||
|
description?: string;
|
||||||
|
allowSearch?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ColumnEditorProps = {
|
||||||
|
column: Column;
|
||||||
|
onChange?: (changes: any) => any;
|
||||||
|
variant: "table" | "details";
|
||||||
|
showSearch?: boolean;
|
||||||
|
testPrefix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ColumnEditor({
|
||||||
|
column,
|
||||||
|
onChange,
|
||||||
|
variant,
|
||||||
|
showSearch = variant === "table",
|
||||||
|
testPrefix,
|
||||||
|
}: ColumnEditorProps) {
|
||||||
|
function handleChange(changes: any) {
|
||||||
|
if (onChange) {
|
||||||
|
onChange({ ...column, ...changes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [handleChangeDebounced] = useDebouncedCallback(handleChange, 200);
|
||||||
|
|
||||||
|
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||||
|
const AdditionalOptions = ColumnTypes[column.displayAs].Editor || null;
|
||||||
|
|
||||||
|
const cssClass = `${variant}-visualization-editor-column`;
|
||||||
|
const dataTestPrefix = testPrefix || `${variant === "table" ? "Table" : "Details"}.Column.${column.name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssClass}>
|
||||||
|
{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
|
||||||
|
<Section>
|
||||||
|
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: Element[]; gutter: number; type:... Remove this comment to see the full error message */}
|
||||||
|
<Grid.Row gutter={15} type="flex" align="middle">
|
||||||
|
<Grid.Col span={16}>
|
||||||
|
<Input
|
||||||
|
data-test={`${dataTestPrefix}.Title`}
|
||||||
|
defaultValue={column.title}
|
||||||
|
onChange={(event: any) => handleChangeDebounced({ title: event.target.value })}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<TextAlignmentSelect
|
||||||
|
data-test={`${dataTestPrefix}.TextAlignment`}
|
||||||
|
defaultValue={column.alignContent}
|
||||||
|
onChange={(event: any) => handleChange({ alignContent: event.target.value })}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid.Row>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{showSearch && (
|
||||||
|
/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */
|
||||||
|
<Section>
|
||||||
|
<Checkbox
|
||||||
|
data-test={`${dataTestPrefix}.UseForSearch`}
|
||||||
|
defaultChecked={column.allowSearch}
|
||||||
|
onChange={event => handleChange({ allowSearch: event.target.checked })}>
|
||||||
|
Use for search
|
||||||
|
</Checkbox>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
|
||||||
|
<Section>
|
||||||
|
<Input
|
||||||
|
label="Description"
|
||||||
|
data-test={`${dataTestPrefix}.Description`}
|
||||||
|
defaultValue={column.description}
|
||||||
|
onChange={(event: any) => handleChangeDebounced({ description: event.target.value })}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
|
||||||
|
<Section>
|
||||||
|
<Select
|
||||||
|
label="Display as:"
|
||||||
|
data-test={`${dataTestPrefix}.DisplayAs`}
|
||||||
|
defaultValue={column.displayAs}
|
||||||
|
onChange={(displayAs: any) => handleChange({ displayAs })}>
|
||||||
|
{map(ColumnTypes, ({ friendlyName }, key) => (
|
||||||
|
// @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message
|
||||||
|
<Select.Option key={key} data-test={`${dataTestPrefix}.DisplayAs.${key}`}>
|
||||||
|
{friendlyName}
|
||||||
|
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{AdditionalOptions && <AdditionalOptions column={column} onChange={handleChange} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnEditor.defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
};
|
||||||
102
viz-lib/src/visualizations/shared/components/ColumnsSettings.tsx
Normal file
102
viz-lib/src/visualizations/shared/components/ColumnsSettings.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { map } from "lodash";
|
||||||
|
import React from "react";
|
||||||
|
import Collapse from "antd/lib/collapse";
|
||||||
|
import Tooltip from "antd/lib/tooltip";
|
||||||
|
import Typography from "antd/lib/typography";
|
||||||
|
// @ts-expect-error ts-migrate(2724) FIXME: Module '"../../../../node_modules/react-sortable-h... Remove this comment to see the full error message
|
||||||
|
import { sortableElement } from "react-sortable-hoc";
|
||||||
|
import { SortableContainer, DragHandle } from "@/components/sortable";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import EyeOutlinedIcon from "@ant-design/icons/EyeOutlined";
|
||||||
|
import EyeInvisibleOutlinedIcon from "@ant-design/icons/EyeInvisibleOutlined";
|
||||||
|
|
||||||
|
import ColumnEditor from "./ColumnEditor";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const SortableItem = sortableElement(Collapse.Panel);
|
||||||
|
|
||||||
|
type ColumnsSettingsProps = {
|
||||||
|
options: any;
|
||||||
|
onOptionsChange: any;
|
||||||
|
variant: "table" | "details";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ColumnsSettings({ options, onOptionsChange, variant }: ColumnsSettingsProps) {
|
||||||
|
function handleColumnChange(newColumn: any, event: any) {
|
||||||
|
if (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
const columns = map(options.columns, c => (c.name === newColumn.name ? newColumn : c));
|
||||||
|
onOptionsChange({ columns });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleColumnsReorder({ oldIndex, newIndex }: any) {
|
||||||
|
const columns = [...options.columns];
|
||||||
|
columns.splice(newIndex, 0, ...columns.splice(oldIndex, 1));
|
||||||
|
onOptionsChange({ columns });
|
||||||
|
}
|
||||||
|
|
||||||
|
const helperClass = `${variant}-editor-columns-dragged-item`;
|
||||||
|
const containerClass = `${variant}-visualization-editor-columns`;
|
||||||
|
const testPrefix = variant === "table" ? "Table" : "Details";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableContainer
|
||||||
|
axis="y"
|
||||||
|
lockAxis="y"
|
||||||
|
useDragHandle
|
||||||
|
helperClass={helperClass}
|
||||||
|
helperContainer={(container: any) => container.firstChild}
|
||||||
|
onSortEnd={handleColumnsReorder}
|
||||||
|
containerProps={{
|
||||||
|
className: containerClass,
|
||||||
|
}}>
|
||||||
|
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'null | u... Remove this comment to see the full error message */}
|
||||||
|
<Collapse bordered={false} defaultActiveKey={[]} expandIconPosition="right">
|
||||||
|
{map(options.columns, (column, index) => (
|
||||||
|
<SortableItem
|
||||||
|
key={column.name}
|
||||||
|
index={index}
|
||||||
|
header={
|
||||||
|
<React.Fragment>
|
||||||
|
<DragHandle />
|
||||||
|
<span data-test={`${testPrefix}.Column.${column.name}.Name`}>
|
||||||
|
{column.name}
|
||||||
|
{column.title !== "" && column.title !== column.name && (
|
||||||
|
<Text type="secondary" style={{ marginLeft: 5 }}>
|
||||||
|
<i>({column.title})</i>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Tooltip title="Toggle visibility" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||||
|
{column.visible ? (
|
||||||
|
<EyeOutlinedIcon
|
||||||
|
data-test={`${testPrefix}.Column.${column.name}.Visibility`}
|
||||||
|
onClick={event => handleColumnChange({ ...column, visible: !column.visible }, event)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EyeInvisibleOutlinedIcon
|
||||||
|
data-test={`${testPrefix}.Column.${column.name}.Visibility`}
|
||||||
|
onClick={event => handleColumnChange({ ...column, visible: !column.visible }, event)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
}>
|
||||||
|
<ColumnEditor column={column} variant={variant} onChange={(changes) => handleColumnChange(changes, undefined)} />
|
||||||
|
</SortableItem>
|
||||||
|
))}
|
||||||
|
</Collapse>
|
||||||
|
</SortableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnsSettings.propTypes = {
|
||||||
|
options: PropTypes.object.isRequired,
|
||||||
|
onOptionsChange: PropTypes.func.isRequired,
|
||||||
|
variant: PropTypes.oneOf(["table", "details"]).isRequired,
|
||||||
|
};
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
import { map, keys } from "lodash";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import SharedColumnEditor from "../../shared/components/ColumnEditor";
|
||||||
import * as Grid from "antd/lib/grid";
|
|
||||||
import { Section, Select, Input, Checkbox, TextAlignmentSelect } from "@/components/visualizations/editor";
|
|
||||||
|
|
||||||
import ColumnTypes from "../columns";
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
column: {
|
column: {
|
||||||
@@ -12,7 +7,9 @@ type OwnProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
alignContent?: "left" | "center" | "right";
|
alignContent?: "left" | "center" | "right";
|
||||||
displayAs?: any; // TODO: PropTypes.oneOf(keys(ColumnTypes))
|
displayAs?: any;
|
||||||
|
allowSearch?: boolean;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
onChange?: (...args: any[]) => any;
|
onChange?: (...args: any[]) => any;
|
||||||
};
|
};
|
||||||
@@ -20,78 +17,13 @@ type OwnProps = {
|
|||||||
type Props = OwnProps & typeof ColumnEditor.defaultProps;
|
type Props = OwnProps & typeof ColumnEditor.defaultProps;
|
||||||
|
|
||||||
export default function ColumnEditor({ column, onChange }: Props) {
|
export default function ColumnEditor({ column, onChange }: Props) {
|
||||||
function handleChange(changes: any) {
|
|
||||||
onChange({ ...column, ...changes });
|
|
||||||
}
|
|
||||||
|
|
||||||
const [handleChangeDebounced] = useDebouncedCallback(handleChange, 200);
|
|
||||||
|
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
const AdditionalOptions = ColumnTypes[column.displayAs].Editor || null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-visualization-editor-column">
|
<SharedColumnEditor
|
||||||
{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
|
column={column}
|
||||||
<Section>
|
onChange={onChange}
|
||||||
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: Element[]; gutter: number; type:... Remove this comment to see the full error message */}
|
variant="table"
|
||||||
<Grid.Row gutter={15} type="flex" align="middle">
|
showSearch={true}
|
||||||
<Grid.Col span={16}>
|
/>
|
||||||
<Input
|
|
||||||
data-test={`Table.Column.${column.name}.Title`}
|
|
||||||
defaultValue={column.title}
|
|
||||||
onChange={(event: any) => handleChangeDebounced({ title: event.target.value })}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={8}>
|
|
||||||
<TextAlignmentSelect
|
|
||||||
data-test={`Table.Column.${column.name}.TextAlignment`}
|
|
||||||
defaultValue={column.alignContent}
|
|
||||||
onChange={(event: any) => handleChange({ alignContent: event.target.value })}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid.Row>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
|
|
||||||
<Section>
|
|
||||||
<Checkbox
|
|
||||||
data-test={`Table.Column.${column.name}.UseForSearch`}
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'allowSearch' does not exist on type '{ n... Remove this comment to see the full error message
|
|
||||||
defaultChecked={column.allowSearch}
|
|
||||||
onChange={event => handleChange({ allowSearch: event.target.checked })}>
|
|
||||||
Use for search
|
|
||||||
</Checkbox>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
|
|
||||||
<Section>
|
|
||||||
<Input
|
|
||||||
label="Description"
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'description' does not exist on type '{ n... Remove this comment to see the full error message
|
|
||||||
defaultValue={column.description}
|
|
||||||
onChange={(event: any) => handleChangeDebounced({ description: event.target.value })}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
|
|
||||||
<Section>
|
|
||||||
<Select
|
|
||||||
label="Display as:"
|
|
||||||
data-test={`Table.Column.${column.name}.DisplayAs`}
|
|
||||||
defaultValue={column.displayAs}
|
|
||||||
onChange={(displayAs: any) => handleChange({ displayAs })}>
|
|
||||||
{map(ColumnTypes, ({ friendlyName }, key) => (
|
|
||||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message
|
|
||||||
<Select.Option key={key} data-test={`Table.Column.${column.name}.DisplayAs.${key}`}>
|
|
||||||
{friendlyName}
|
|
||||||
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
{AdditionalOptions && <AdditionalOptions column={column} onChange={handleChange} />}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,88 +1,14 @@
|
|||||||
import { map } from "lodash";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Collapse from "antd/lib/collapse";
|
import SharedColumnsSettings from "../../shared/components/ColumnsSettings";
|
||||||
import Tooltip from "antd/lib/tooltip";
|
|
||||||
import Typography from "antd/lib/typography";
|
|
||||||
// @ts-expect-error ts-migrate(2724) FIXME: Module '"../../../../node_modules/react-sortable-h... Remove this comment to see the full error message
|
|
||||||
import { sortableElement } from "react-sortable-hoc";
|
|
||||||
import { SortableContainer, DragHandle } from "@/components/sortable";
|
|
||||||
import { EditorPropTypes } from "@/visualizations/prop-types";
|
import { EditorPropTypes } from "@/visualizations/prop-types";
|
||||||
|
|
||||||
import EyeOutlinedIcon from "@ant-design/icons/EyeOutlined";
|
export default function ColumnsSettings({ options, onOptionsChange, data }: any) {
|
||||||
import EyeInvisibleOutlinedIcon from "@ant-design/icons/EyeInvisibleOutlined";
|
|
||||||
|
|
||||||
import ColumnEditor from "./ColumnEditor";
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
const SortableItem = sortableElement(Collapse.Panel);
|
|
||||||
|
|
||||||
export default function ColumnsSettings({ options, onOptionsChange }: any) {
|
|
||||||
function handleColumnChange(newColumn: any, event: any) {
|
|
||||||
if (event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
const columns = map(options.columns, c => (c.name === newColumn.name ? newColumn : c));
|
|
||||||
onOptionsChange({ columns });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleColumnsReorder({ oldIndex, newIndex }: any) {
|
|
||||||
const columns = [...options.columns];
|
|
||||||
columns.splice(newIndex, 0, ...columns.splice(oldIndex, 1));
|
|
||||||
onOptionsChange({ columns });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortableContainer
|
<SharedColumnsSettings
|
||||||
axis="y"
|
options={options}
|
||||||
lockAxis="y"
|
onOptionsChange={onOptionsChange}
|
||||||
useDragHandle
|
variant="table"
|
||||||
helperClass="table-editor-columns-dragged-item"
|
/>
|
||||||
helperContainer={(container: any) => container.firstChild}
|
|
||||||
onSortEnd={handleColumnsReorder}
|
|
||||||
containerProps={{
|
|
||||||
className: "table-visualization-editor-columns",
|
|
||||||
}}>
|
|
||||||
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'null | u... Remove this comment to see the full error message */}
|
|
||||||
<Collapse bordered={false} defaultActiveKey={[]} expandIconPosition="right">
|
|
||||||
{map(options.columns, (column, index) => (
|
|
||||||
<SortableItem
|
|
||||||
key={column.name}
|
|
||||||
index={index}
|
|
||||||
header={
|
|
||||||
<React.Fragment>
|
|
||||||
<DragHandle />
|
|
||||||
<span data-test={`Table.Column.${column.name}.Name`}>
|
|
||||||
{column.name}
|
|
||||||
{column.title !== "" && column.title !== column.name && (
|
|
||||||
<Text type="secondary" style={{ marginLeft: 5 }}>
|
|
||||||
<i>({column.title})</i>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
extra={
|
|
||||||
<Tooltip title="Toggle visibility" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
|
||||||
{column.visible ? (
|
|
||||||
<EyeOutlinedIcon
|
|
||||||
data-test={`Table.Column.${column.name}.Visibility`}
|
|
||||||
onClick={event => handleColumnChange({ ...column, visible: !column.visible }, event)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EyeInvisibleOutlinedIcon
|
|
||||||
data-test={`Table.Column.${column.name}.Visibility`}
|
|
||||||
onClick={event => handleColumnChange({ ...column, visible: !column.visible }, event)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
}>
|
|
||||||
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '(newColumn: any, event: any) => void' is not... Remove this comment to see the full error message */}
|
|
||||||
<ColumnEditor column={column} onChange={handleColumnChange} />
|
|
||||||
</SortableItem>
|
|
||||||
))}
|
|
||||||
</Collapse>
|
|
||||||
</SortableContainer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Object {
|
|||||||
"true",
|
"true",
|
||||||
],
|
],
|
||||||
"dateTimeFormat": undefined,
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
"displayAs": "string",
|
"displayAs": "string",
|
||||||
"highlightLinks": false,
|
"highlightLinks": false,
|
||||||
"imageHeight": "",
|
"imageHeight": "",
|
||||||
@@ -46,6 +47,7 @@ Object {
|
|||||||
"true",
|
"true",
|
||||||
],
|
],
|
||||||
"dateTimeFormat": undefined,
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
"displayAs": "number",
|
"displayAs": "number",
|
||||||
"highlightLinks": false,
|
"highlightLinks": false,
|
||||||
"imageHeight": "",
|
"imageHeight": "",
|
||||||
@@ -80,6 +82,7 @@ Object {
|
|||||||
"true",
|
"true",
|
||||||
],
|
],
|
||||||
"dateTimeFormat": undefined,
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
"displayAs": "string",
|
"displayAs": "string",
|
||||||
"highlightLinks": false,
|
"highlightLinks": false,
|
||||||
"imageHeight": "",
|
"imageHeight": "",
|
||||||
@@ -114,6 +117,7 @@ Object {
|
|||||||
"true",
|
"true",
|
||||||
],
|
],
|
||||||
"dateTimeFormat": undefined,
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
"displayAs": "string",
|
"displayAs": "string",
|
||||||
"highlightLinks": false,
|
"highlightLinks": false,
|
||||||
"imageHeight": "",
|
"imageHeight": "",
|
||||||
@@ -148,6 +152,7 @@ Object {
|
|||||||
"true",
|
"true",
|
||||||
],
|
],
|
||||||
"dateTimeFormat": undefined,
|
"dateTimeFormat": undefined,
|
||||||
|
"description": "",
|
||||||
"displayAs": "string",
|
"displayAs": "string",
|
||||||
"highlightLinks": false,
|
"highlightLinks": false,
|
||||||
"imageHeight": "",
|
"imageHeight": "",
|
||||||
|
|||||||
@@ -1,133 +1,19 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
|
import {
|
||||||
|
getDefaultColumnsOptions,
|
||||||
|
getDefaultFormatOptions,
|
||||||
|
getColumnsOptions,
|
||||||
|
} from "@/visualizations/shared/columnUtils";
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
itemsPerPage: 25,
|
itemsPerPage: 25,
|
||||||
paginationSize: "default", // not editable through Editor
|
paginationSize: "default", // not editable through Editor
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterTypes = ["filter", "multi-filter", "multiFilter"];
|
|
||||||
|
|
||||||
function getColumnNameWithoutType(column: any) {
|
|
||||||
let typeSplit;
|
|
||||||
if (column.indexOf("::") !== -1) {
|
|
||||||
typeSplit = "::";
|
|
||||||
} else if (column.indexOf("__") !== -1) {
|
|
||||||
typeSplit = "__";
|
|
||||||
} else {
|
|
||||||
return column;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = column.split(typeSplit);
|
|
||||||
if (parts[0] === "" && parts.length === 2) {
|
|
||||||
return parts[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.includes(filterTypes, parts[1])) {
|
|
||||||
return column;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColumnContentAlignment(type: any) {
|
|
||||||
return ["integer", "float", "boolean", "date", "datetime"].indexOf(type) >= 0 ? "right" : "left";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultColumnsOptions(columns: any) {
|
|
||||||
const displayAs = {
|
|
||||||
integer: "number",
|
|
||||||
float: "number",
|
|
||||||
boolean: "boolean",
|
|
||||||
date: "datetime",
|
|
||||||
datetime: "datetime",
|
|
||||||
};
|
|
||||||
|
|
||||||
return _.map(columns, (col, index) => ({
|
|
||||||
name: col.name,
|
|
||||||
type: col.type,
|
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
displayAs: displayAs[col.type] || "string",
|
|
||||||
visible: true,
|
|
||||||
order: 100000 + index,
|
|
||||||
title: getColumnNameWithoutType(col.name),
|
|
||||||
allowSearch: false,
|
|
||||||
alignContent: getColumnContentAlignment(col.type),
|
|
||||||
// `string` cell options
|
|
||||||
allowHTML: false,
|
|
||||||
highlightLinks: false,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultFormatOptions(column: any) {
|
|
||||||
const dateTimeFormat = {
|
|
||||||
date: visualizationsSettings.dateFormat || "DD/MM/YYYY",
|
|
||||||
datetime: visualizationsSettings.dateTimeFormat || "DD/MM/YYYY HH:mm",
|
|
||||||
};
|
|
||||||
const numberFormat = {
|
|
||||||
integer: visualizationsSettings.integerFormat || "0,0",
|
|
||||||
float: visualizationsSettings.floatFormat || "0,0.00",
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
dateTimeFormat: dateTimeFormat[column.type],
|
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
|
||||||
numberFormat: numberFormat[column.type],
|
|
||||||
nullValue: visualizationsSettings.nullValue,
|
|
||||||
booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
|
|
||||||
// `image` cell options
|
|
||||||
imageUrlTemplate: "{{ @ }}",
|
|
||||||
imageTitleTemplate: "{{ @ }}",
|
|
||||||
imageWidth: "",
|
|
||||||
imageHeight: "",
|
|
||||||
// `link` cell options
|
|
||||||
linkUrlTemplate: "{{ @ }}",
|
|
||||||
linkTextTemplate: "{{ @ }}",
|
|
||||||
linkTitleTemplate: "{{ @ }}",
|
|
||||||
linkOpenInNewTab: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function wereColumnsReordered(queryColumns: any, visualizationColumns: any) {
|
|
||||||
queryColumns = _.map(queryColumns, col => col.name);
|
|
||||||
visualizationColumns = _.map(visualizationColumns, col => col.name);
|
|
||||||
|
|
||||||
// Some columns may be removed - so skip them (but keep original order)
|
|
||||||
visualizationColumns = _.filter(visualizationColumns, col => _.includes(queryColumns, col));
|
|
||||||
// Pick query columns that were previously saved with viz (but keep order too)
|
|
||||||
queryColumns = _.filter(queryColumns, col => _.includes(visualizationColumns, col));
|
|
||||||
|
|
||||||
// Both array now have the same size as they both contains only common columns
|
|
||||||
// (in fact, it was an intersection, that kept order of items on both arrays).
|
|
||||||
// Now check for equality item-by-item; if common columns are in the same order -
|
|
||||||
// they were not reordered in editor
|
|
||||||
for (let i = 0; i < queryColumns.length; i += 1) {
|
|
||||||
if (visualizationColumns[i] !== queryColumns[i]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColumnsOptions(columns: any, visualizationColumns: any) {
|
|
||||||
const options = getDefaultColumnsOptions(columns);
|
|
||||||
|
|
||||||
if (wereColumnsReordered(columns, visualizationColumns)) {
|
|
||||||
visualizationColumns = _.fromPairs(
|
|
||||||
_.map(visualizationColumns, (col, index) => [col.name, _.extend({}, col, { order: index })])
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
visualizationColumns = _.fromPairs(_.map(visualizationColumns, col => [col.name, _.omit(col, "order")]));
|
|
||||||
}
|
|
||||||
|
|
||||||
_.each(options, col => _.extend(col, visualizationColumns[col.name]));
|
|
||||||
|
|
||||||
return _.sortBy(options, "order");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function getOptions(options: any, { columns }: any) {
|
export default function getOptions(options: any, { columns }: any) {
|
||||||
options = { ...DEFAULT_OPTIONS, ...options };
|
options = { ...DEFAULT_OPTIONS, ...options };
|
||||||
options.columns = _.map(getColumnsOptions(columns, options.columns), col => ({
|
options.columns = _.map(getColumnsOptions(columns, options.columns, { allowSearch: false }), col => ({
|
||||||
...getDefaultFormatOptions(col),
|
...getDefaultFormatOptions(col),
|
||||||
...col,
|
...col,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { isNil, map, get, filter, each, sortBy, some, findIndex, toString } from
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import Tooltip from "antd/lib/tooltip";
|
import Tooltip from "antd/lib/tooltip";
|
||||||
import ColumnTypes from "./columns";
|
import ColumnTypes from "../shared/columns";
|
||||||
|
|
||||||
function nextOrderByDirection(direction: any) {
|
function nextOrderByDirection(direction: any) {
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
|
|||||||
Reference in New Issue
Block a user