mirror of
https://github.com/getredash/redash.git
synced 2025-12-20 01:47:39 -05:00
Compare commits
20 Commits
25.10.0-de
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43ee21ac20 | ||
|
|
262d46f465 | ||
|
|
bc68b1c38b | ||
|
|
4353a82c7a | ||
|
|
761eb0b68b | ||
|
|
9743820efe | ||
|
|
9d49e0457f | ||
|
|
b5781a8ebe | ||
|
|
b6f4159be9 | ||
|
|
d5fbf547cf | ||
|
|
772b160a79 | ||
|
|
bac2160e2a | ||
|
|
c5aa5da6a2 | ||
|
|
9503cc9fb8 | ||
|
|
b353057f9a | ||
|
|
8747d02bbe | ||
|
|
5b463b0d83 | ||
|
|
ea589ad477 | ||
|
|
617124850b | ||
|
|
1cc200843c |
@@ -18,7 +18,7 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
postgres:
|
postgres:
|
||||||
image: pgautoupgrade/pgautoupgrade:latest
|
image: postgres:18-alpine
|
||||||
command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF"
|
command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
postgres:
|
postgres:
|
||||||
image: pgautoupgrade/pgautoupgrade:latest
|
image: postgres:18-alpine
|
||||||
command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF"
|
command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: compose_build up test_db create_database clean clean-all down tests lint backend-unit-tests frontend-unit-tests test build watch start redis-cli bash
|
.PHONY: compose_build up test_db create_database clean down tests lint backend-unit-tests frontend-unit-tests test build watch start redis-cli bash
|
||||||
|
|
||||||
compose_build: .env
|
compose_build: .env
|
||||||
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose build
|
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose build
|
||||||
@@ -32,11 +32,6 @@ clean:
|
|||||||
docker image prune --force
|
docker image prune --force
|
||||||
docker volume prune --force
|
docker volume prune --force
|
||||||
|
|
||||||
clean-all: clean
|
|
||||||
docker image rm --force \
|
|
||||||
redash/redash:latest redis:7-alpine maildev/maildev:latest \
|
|
||||||
pgautoupgrade/pgautoupgrade:15-alpine3.8 pgautoupgrade/pgautoupgrade:latest
|
|
||||||
|
|
||||||
down:
|
down:
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
|
|||||||
BIN
client/app/assets/images/db-logos/duckdb.png
Normal file
BIN
client/app/assets/images/db-logos/duckdb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -9,121 +9,85 @@ const DYNAMIC_DATE_OPTIONS = [
|
|||||||
name: "This week",
|
name: "This week",
|
||||||
value: getDynamicDateRangeFromString("d_this_week"),
|
value: getDynamicDateRangeFromString("d_this_week"),
|
||||||
label: () =>
|
label: () =>
|
||||||
getDynamicDateRangeFromString("d_this_week")
|
getDynamicDateRangeFromString("d_this_week").value()[0].format("MMM D") +
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") +
|
|
||||||
" - " +
|
" - " +
|
||||||
getDynamicDateRangeFromString("d_this_week")
|
getDynamicDateRangeFromString("d_this_week").value()[1].format("MMM D"),
|
||||||
.value()[1]
|
|
||||||
.format("MMM D"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "This month",
|
name: "This month",
|
||||||
value: getDynamicDateRangeFromString("d_this_month"),
|
value: getDynamicDateRangeFromString("d_this_month"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_this_month").value()[0].format("MMMM"),
|
||||||
getDynamicDateRangeFromString("d_this_month")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMMM"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "This year",
|
name: "This year",
|
||||||
value: getDynamicDateRangeFromString("d_this_year"),
|
value: getDynamicDateRangeFromString("d_this_year"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_this_year").value()[0].format("YYYY"),
|
||||||
getDynamicDateRangeFromString("d_this_year")
|
|
||||||
.value()[0]
|
|
||||||
.format("YYYY"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last week",
|
name: "Last week",
|
||||||
value: getDynamicDateRangeFromString("d_last_week"),
|
value: getDynamicDateRangeFromString("d_last_week"),
|
||||||
label: () =>
|
label: () =>
|
||||||
getDynamicDateRangeFromString("d_last_week")
|
getDynamicDateRangeFromString("d_last_week").value()[0].format("MMM D") +
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") +
|
|
||||||
" - " +
|
" - " +
|
||||||
getDynamicDateRangeFromString("d_last_week")
|
getDynamicDateRangeFromString("d_last_week").value()[1].format("MMM D"),
|
||||||
.value()[1]
|
|
||||||
.format("MMM D"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last month",
|
name: "Last month",
|
||||||
value: getDynamicDateRangeFromString("d_last_month"),
|
value: getDynamicDateRangeFromString("d_last_month"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_month").value()[0].format("MMMM"),
|
||||||
getDynamicDateRangeFromString("d_last_month")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMMM"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last year",
|
name: "Last year",
|
||||||
value: getDynamicDateRangeFromString("d_last_year"),
|
value: getDynamicDateRangeFromString("d_last_year"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_year").value()[0].format("YYYY"),
|
||||||
getDynamicDateRangeFromString("d_last_year")
|
|
||||||
.value()[0]
|
|
||||||
.format("YYYY"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 7 days",
|
name: "Last 7 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_7_days"),
|
value: getDynamicDateRangeFromString("d_last_7_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_7_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_7_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 14 days",
|
name: "Last 14 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_14_days"),
|
value: getDynamicDateRangeFromString("d_last_14_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_14_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_14_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 30 days",
|
name: "Last 30 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_30_days"),
|
value: getDynamicDateRangeFromString("d_last_30_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_30_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_30_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 60 days",
|
name: "Last 60 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_60_days"),
|
value: getDynamicDateRangeFromString("d_last_60_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_60_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_60_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 90 days",
|
name: "Last 90 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_90_days"),
|
value: getDynamicDateRangeFromString("d_last_90_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_90_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_90_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 12 months",
|
name: "Last 12 months",
|
||||||
value: getDynamicDateRangeFromString("d_last_12_months"),
|
value: getDynamicDateRangeFromString("d_last_12_months"),
|
||||||
label: null,
|
label: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Last 10 years",
|
||||||
|
value: getDynamicDateRangeFromString("d_last_10_years"),
|
||||||
|
label: null,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const DYNAMIC_DATETIME_OPTIONS = [
|
const DYNAMIC_DATETIME_OPTIONS = [
|
||||||
{
|
{
|
||||||
name: "Today",
|
name: "Today",
|
||||||
value: getDynamicDateRangeFromString("d_today"),
|
value: getDynamicDateRangeFromString("d_today"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_today").value()[0].format("MMM D"),
|
||||||
getDynamicDateRangeFromString("d_today")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Yesterday",
|
name: "Yesterday",
|
||||||
value: getDynamicDateRangeFromString("d_yesterday"),
|
value: getDynamicDateRangeFromString("d_yesterday"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_yesterday").value()[0].format("MMM D"),
|
||||||
getDynamicDateRangeFromString("d_yesterday")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D"),
|
|
||||||
},
|
},
|
||||||
...DYNAMIC_DATE_OPTIONS,
|
...DYNAMIC_DATE_OPTIONS,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -145,11 +145,32 @@ export function wrap<I, P = any>(
|
|||||||
|
|
||||||
const initialState = this.getState({ ...itemsSource.getState(), isLoaded: false });
|
const initialState = this.getState({ ...itemsSource.getState(), isLoaded: false });
|
||||||
const { updatePagination, toggleSorting, setSorting, updateSearch, updateSelectedTags, update, handleError } = itemsSource;
|
const { updatePagination, toggleSorting, setSorting, updateSearch, updateSelectedTags, update, handleError } = itemsSource;
|
||||||
|
|
||||||
|
let isRunningUpdateSearch = false;
|
||||||
|
let pendingUpdateSearchParams: any[] | null = null;
|
||||||
|
const debouncedUpdateSearch = debounce(async (...params) => {
|
||||||
|
// Avoid running multiple updateSerch concurrently.
|
||||||
|
// If an updateSearch is already running, we save the params for the latest call.
|
||||||
|
// When the current updateSearch is finished, we call debouncedUpdateSearch again with the saved params.
|
||||||
|
if (isRunningUpdateSearch) {
|
||||||
|
pendingUpdateSearchParams = params;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRunningUpdateSearch = true;
|
||||||
|
await updateSearch(...params);
|
||||||
|
isRunningUpdateSearch = false;
|
||||||
|
if (pendingUpdateSearchParams) {
|
||||||
|
const pendingParams = pendingUpdateSearchParams;
|
||||||
|
pendingUpdateSearchParams = null;
|
||||||
|
debouncedUpdateSearch(...pendingParams);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
...initialState,
|
...initialState,
|
||||||
toggleSorting, // eslint-disable-line react/no-unused-state
|
toggleSorting, // eslint-disable-line react/no-unused-state
|
||||||
setSorting, // eslint-disable-line react/no-unused-state
|
setSorting, // eslint-disable-line react/no-unused-state
|
||||||
updateSearch: debounce(updateSearch, 200), // eslint-disable-line react/no-unused-state
|
updateSearch: debouncedUpdateSearch, // eslint-disable-line react/no-unused-state
|
||||||
updateSelectedTags, // eslint-disable-line react/no-unused-state
|
updateSelectedTags, // eslint-disable-line react/no-unused-state
|
||||||
updatePagination, // eslint-disable-line react/no-unused-state
|
updatePagination, // eslint-disable-line react/no-unused-state
|
||||||
update, // eslint-disable-line react/no-unused-state
|
update, // eslint-disable-line react/no-unused-state
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export class ItemsSource {
|
|||||||
this._sorter.setField(null);
|
this._sorter.setField(null);
|
||||||
}
|
}
|
||||||
this._paginator.setPage(1);
|
this._paginator.setPage(1);
|
||||||
this._changed({ search: true, pagination: { page: true } });
|
return this._changed({ search: true, pagination: { page: true } });
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSelectedTags = (selectedTags) => {
|
updateSelectedTags = (selectedTags) => {
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ function SchemaItem({ item, expanded, onToggle, onSelect, ...props }) {
|
|||||||
mouseEnterDelay={0}
|
mouseEnterDelay={0}
|
||||||
mouseLeaveDelay={0}
|
mouseLeaveDelay={0}
|
||||||
placement="rightTop"
|
placement="rightTop"
|
||||||
|
overlayStyle={{ whiteSpace: "pre-line" }}
|
||||||
>
|
>
|
||||||
<PlainButton
|
<PlainButton
|
||||||
key={columnName}
|
key={columnName}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ function DashboardSettings({ dashboardConfiguration }) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!!dashboard.dashboard_filters_enabled}
|
checked={!!dashboard.dashboard_filters_enabled}
|
||||||
onChange={({ target }) => updateDashboard({ dashboard_filters_enabled: target.checked })}
|
onChange={({ target }) => updateDashboard({ dashboard_filters_enabled: target.checked })}
|
||||||
data-test="DashboardFiltersCheckbox">
|
data-test="DashboardFiltersCheckbox"
|
||||||
|
>
|
||||||
Use Dashboard Level Filters
|
Use Dashboard Level Filters
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,9 +91,9 @@ function DashboardComponent(props) {
|
|||||||
|
|
||||||
const [pageContainer, setPageContainer] = useState(null);
|
const [pageContainer, setPageContainer] = useState(null);
|
||||||
const [bottomPanelStyles, setBottomPanelStyles] = useState({});
|
const [bottomPanelStyles, setBottomPanelStyles] = useState({});
|
||||||
const onParametersEdit = parameters => {
|
const onParametersEdit = (parameters) => {
|
||||||
const paramOrder = map(parameters, "name");
|
const paramOrder = map(parameters, "name");
|
||||||
updateDashboard({ options: { globalParamOrder: paramOrder } });
|
updateDashboard({ options: { ...dashboard.options, globalParamOrder: paramOrder } });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -175,7 +176,7 @@ function DashboardPage({ dashboardSlug, dashboardId, onError }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Dashboard.get({ id: dashboardId, slug: dashboardSlug })
|
Dashboard.get({ id: dashboardId, slug: dashboardSlug })
|
||||||
.then(dashboardData => {
|
.then((dashboardData) => {
|
||||||
recordEvent("view", "dashboard", dashboardData.id);
|
recordEvent("view", "dashboard", dashboardData.id);
|
||||||
setDashboard(dashboardData);
|
setDashboard(dashboardData);
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ routes.register(
|
|||||||
"Dashboards.LegacyViewOrEdit",
|
"Dashboards.LegacyViewOrEdit",
|
||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboard/:dashboardSlug",
|
path: "/dashboard/:dashboardSlug",
|
||||||
render: pageProps => <DashboardPage {...pageProps} />,
|
render: (pageProps) => <DashboardPage {...pageProps} />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -215,6 +216,6 @@ routes.register(
|
|||||||
"Dashboards.ViewOrEdit",
|
"Dashboards.ViewOrEdit",
|
||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboards/:dashboardId([^-]+)(-.*)?",
|
path: "/dashboards/:dashboardId([^-]+)(-.*)?",
|
||||||
render: pageProps => <DashboardPage {...pageProps} />,
|
render: (pageProps) => <DashboardPage {...pageProps} />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { DashboardStatusEnum } from "../hooks/useDashboard";
|
|||||||
import "./DashboardHeader.less";
|
import "./DashboardHeader.less";
|
||||||
|
|
||||||
function getDashboardTags() {
|
function getDashboardTags() {
|
||||||
return getTags("api/dashboards/tags").then(tags => map(tags, t => t.name));
|
return getTags("api/dashboards/tags").then((tags) => map(tags, (t) => t.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buttonType(value) {
|
function buttonType(value) {
|
||||||
@@ -38,7 +38,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
|
|||||||
<h3>
|
<h3>
|
||||||
<EditInPlace
|
<EditInPlace
|
||||||
isEditable={editingLayout}
|
isEditable={editingLayout}
|
||||||
onDone={name => updateDashboard({ name })}
|
onDone={(name) => updateDashboard({ name })}
|
||||||
value={dashboard.name}
|
value={dashboard.name}
|
||||||
ignoreBlanks
|
ignoreBlanks
|
||||||
/>
|
/>
|
||||||
@@ -53,7 +53,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
|
|||||||
isArchived={dashboard.is_archived}
|
isArchived={dashboard.is_archived}
|
||||||
canEdit={canEditDashboard}
|
canEdit={canEditDashboard}
|
||||||
getAvailableTags={getDashboardTags}
|
getAvailableTags={getDashboardTags}
|
||||||
onEdit={tags => updateDashboard({ tags })}
|
onEdit={(tags) => updateDashboard({ tags })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -89,14 +89,15 @@ function RefreshButton({ dashboardConfiguration }) {
|
|||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
overlay={
|
overlay={
|
||||||
<Menu onClick={onRefreshRateSelected} selectedKeys={[`${refreshRate}`]}>
|
<Menu onClick={onRefreshRateSelected} selectedKeys={[`${refreshRate}`]}>
|
||||||
{refreshRateOptions.map(option => (
|
{refreshRateOptions.map((option) => (
|
||||||
<Menu.Item key={`${option}`} disabled={!includes(allowedIntervals, option)}>
|
<Menu.Item key={`${option}`} disabled={!includes(allowedIntervals, option)}>
|
||||||
{durationHumanize(option)}
|
{durationHumanize(option)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
{refreshRate && <Menu.Item key={null}>Disable auto refresh</Menu.Item>}
|
{refreshRate && <Menu.Item key={null}>Disable auto refresh</Menu.Item>}
|
||||||
</Menu>
|
</Menu>
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<Button className="icon-button hidden-xs" type={buttonType(refreshRate)}>
|
<Button className="icon-button hidden-xs" type={buttonType(refreshRate)}>
|
||||||
<i className="fa fa-angle-down" aria-hidden="true" />
|
<i className="fa fa-angle-down" aria-hidden="true" />
|
||||||
<span className="sr-only">Split button!</span>
|
<span className="sr-only">Split button!</span>
|
||||||
@@ -166,7 +167,8 @@ function DashboardMoreOptionsButton({ dashboardConfiguration }) {
|
|||||||
<PlainButton onClick={archive}>Archive</PlainButton>
|
<PlainButton onClick={archive}>Archive</PlainButton>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<Button className="icon-button m-l-5" data-test="DashboardMoreButton" aria-label="More actions">
|
<Button className="icon-button m-l-5" data-test="DashboardMoreButton" aria-label="More actions">
|
||||||
<EllipsisOutlinedIcon rotate={90} aria-hidden="true" />
|
<EllipsisOutlinedIcon rotate={90} aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -216,7 +218,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
|
|||||||
type={buttonType(fullscreen)}
|
type={buttonType(fullscreen)}
|
||||||
className="icon-button m-l-5"
|
className="icon-button m-l-5"
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
aria-label="Toggle fullscreen display">
|
aria-label="Toggle fullscreen display"
|
||||||
|
>
|
||||||
<i className="zmdi zmdi-fullscreen" aria-hidden="true" />
|
<i className="zmdi zmdi-fullscreen" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -229,7 +232,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
|
|||||||
type={buttonType(dashboard.publicAccessEnabled)}
|
type={buttonType(dashboard.publicAccessEnabled)}
|
||||||
onClick={showShareDashboardDialog}
|
onClick={showShareDashboardDialog}
|
||||||
data-test="OpenShareForm"
|
data-test="OpenShareForm"
|
||||||
aria-label="Share">
|
aria-label="Share"
|
||||||
|
>
|
||||||
<i className="zmdi zmdi-share" aria-hidden="true" />
|
<i className="zmdi zmdi-share" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -252,7 +256,11 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
|
|||||||
doneBtnClickedWhileSaving,
|
doneBtnClickedWhileSaving,
|
||||||
dashboardStatus,
|
dashboardStatus,
|
||||||
retrySaveDashboardLayout,
|
retrySaveDashboardLayout,
|
||||||
|
saveDashboardParameters,
|
||||||
} = dashboardConfiguration;
|
} = dashboardConfiguration;
|
||||||
|
const handleDoneEditing = () => {
|
||||||
|
saveDashboardParameters().then(() => setEditingLayout(false));
|
||||||
|
};
|
||||||
let status;
|
let status;
|
||||||
if (dashboardStatus === DashboardStatusEnum.SAVED) {
|
if (dashboardStatus === DashboardStatusEnum.SAVED) {
|
||||||
status = <span className="save-status">Saved</span>;
|
status = <span className="save-status">Saved</span>;
|
||||||
@@ -277,7 +285,7 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
|
|||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button loading={doneBtnClickedWhileSaving} type="primary" onClick={() => setEditingLayout(false)}>
|
<Button loading={doneBtnClickedWhileSaving} type="primary" onClick={handleDoneEditing}>
|
||||||
{!doneBtnClickedWhileSaving && <i className="fa fa-check m-r-5" aria-hidden="true" />} Done Editing
|
{!doneBtnClickedWhileSaving && <i className="fa fa-check m-r-5" aria-hidden="true" />} Done Editing
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ export { DashboardStatusEnum } from "./useEditModeHandler";
|
|||||||
|
|
||||||
function getAffectedWidgets(widgets, updatedParameters = []) {
|
function getAffectedWidgets(widgets, updatedParameters = []) {
|
||||||
return !isEmpty(updatedParameters)
|
return !isEmpty(updatedParameters)
|
||||||
? widgets.filter(widget =>
|
? widgets.filter((widget) =>
|
||||||
Object.values(widget.getParameterMappings())
|
Object.values(widget.getParameterMappings())
|
||||||
.filter(({ type }) => type === "dashboard-level")
|
.filter(({ type }) => type === "dashboard-level")
|
||||||
.some(({ mapTo }) =>
|
.some(({ mapTo }) =>
|
||||||
includes(
|
includes(
|
||||||
updatedParameters.map(p => p.name),
|
updatedParameters.map((p) => p.name),
|
||||||
mapTo
|
mapTo
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -50,7 +50,7 @@ function useDashboard(dashboardData) {
|
|||||||
[dashboard]
|
[dashboard]
|
||||||
);
|
);
|
||||||
const hasOnlySafeQueries = useMemo(
|
const hasOnlySafeQueries = useMemo(
|
||||||
() => every(dashboard.widgets, w => (w.getQuery() ? w.getQuery().is_safe : true)),
|
() => every(dashboard.widgets, (w) => (w.getQuery() ? w.getQuery().is_safe : true)),
|
||||||
[dashboard]
|
[dashboard]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -67,19 +67,19 @@ function useDashboard(dashboardData) {
|
|||||||
|
|
||||||
const updateDashboard = useCallback(
|
const updateDashboard = useCallback(
|
||||||
(data, includeVersion = true) => {
|
(data, includeVersion = true) => {
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard, data));
|
setDashboard((currentDashboard) => extend({}, currentDashboard, data));
|
||||||
data = { ...data, id: dashboard.id };
|
data = { ...data, id: dashboard.id };
|
||||||
if (includeVersion) {
|
if (includeVersion) {
|
||||||
data = { ...data, version: dashboard.version };
|
data = { ...data, version: dashboard.version };
|
||||||
}
|
}
|
||||||
return Dashboard.save(data)
|
return Dashboard.save(data)
|
||||||
.then(updatedDashboard => {
|
.then((updatedDashboard) => {
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
|
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
|
||||||
if (has(data, "name")) {
|
if (has(data, "name")) {
|
||||||
location.setPath(url.parse(updatedDashboard.url).pathname, true);
|
location.setPath(url.parse(updatedDashboard.url).pathname, true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
const status = get(error, "response.status");
|
const status = get(error, "response.status");
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
notification.error("Dashboard update failed", "Permission Denied.");
|
notification.error("Dashboard update failed", "Permission Denied.");
|
||||||
@@ -102,25 +102,25 @@ function useDashboard(dashboardData) {
|
|||||||
|
|
||||||
const loadWidget = useCallback((widget, forceRefresh = false) => {
|
const loadWidget = useCallback((widget, forceRefresh = false) => {
|
||||||
widget.getParametersDefs(); // Force widget to read parameters values from URL
|
widget.getParametersDefs(); // Force widget to read parameters values from URL
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard));
|
setDashboard((currentDashboard) => extend({}, currentDashboard));
|
||||||
return widget
|
return widget
|
||||||
.load(forceRefresh)
|
.load(forceRefresh)
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
// QueryResultErrors are expected
|
// QueryResultErrors are expected
|
||||||
if (error instanceof QueryResultError) {
|
if (error instanceof QueryResultError) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
})
|
})
|
||||||
.finally(() => setDashboard(currentDashboard => extend({}, currentDashboard)));
|
.finally(() => setDashboard((currentDashboard) => extend({}, currentDashboard)));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refreshWidget = useCallback(widget => loadWidget(widget, true), [loadWidget]);
|
const refreshWidget = useCallback((widget) => loadWidget(widget, true), [loadWidget]);
|
||||||
|
|
||||||
const removeWidget = useCallback(widgetId => {
|
const removeWidget = useCallback((widgetId) => {
|
||||||
setDashboard(currentDashboard =>
|
setDashboard((currentDashboard) =>
|
||||||
extend({}, currentDashboard, {
|
extend({}, currentDashboard, {
|
||||||
widgets: currentDashboard.widgets.filter(widget => widget.id !== undefined && widget.id !== widgetId),
|
widgets: currentDashboard.widgets.filter((widget) => widget.id !== undefined && widget.id !== widgetId),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -132,11 +132,11 @@ function useDashboard(dashboardData) {
|
|||||||
(forceRefresh = false, updatedParameters = []) => {
|
(forceRefresh = false, updatedParameters = []) => {
|
||||||
const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters);
|
const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters);
|
||||||
const loadWidgetPromises = compact(
|
const loadWidgetPromises = compact(
|
||||||
affectedWidgets.map(widget => loadWidget(widget, forceRefresh).catch(error => error))
|
affectedWidgets.map((widget) => loadWidget(widget, forceRefresh).catch((error) => error))
|
||||||
);
|
);
|
||||||
|
|
||||||
return Promise.all(loadWidgetPromises).then(() => {
|
return Promise.all(loadWidgetPromises).then(() => {
|
||||||
const queryResults = compact(map(dashboardRef.current.widgets, widget => widget.getQueryResult()));
|
const queryResults = compact(map(dashboardRef.current.widgets, (widget) => widget.getQueryResult()));
|
||||||
const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search);
|
const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search);
|
||||||
setFilters(updatedFilters);
|
setFilters(updatedFilters);
|
||||||
});
|
});
|
||||||
@@ -145,7 +145,7 @@ function useDashboard(dashboardData) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const refreshDashboard = useCallback(
|
const refreshDashboard = useCallback(
|
||||||
updatedParameters => {
|
(updatedParameters) => {
|
||||||
if (!refreshing) {
|
if (!refreshing) {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
loadDashboard(true, updatedParameters).finally(() => setRefreshing(false));
|
loadDashboard(true, updatedParameters).finally(() => setRefreshing(false));
|
||||||
@@ -154,15 +154,30 @@ function useDashboard(dashboardData) {
|
|||||||
[refreshing, loadDashboard]
|
[refreshing, loadDashboard]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const saveDashboardParameters = useCallback(() => {
|
||||||
|
const currentDashboard = dashboardRef.current;
|
||||||
|
|
||||||
|
return updateDashboard({
|
||||||
|
options: {
|
||||||
|
...currentDashboard.options,
|
||||||
|
parameters: map(globalParameters, (p) => p.toSaveableObject()),
|
||||||
|
},
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to persist parameter values:", error);
|
||||||
|
notification.error("Parameter values could not be saved. Your changes may not be persisted.");
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}, [globalParameters, updateDashboard]);
|
||||||
|
|
||||||
const archiveDashboard = useCallback(() => {
|
const archiveDashboard = useCallback(() => {
|
||||||
recordEvent("archive", "dashboard", dashboard.id);
|
recordEvent("archive", "dashboard", dashboard.id);
|
||||||
Dashboard.delete(dashboard).then(updatedDashboard =>
|
Dashboard.delete(dashboard).then((updatedDashboard) =>
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
|
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
|
||||||
);
|
);
|
||||||
}, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const showShareDashboardDialog = useCallback(() => {
|
const showShareDashboardDialog = useCallback(() => {
|
||||||
const handleDialogClose = () => setDashboard(currentDashboard => extend({}, currentDashboard));
|
const handleDialogClose = () => setDashboard((currentDashboard) => extend({}, currentDashboard));
|
||||||
|
|
||||||
ShareDashboardDialog.showModal({
|
ShareDashboardDialog.showModal({
|
||||||
dashboard,
|
dashboard,
|
||||||
@@ -175,8 +190,8 @@ function useDashboard(dashboardData) {
|
|||||||
const showAddTextboxDialog = useCallback(() => {
|
const showAddTextboxDialog = useCallback(() => {
|
||||||
TextboxDialog.showModal({
|
TextboxDialog.showModal({
|
||||||
isNew: true,
|
isNew: true,
|
||||||
}).onClose(text =>
|
}).onClose((text) =>
|
||||||
dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard)))
|
dashboard.addWidget(text).then(() => setDashboard((currentDashboard) => extend({}, currentDashboard)))
|
||||||
);
|
);
|
||||||
}, [dashboard]);
|
}, [dashboard]);
|
||||||
|
|
||||||
@@ -188,13 +203,13 @@ function useDashboard(dashboardData) {
|
|||||||
.addWidget(visualization, {
|
.addWidget(visualization, {
|
||||||
parameterMappings: editableMappingsToParameterMappings(parameterMappings),
|
parameterMappings: editableMappingsToParameterMappings(parameterMappings),
|
||||||
})
|
})
|
||||||
.then(widget => {
|
.then((widget) => {
|
||||||
const widgetsToSave = [
|
const widgetsToSave = [
|
||||||
widget,
|
widget,
|
||||||
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
|
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
|
||||||
];
|
];
|
||||||
return Promise.all(widgetsToSave.map(w => w.save())).then(() =>
|
return Promise.all(widgetsToSave.map((w) => w.save())).then(() =>
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard))
|
setDashboard((currentDashboard) => extend({}, currentDashboard))
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -238,6 +253,7 @@ function useDashboard(dashboardData) {
|
|||||||
setRefreshRate,
|
setRefreshRate,
|
||||||
disableRefreshRate,
|
disableRefreshRate,
|
||||||
...editModeHandler,
|
...editModeHandler,
|
||||||
|
saveDashboardParameters,
|
||||||
gridDisabled,
|
gridDisabled,
|
||||||
setGridDisabled,
|
setGridDisabled,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ export const urlForDashboard = ({ id, slug }) => `dashboards/${id}-${slug}`;
|
|||||||
|
|
||||||
export function collectDashboardFilters(dashboard, queryResults, urlParams) {
|
export function collectDashboardFilters(dashboard, queryResults, urlParams) {
|
||||||
const filters = {};
|
const filters = {};
|
||||||
_.each(queryResults, queryResult => {
|
_.each(queryResults, (queryResult) => {
|
||||||
const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : [];
|
const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : [];
|
||||||
_.each(queryFilters, queryFilter => {
|
_.each(queryFilters, (queryFilter) => {
|
||||||
const hasQueryStringValue = _.has(urlParams, queryFilter.name);
|
const hasQueryStringValue = _.has(urlParams, queryFilter.name);
|
||||||
|
|
||||||
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
|
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
|
||||||
@@ -44,7 +44,7 @@ function prepareWidgetsForDashboard(widgets) {
|
|||||||
const defaultWidgetSizeY =
|
const defaultWidgetSizeY =
|
||||||
Math.max(
|
Math.max(
|
||||||
_.chain(widgets)
|
_.chain(widgets)
|
||||||
.map(w => w.options.position.sizeY)
|
.map((w) => w.options.position.sizeY)
|
||||||
.max()
|
.max()
|
||||||
.value(),
|
.value(),
|
||||||
20
|
20
|
||||||
@@ -55,11 +55,11 @@ function prepareWidgetsForDashboard(widgets) {
|
|||||||
// 2. update position of widgets in each row - place it right below
|
// 2. update position of widgets in each row - place it right below
|
||||||
// biggest widget from previous row
|
// biggest widget from previous row
|
||||||
_.chain(widgets)
|
_.chain(widgets)
|
||||||
.sortBy(widget => widget.options.position.row)
|
.sortBy((widget) => widget.options.position.row)
|
||||||
.groupBy(widget => widget.options.position.row)
|
.groupBy((widget) => widget.options.position.row)
|
||||||
.reduce((row, widgetsAtRow) => {
|
.reduce((row, widgetsAtRow) => {
|
||||||
let height = 1;
|
let height = 1;
|
||||||
_.each(widgetsAtRow, widget => {
|
_.each(widgetsAtRow, (widget) => {
|
||||||
height = Math.max(
|
height = Math.max(
|
||||||
height,
|
height,
|
||||||
widget.options.position.autoHeight ? defaultWidgetSizeY : widget.options.position.sizeY
|
widget.options.position.autoHeight ? defaultWidgetSizeY : widget.options.position.sizeY
|
||||||
@@ -74,8 +74,8 @@ function prepareWidgetsForDashboard(widgets) {
|
|||||||
.value();
|
.value();
|
||||||
|
|
||||||
// Sort widgets by updated column and row value
|
// Sort widgets by updated column and row value
|
||||||
widgets = _.sortBy(widgets, widget => widget.options.position.col);
|
widgets = _.sortBy(widgets, (widget) => widget.options.position.col);
|
||||||
widgets = _.sortBy(widgets, widget => widget.options.position.row);
|
widgets = _.sortBy(widgets, (widget) => widget.options.position.row);
|
||||||
|
|
||||||
return widgets;
|
return widgets;
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
|
|||||||
|
|
||||||
// Find first free row for each column
|
// Find first free row for each column
|
||||||
const bottomLine = _.chain(existingWidgets)
|
const bottomLine = _.chain(existingWidgets)
|
||||||
.map(w => {
|
.map((w) => {
|
||||||
const options = _.extend({}, w.options);
|
const options = _.extend({}, w.options);
|
||||||
const position = _.extend({ row: 0, sizeY: 0 }, options.position);
|
const position = _.extend({ row: 0, sizeY: 0 }, options.position);
|
||||||
return {
|
return {
|
||||||
@@ -97,21 +97,24 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
|
|||||||
height: position.sizeY,
|
height: position.sizeY,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.reduce((result, item) => {
|
.reduce(
|
||||||
|
(result, item) => {
|
||||||
const from = Math.max(item.left, 0);
|
const from = Math.max(item.left, 0);
|
||||||
const to = Math.min(item.right, result.length + 1);
|
const to = Math.min(item.right, result.length + 1);
|
||||||
for (let i = from; i < to; i += 1) {
|
for (let i = from; i < to; i += 1) {
|
||||||
result[i] = Math.max(result[i], item.bottom);
|
result[i] = Math.max(result[i], item.bottom);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, _.map(new Array(dashboardGridOptions.columns), _.constant(0)))
|
},
|
||||||
|
_.map(new Array(dashboardGridOptions.columns), _.constant(0))
|
||||||
|
)
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
// Go through columns, pick them by count necessary to hold new block,
|
// Go through columns, pick them by count necessary to hold new block,
|
||||||
// and calculate bottom-most free row per group.
|
// and calculate bottom-most free row per group.
|
||||||
// Choose group with the top-most free row (comparing to other groups)
|
// Choose group with the top-most free row (comparing to other groups)
|
||||||
return _.chain(_.range(0, dashboardGridOptions.columns - width + 1))
|
return _.chain(_.range(0, dashboardGridOptions.columns - width + 1))
|
||||||
.map(col => ({
|
.map((col) => ({
|
||||||
col,
|
col,
|
||||||
row: _.chain(bottomLine)
|
row: _.chain(bottomLine)
|
||||||
.slice(col, col + width)
|
.slice(col, col + width)
|
||||||
@@ -126,14 +129,14 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
|
|||||||
export function Dashboard(dashboard) {
|
export function Dashboard(dashboard) {
|
||||||
_.extend(this, dashboard);
|
_.extend(this, dashboard);
|
||||||
Object.defineProperty(this, "url", {
|
Object.defineProperty(this, "url", {
|
||||||
get: function() {
|
get: function () {
|
||||||
return urlForDashboard(this);
|
return urlForDashboard(this);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareDashboardWidgets(widgets) {
|
function prepareDashboardWidgets(widgets) {
|
||||||
return prepareWidgetsForDashboard(_.map(widgets, widget => new Widget(widget)));
|
return prepareWidgetsForDashboard(_.map(widgets, (widget) => new Widget(widget)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformSingle(dashboard) {
|
function transformSingle(dashboard) {
|
||||||
@@ -154,7 +157,7 @@ function transformResponse(data) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveOrCreateUrl = data => (data.id ? `api/dashboards/${data.id}` : "api/dashboards");
|
const saveOrCreateUrl = (data) => (data.id ? `api/dashboards/${data.id}` : "api/dashboards");
|
||||||
const DashboardService = {
|
const DashboardService = {
|
||||||
get: ({ id, slug }) => {
|
get: ({ id, slug }) => {
|
||||||
const params = {};
|
const params = {};
|
||||||
@@ -164,12 +167,12 @@ const DashboardService = {
|
|||||||
return axios.get(`api/dashboards/${id || slug}`, { params }).then(transformResponse);
|
return axios.get(`api/dashboards/${id || slug}`, { params }).then(transformResponse);
|
||||||
},
|
},
|
||||||
getByToken: ({ token }) => axios.get(`api/dashboards/public/${token}`).then(transformResponse),
|
getByToken: ({ token }) => axios.get(`api/dashboards/public/${token}`).then(transformResponse),
|
||||||
save: data => axios.post(saveOrCreateUrl(data), data).then(transformResponse),
|
save: (data) => axios.post(saveOrCreateUrl(data), data).then(transformResponse),
|
||||||
delete: ({ id }) => axios.delete(`api/dashboards/${id}`).then(transformResponse),
|
delete: ({ id }) => axios.delete(`api/dashboards/${id}`).then(transformResponse),
|
||||||
query: params => axios.get("api/dashboards", { params }).then(transformResponse),
|
query: (params) => axios.get("api/dashboards", { params }).then(transformResponse),
|
||||||
recent: params => axios.get("api/dashboards/recent", { params }).then(transformResponse),
|
recent: (params) => axios.get("api/dashboards/recent", { params }).then(transformResponse),
|
||||||
myDashboards: params => axios.get("api/dashboards/my", { params }).then(transformResponse),
|
myDashboards: (params) => axios.get("api/dashboards/my", { params }).then(transformResponse),
|
||||||
favorites: params => axios.get("api/dashboards/favorites", { params }).then(transformResponse),
|
favorites: (params) => axios.get("api/dashboards/favorites", { params }).then(transformResponse),
|
||||||
favorite: ({ id }) => axios.post(`api/dashboards/${id}/favorite`),
|
favorite: ({ id }) => axios.post(`api/dashboards/${id}/favorite`),
|
||||||
unfavorite: ({ id }) => axios.delete(`api/dashboards/${id}/favorite`),
|
unfavorite: ({ id }) => axios.delete(`api/dashboards/${id}/favorite`),
|
||||||
fork: ({ id }) => axios.post(`api/dashboards/${id}/fork`, { id }).then(transformResponse),
|
fork: ({ id }) => axios.post(`api/dashboards/${id}/fork`, { id }).then(transformResponse),
|
||||||
@@ -187,13 +190,13 @@ Dashboard.prototype.canEdit = function canEdit() {
|
|||||||
Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
||||||
const globalParams = {};
|
const globalParams = {};
|
||||||
const queryParams = location.search;
|
const queryParams = location.search;
|
||||||
_.each(this.widgets, widget => {
|
_.each(this.widgets, (widget) => {
|
||||||
if (widget.getQuery()) {
|
if (widget.getQuery()) {
|
||||||
const mappings = widget.getParameterMappings();
|
const mappings = widget.getParameterMappings();
|
||||||
widget
|
widget
|
||||||
.getQuery()
|
.getQuery()
|
||||||
.getParametersDefs(false)
|
.getParametersDefs(false)
|
||||||
.forEach(param => {
|
.forEach((param) => {
|
||||||
const mapping = mappings[param.name];
|
const mapping = mappings[param.name];
|
||||||
if (mapping.type === Widget.MappingType.DashboardLevel) {
|
if (mapping.type === Widget.MappingType.DashboardLevel) {
|
||||||
// create global param
|
// create global param
|
||||||
@@ -210,15 +213,19 @@ Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const mergedValues = {
|
||||||
|
..._.mapValues(globalParams, (p) => p.value),
|
||||||
|
...Object.fromEntries((this.options.parameters || []).map((param) => [param.name, param.value])),
|
||||||
|
};
|
||||||
const resultingGlobalParams = _.values(
|
const resultingGlobalParams = _.values(
|
||||||
_.each(globalParams, param => {
|
_.each(globalParams, (param) => {
|
||||||
param.setValue(param.value); // apply global param value to all locals
|
param.setValue(mergedValues[param.name]); // apply merged value
|
||||||
param.fromUrlParams(queryParams); // try to initialize from url (may do nothing)
|
param.fromUrlParams(queryParams); // allow param-specific parsing logic
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// order dashboard params using paramOrder
|
// order dashboard params using paramOrder
|
||||||
return _.sortBy(resultingGlobalParams, param =>
|
return _.sortBy(resultingGlobalParams, (param) =>
|
||||||
_.includes(this.options.globalParamOrder, param.name)
|
_.includes(this.options.globalParamOrder, param.name)
|
||||||
? _.indexOf(this.options.globalParamOrder, param.name)
|
? _.indexOf(this.options.globalParamOrder, param.name)
|
||||||
: _.size(this.options.globalParamOrder)
|
: _.size(this.options.globalParamOrder)
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ const DYNAMIC_PREFIX = "d_";
|
|||||||
* @param now {function(): moment.Moment=} moment - defaults to now
|
* @param now {function(): moment.Moment=} moment - defaults to now
|
||||||
* @returns {function(withNow: boolean): [moment.Moment, moment.Moment|undefined]}
|
* @returns {function(withNow: boolean): [moment.Moment, moment.Moment|undefined]}
|
||||||
*/
|
*/
|
||||||
const untilNow = (from, now = () => moment()) => (withNow = true) => [from(), withNow ? now() : undefined];
|
const untilNow =
|
||||||
|
(from, now = () => moment()) =>
|
||||||
|
(withNow = true) => [from(), withNow ? now() : undefined];
|
||||||
|
|
||||||
const DYNAMIC_DATE_RANGES = {
|
const DYNAMIC_DATE_RANGES = {
|
||||||
today: {
|
today: {
|
||||||
@@ -26,14 +28,7 @@ const DYNAMIC_DATE_RANGES = {
|
|||||||
},
|
},
|
||||||
yesterday: {
|
yesterday: {
|
||||||
name: "Yesterday",
|
name: "Yesterday",
|
||||||
value: () => [
|
value: () => [moment().subtract(1, "day").startOf("day"), moment().subtract(1, "day").endOf("day")],
|
||||||
moment()
|
|
||||||
.subtract(1, "day")
|
|
||||||
.startOf("day"),
|
|
||||||
moment()
|
|
||||||
.subtract(1, "day")
|
|
||||||
.endOf("day"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
this_week: {
|
this_week: {
|
||||||
name: "This week",
|
name: "This week",
|
||||||
@@ -49,36 +44,15 @@ const DYNAMIC_DATE_RANGES = {
|
|||||||
},
|
},
|
||||||
last_week: {
|
last_week: {
|
||||||
name: "Last week",
|
name: "Last week",
|
||||||
value: () => [
|
value: () => [moment().subtract(1, "week").startOf("week"), moment().subtract(1, "week").endOf("week")],
|
||||||
moment()
|
|
||||||
.subtract(1, "week")
|
|
||||||
.startOf("week"),
|
|
||||||
moment()
|
|
||||||
.subtract(1, "week")
|
|
||||||
.endOf("week"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
last_month: {
|
last_month: {
|
||||||
name: "Last month",
|
name: "Last month",
|
||||||
value: () => [
|
value: () => [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")],
|
||||||
moment()
|
|
||||||
.subtract(1, "month")
|
|
||||||
.startOf("month"),
|
|
||||||
moment()
|
|
||||||
.subtract(1, "month")
|
|
||||||
.endOf("month"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
last_year: {
|
last_year: {
|
||||||
name: "Last year",
|
name: "Last year",
|
||||||
value: () => [
|
value: () => [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")],
|
||||||
moment()
|
|
||||||
.subtract(1, "year")
|
|
||||||
.startOf("year"),
|
|
||||||
moment()
|
|
||||||
.subtract(1, "year")
|
|
||||||
.endOf("year"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
last_hour: {
|
last_hour: {
|
||||||
name: "Last hour",
|
name: "Last hour",
|
||||||
@@ -94,63 +68,31 @@ const DYNAMIC_DATE_RANGES = {
|
|||||||
},
|
},
|
||||||
last_7_days: {
|
last_7_days: {
|
||||||
name: "Last 7 days",
|
name: "Last 7 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(7, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(7, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_14_days: {
|
last_14_days: {
|
||||||
name: "Last 14 days",
|
name: "Last 14 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(14, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(14, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_30_days: {
|
last_30_days: {
|
||||||
name: "Last 30 days",
|
name: "Last 30 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(30, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(30, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_60_days: {
|
last_60_days: {
|
||||||
name: "Last 60 days",
|
name: "Last 60 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(60, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(60, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_90_days: {
|
last_90_days: {
|
||||||
name: "Last 90 days",
|
name: "Last 90 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(90, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(90, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_12_months: {
|
last_12_months: {
|
||||||
name: "Last 12 months",
|
name: "Last 12 months",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(12, "months").startOf("day")),
|
||||||
() =>
|
},
|
||||||
moment()
|
last_10_years: {
|
||||||
.subtract(12, "months")
|
name: "Last 10 years",
|
||||||
.startOf("day"),
|
value: untilNow(() => moment().subtract(10, "years").startOf("day")),
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +106,7 @@ export function isDynamicDateRangeString(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDynamicDateRangeStringFromName(dynamicRangeName) {
|
export function getDynamicDateRangeStringFromName(dynamicRangeName) {
|
||||||
const key = findKey(DYNAMIC_DATE_RANGES, range => range.name === dynamicRangeName);
|
const key = findKey(DYNAMIC_DATE_RANGES, (range) => range.name === dynamicRangeName);
|
||||||
return key ? DYNAMIC_PREFIX + key : undefined;
|
return key ? DYNAMIC_PREFIX + key : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +175,7 @@ class DateRangeParameter extends Parameter {
|
|||||||
|
|
||||||
getExecutionValue() {
|
getExecutionValue() {
|
||||||
if (this.hasDynamicValue) {
|
if (this.hasDynamicValue) {
|
||||||
const format = date => date.format(DATETIME_FORMATS[this.type]);
|
const format = (date) => date.format(DATETIME_FORMATS[this.type]);
|
||||||
const [start, end] = this.normalizedValue.value().map(format);
|
const [start, end] = this.normalizedValue.value().map(format);
|
||||||
return { start, end };
|
return { start, end };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class Parameter {
|
|||||||
|
|
||||||
updateLocals() {
|
updateLocals() {
|
||||||
if (isArray(this.locals)) {
|
if (isArray(this.locals)) {
|
||||||
each(this.locals, local => {
|
each(this.locals, (local) => {
|
||||||
local.setValue(this.value);
|
local.setValue(this.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ class Parameter {
|
|||||||
|
|
||||||
/** Get a saveable version of the Parameter by omitting unnecessary props */
|
/** Get a saveable version of the Parameter by omitting unnecessary props */
|
||||||
toSaveableObject() {
|
toSaveableObject() {
|
||||||
return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId"]);
|
return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId", "locals"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe("Dashboard Filters", () => {
|
|||||||
name: "Query Filters",
|
name: "Query Filters",
|
||||||
query: `SELECT stage1 AS "stage1::filter", stage2, value FROM (${SQL}) q`,
|
query: `SELECT stage1 AS "stage1::filter", stage2, value FROM (${SQL}) q`,
|
||||||
};
|
};
|
||||||
cy.createDashboard("Dashboard Filters").then(dashboard => {
|
cy.createDashboard("Dashboard Filters").then((dashboard) => {
|
||||||
createQueryAndAddWidget(dashboard.id, queryData)
|
createQueryAndAddWidget(dashboard.id, queryData)
|
||||||
.as("widget1TestId")
|
.as("widget1TestId")
|
||||||
.then(() => createQueryAndAddWidget(dashboard.id, queryData, { position: { col: 4 } }))
|
.then(() => createQueryAndAddWidget(dashboard.id, queryData, { position: { col: 4 } }))
|
||||||
@@ -32,26 +32,23 @@ describe("Dashboard Filters", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters rows in a Table Visualization", function() {
|
it("filters rows in a Table Visualization", function () {
|
||||||
editDashboard();
|
editDashboard();
|
||||||
cy.getByTestId("DashboardFilters").should("not.exist");
|
cy.getByTestId("DashboardFilters").should("not.exist");
|
||||||
cy.getByTestId("DashboardFiltersCheckbox").click();
|
cy.getByTestId("DashboardFiltersCheckbox").click();
|
||||||
|
|
||||||
cy.getByTestId("DashboardFilters").within(() => {
|
cy.getByTestId("DashboardFilters").within(() => {
|
||||||
cy.getByTestId("FilterName-stage1::filter")
|
cy.getByTestId("FilterName-stage1::filter").find(".ant-select-selection-item").should("have.text", "a");
|
||||||
.find(".ant-select-selection-item")
|
|
||||||
.should("have.text", "a");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId(this.widget1TestId).within(() => {
|
cy.getByTestId(this.widget1TestId).within(() => {
|
||||||
expectTableToHaveLength(4);
|
expectTableToHaveLength(4);
|
||||||
expectFirstColumnToHaveMembers(["a", "a", "a", "a"]);
|
expectFirstColumnToHaveMembers(["a", "a", "a", "a"]);
|
||||||
|
|
||||||
cy.getByTestId("FilterName-stage1::filter")
|
cy.getByTestId("FilterName-stage1::filter").find(".ant-select").click();
|
||||||
.find(".ant-select")
|
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.contains(".ant-select-item-option-content:visible", "b").click();
|
cy.contains(".ant-select-item-option-content:visible", "b").click();
|
||||||
|
|
||||||
cy.getByTestId(this.widget1TestId).within(() => {
|
cy.getByTestId(this.widget1TestId).within(() => {
|
||||||
@@ -69,14 +66,13 @@ describe("Dashboard Filters", () => {
|
|||||||
// assert that changing a global filter affects all widgets
|
// assert that changing a global filter affects all widgets
|
||||||
|
|
||||||
cy.getByTestId("DashboardFilters").within(() => {
|
cy.getByTestId("DashboardFilters").within(() => {
|
||||||
cy.getByTestId("FilterName-stage1::filter")
|
cy.getByTestId("FilterName-stage1::filter").find(".ant-select").click();
|
||||||
.find(".ant-select")
|
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.contains(".ant-select-item-option-content:visible", "c").click();
|
cy.contains(".ant-select-item-option-content:visible", "c").click();
|
||||||
|
|
||||||
[this.widget1TestId, this.widget2TestId].forEach(widgetTestId =>
|
[this.widget1TestId, this.widget2TestId].forEach((widgetTestId) =>
|
||||||
cy.getByTestId(widgetTestId).within(() => {
|
cy.getByTestId(widgetTestId).within(() => {
|
||||||
expectTableToHaveLength(4);
|
expectTableToHaveLength(4);
|
||||||
expectFirstColumnToHaveMembers(["c", "c", "c", "c"]);
|
expectFirstColumnToHaveMembers(["c", "c", "c", "c"]);
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ describe("Embedded Queries", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("is unavailable when public urls feature is disabled", () => {
|
it("is unavailable when public urls feature is disabled", () => {
|
||||||
cy.createQuery({ query: "select name from users order by name" }).then(query => {
|
cy.createQuery({ query: "select name from users order by name" }).then((query) => {
|
||||||
cy.visit(`/queries/${query.id}/source`);
|
cy.visit(`/queries/${query.id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
||||||
cy.clickThrough(`
|
cy.clickThrough(`
|
||||||
@@ -15,7 +16,7 @@ describe("Embedded Queries", () => {
|
|||||||
`);
|
`);
|
||||||
cy.getByTestId("EmbedIframe")
|
cy.getByTestId("EmbedIframe")
|
||||||
.invoke("text")
|
.invoke("text")
|
||||||
.then(embedUrl => {
|
.then((embedUrl) => {
|
||||||
// disable the feature
|
// disable the feature
|
||||||
cy.updateOrgSettings({ disable_public_urls: true });
|
cy.updateOrgSettings({ disable_public_urls: true });
|
||||||
|
|
||||||
@@ -23,9 +24,7 @@ describe("Embedded Queries", () => {
|
|||||||
cy.visit(`/queries/${query.id}/source`);
|
cy.visit(`/queries/${query.id}/source`);
|
||||||
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
||||||
cy.getByTestId("QueryPageHeaderMoreButton").click();
|
cy.getByTestId("QueryPageHeaderMoreButton").click();
|
||||||
cy.get(".ant-dropdown-menu-item")
|
cy.get(".ant-dropdown-menu-item").should("exist").should("not.contain", "Show API Key");
|
||||||
.should("exist")
|
|
||||||
.should("not.contain", "Show API Key");
|
|
||||||
cy.getByTestId("QueryControlDropdownButton").click();
|
cy.getByTestId("QueryControlDropdownButton").click();
|
||||||
cy.get(".ant-dropdown-menu-item").should("exist");
|
cy.get(".ant-dropdown-menu-item").should("exist");
|
||||||
cy.getByTestId("ShowEmbedDialogButton").should("not.exist");
|
cy.getByTestId("ShowEmbedDialogButton").should("not.exist");
|
||||||
@@ -42,8 +41,9 @@ describe("Embedded Queries", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("can be shared without parameters", () => {
|
it("can be shared without parameters", () => {
|
||||||
cy.createQuery({ query: "select name from users order by name" }).then(query => {
|
cy.createQuery({ query: "select name from users order by name" }).then((query) => {
|
||||||
cy.visit(`/queries/${query.id}/source`);
|
cy.visit(`/queries/${query.id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
||||||
cy.clickThrough(`
|
cy.clickThrough(`
|
||||||
@@ -52,7 +52,7 @@ describe("Embedded Queries", () => {
|
|||||||
`);
|
`);
|
||||||
cy.getByTestId("EmbedIframe")
|
cy.getByTestId("EmbedIframe")
|
||||||
.invoke("text")
|
.invoke("text")
|
||||||
.then(embedUrl => {
|
.then((embedUrl) => {
|
||||||
cy.logout();
|
cy.logout();
|
||||||
cy.visit(embedUrl);
|
cy.visit(embedUrl);
|
||||||
cy.getByTestId("VisualizationEmbed", { timeout: 10000 }).should("exist");
|
cy.getByTestId("VisualizationEmbed", { timeout: 10000 }).should("exist");
|
||||||
@@ -90,7 +90,7 @@ describe("Embedded Queries", () => {
|
|||||||
|
|
||||||
cy.getByTestId("EmbedIframe")
|
cy.getByTestId("EmbedIframe")
|
||||||
.invoke("text")
|
.invoke("text")
|
||||||
.then(embedUrl => {
|
.then((embedUrl) => {
|
||||||
cy.logout();
|
cy.logout();
|
||||||
cy.visit(embedUrl);
|
cy.visit(embedUrl);
|
||||||
cy.getByTestId("VisualizationEmbed", { timeout: 10000 }).should("exist");
|
cy.getByTestId("VisualizationEmbed", { timeout: 10000 }).should("exist");
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ describe("Box Plot", () => {
|
|||||||
.then(({ id }) => cy.createVisualization(id, "BOXPLOT", "Boxplot (Deprecated)", {}))
|
.then(({ id }) => cy.createVisualization(id, "BOXPLOT", "Boxplot (Deprecated)", {}))
|
||||||
.then(({ id: visualizationId, query_id: queryId }) => {
|
.then(({ id: visualizationId, query_id: queryId }) => {
|
||||||
cy.visit(`queries/${queryId}/source#${visualizationId}`);
|
cy.visit(`queries/${queryId}/source#${visualizationId}`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -61,9 +62,7 @@ describe("Box Plot", () => {
|
|||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("svg").should("exist");
|
||||||
.find("svg")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Box Plot", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Box Plot", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ describe("Chart", () => {
|
|||||||
|
|
||||||
it("creates Bar charts", function () {
|
it("creates Bar charts", function () {
|
||||||
cy.visit(`queries/${this.queryId}/source`);
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
|
||||||
const getBarChartAssertionFunction =
|
const getBarChartAssertionFunction =
|
||||||
@@ -109,6 +110,7 @@ describe("Chart", () => {
|
|||||||
});
|
});
|
||||||
it("colors Bar charts", function () {
|
it("colors Bar charts", function () {
|
||||||
cy.visit(`queries/${this.queryId}/source`);
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
cy.getByTestId("Chart.ColumnMapping.x").selectAntdOption("Chart.ColumnMapping.x.stage");
|
cy.getByTestId("Chart.ColumnMapping.x").selectAntdOption("Chart.ColumnMapping.x.stage");
|
||||||
@@ -123,6 +125,7 @@ describe("Chart", () => {
|
|||||||
});
|
});
|
||||||
it("colors Pie charts", function () {
|
it("colors Pie charts", function () {
|
||||||
cy.visit(`queries/${this.queryId}/source`);
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
cy.getByTestId("Chart.GlobalSeriesType").click();
|
cy.getByTestId("Chart.GlobalSeriesType").click();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ describe("Choropleth", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
@@ -76,9 +77,7 @@ describe("Choropleth", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".map-visualization-container.leaflet-container").should("exist");
|
||||||
.find(".map-visualization-container.leaflet-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Choropleth", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Choropleth", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe("Cohort", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
@@ -51,9 +52,7 @@ describe("Cohort", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
|
||||||
.should("exist");
|
|
||||||
cy.percySnapshot("Visualizations - Cohort (simple)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Cohort (simple)", { widths: [viewportWidth] });
|
||||||
|
|
||||||
cy.clickThrough(`
|
cy.clickThrough(`
|
||||||
@@ -64,9 +63,7 @@ describe("Cohort", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
|
||||||
.should("exist");
|
|
||||||
cy.percySnapshot("Visualizations - Cohort (diagonal)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Cohort (diagonal)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ describe("Counter", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
@@ -24,9 +25,7 @@ describe("Counter", () => {
|
|||||||
Counter.General.ValueColumn.a
|
Counter.General.ValueColumn.a
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -43,9 +42,7 @@ describe("Counter", () => {
|
|||||||
"Counter.General.Label": "Custom Label",
|
"Counter.General.Label": "Custom Label",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -65,9 +62,7 @@ describe("Counter", () => {
|
|||||||
"Counter.General.TargetValueRowNumber": "2",
|
"Counter.General.TargetValueRowNumber": "2",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -83,9 +78,7 @@ describe("Counter", () => {
|
|||||||
Counter.General.TargetValueColumn.b
|
Counter.General.TargetValueColumn.b
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -106,9 +99,7 @@ describe("Counter", () => {
|
|||||||
"Counter.General.TargetValueRowNumber": "2",
|
"Counter.General.TargetValueRowNumber": "2",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -123,9 +114,7 @@ describe("Counter", () => {
|
|||||||
Counter.General.CountRows
|
Counter.General.CountRows
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -151,9 +140,7 @@ describe("Counter", () => {
|
|||||||
"Counter.Formatting.StringSuffix": "%",
|
"Counter.Formatting.StringSuffix": "%",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -180,9 +167,7 @@ describe("Counter", () => {
|
|||||||
"Counter.Formatting.StringSuffix": "%",
|
"Counter.Formatting.StringSuffix": "%",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|||||||
@@ -5,34 +5,25 @@ describe("Edit visualization dialog", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery().then(({ id }) => {
|
cy.createQuery().then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens New Visualization dialog", () => {
|
it("opens New Visualization dialog", () => {
|
||||||
cy.getByTestId("NewVisualization")
|
cy.getByTestId("NewVisualization").should("exist").click();
|
||||||
.should("exist")
|
|
||||||
.click();
|
|
||||||
cy.getByTestId("EditVisualizationDialog").should("exist");
|
cy.getByTestId("EditVisualizationDialog").should("exist");
|
||||||
// Default visualization should be selected
|
// Default visualization should be selected
|
||||||
cy.getByTestId("VisualizationType")
|
cy.getByTestId("VisualizationType").should("exist").should("contain", "Chart");
|
||||||
.should("exist")
|
cy.getByTestId("VisualizationName").should("exist").should("have.value", "Chart");
|
||||||
.should("contain", "Chart");
|
|
||||||
cy.getByTestId("VisualizationName")
|
|
||||||
.should("exist")
|
|
||||||
.should("have.value", "Chart");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens Edit Visualization dialog", () => {
|
it("opens Edit Visualization dialog", () => {
|
||||||
cy.getByTestId("EditVisualization").click();
|
cy.getByTestId("EditVisualization").click();
|
||||||
cy.getByTestId("EditVisualizationDialog").should("exist");
|
cy.getByTestId("EditVisualizationDialog").should("exist");
|
||||||
// Default `Table` visualization should be selected
|
// Default `Table` visualization should be selected
|
||||||
cy.getByTestId("VisualizationType")
|
cy.getByTestId("VisualizationType").should("exist").should("contain", "Table");
|
||||||
.should("exist")
|
cy.getByTestId("VisualizationName").should("exist").should("have.value", "Table");
|
||||||
.should("contain", "Table");
|
|
||||||
cy.getByTestId("VisualizationName")
|
|
||||||
.should("exist")
|
|
||||||
.should("have.value", "Table");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates visualization with custom name", () => {
|
it("creates visualization with custom name", () => {
|
||||||
@@ -44,15 +35,9 @@ describe("Edit visualization dialog", () => {
|
|||||||
VisualizationType.TABLE
|
VisualizationType.TABLE
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("VisualizationName")
|
cy.getByTestId("VisualizationName").clear().type(visualizationName);
|
||||||
.clear()
|
|
||||||
.type(visualizationName);
|
|
||||||
|
|
||||||
cy.getByTestId("EditVisualizationDialog")
|
cy.getByTestId("EditVisualizationDialog").contains("button", "Save").click();
|
||||||
.contains("button", "Save")
|
cy.getByTestId("QueryPageVisualizationTabs").contains("span", visualizationName).should("exist");
|
||||||
.click();
|
|
||||||
cy.getByTestId("QueryPageVisualizationTabs")
|
|
||||||
.contains("span", visualizationName)
|
|
||||||
.should("exist");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ describe("Funnel", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -59,9 +60,7 @@ describe("Funnel", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
|
||||||
.should("exist");
|
|
||||||
cy.percySnapshot("Visualizations - Funnel (basic)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Funnel (basic)", { widths: [viewportWidth] });
|
||||||
|
|
||||||
cy.clickThrough(`
|
cy.clickThrough(`
|
||||||
@@ -81,9 +80,7 @@ describe("Funnel", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
|
||||||
.should("exist");
|
|
||||||
cy.percySnapshot("Visualizations - Funnel (extra options)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Funnel (extra options)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe("Map (Markers)", () => {
|
|||||||
.then(({ id }) => cy.createVisualization(id, "MAP", "Map (Markers)", { mapTileUrl }))
|
.then(({ id }) => cy.createVisualization(id, "MAP", "Map (Markers)", { mapTileUrl }))
|
||||||
.then(({ id: visualizationId, query_id: queryId }) => {
|
.then(({ id: visualizationId, query_id: queryId }) => {
|
||||||
cy.visit(`queries/${queryId}/source#${visualizationId}`);
|
cy.visit(`queries/${queryId}/source#${visualizationId}`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -51,9 +52,7 @@ describe("Map (Markers)", () => {
|
|||||||
cy.fillInputs({ "ColorPicker.CustomColor": "blue{enter}" });
|
cy.fillInputs({ "ColorPicker.CustomColor": "blue{enter}" });
|
||||||
cy.getByTestId("ColorPicker.CustomColor").should("not.be.visible");
|
cy.getByTestId("ColorPicker.CustomColor").should("not.be.visible");
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".leaflet-control-zoom-in").click();
|
||||||
.find(".leaflet-control-zoom-in")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -85,9 +84,7 @@ describe("Map (Markers)", () => {
|
|||||||
cy.fillInputs({ "ColorPicker.CustomColor": "maroon{enter}" });
|
cy.fillInputs({ "ColorPicker.CustomColor": "maroon{enter}" });
|
||||||
cy.getByTestId("ColorPicker.CustomColor").should("not.be.visible");
|
cy.getByTestId("ColorPicker.CustomColor").should("not.be.visible");
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".leaflet-control-zoom-in").click();
|
||||||
.find(".leaflet-control-zoom-in")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ const SQL = `
|
|||||||
function createPivotThroughUI(visualizationName, options = {}) {
|
function createPivotThroughUI(visualizationName, options = {}) {
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.PIVOT");
|
cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.PIVOT");
|
||||||
cy.getByTestId("VisualizationName")
|
cy.getByTestId("VisualizationName").clear().type(visualizationName);
|
||||||
.clear()
|
|
||||||
.type(visualizationName);
|
|
||||||
if (options.hideControls) {
|
if (options.hideControls) {
|
||||||
cy.getByTestId("PivotEditor.HideControls").click();
|
cy.getByTestId("PivotEditor.HideControls").click();
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview")
|
||||||
@@ -29,36 +27,30 @@ function createPivotThroughUI(visualizationName, options = {}) {
|
|||||||
.find(".pvtAxisContainer, .pvtRenderer, .pvtVals")
|
.find(".pvtAxisContainer, .pvtRenderer, .pvtVals")
|
||||||
.should("be.not.visible");
|
.should("be.not.visible");
|
||||||
}
|
}
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
cy.getByTestId("EditVisualizationDialog").contains("button", "Save").click();
|
||||||
.should("exist");
|
|
||||||
cy.getByTestId("EditVisualizationDialog")
|
|
||||||
.contains("button", "Save")
|
|
||||||
.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Pivot", () => {
|
describe("Pivot", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ name: "Pivot Visualization", query: SQL })
|
cy.createQuery({ name: "Pivot Visualization", query: SQL }).its("id").as("queryId");
|
||||||
.its("id")
|
|
||||||
.as("queryId");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates Pivot with controls", function() {
|
it("creates Pivot with controls", function () {
|
||||||
cy.visit(`queries/${this.queryId}/source`);
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
|
||||||
const visualizationName = "Pivot";
|
const visualizationName = "Pivot";
|
||||||
createPivotThroughUI(visualizationName);
|
createPivotThroughUI(visualizationName);
|
||||||
|
|
||||||
cy.getByTestId("QueryPageVisualizationTabs")
|
cy.getByTestId("QueryPageVisualizationTabs").contains("span", visualizationName).should("exist");
|
||||||
.contains("span", visualizationName)
|
|
||||||
.should("exist");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates Pivot without controls", function() {
|
it("creates Pivot without controls", function () {
|
||||||
cy.visit(`queries/${this.queryId}/source`);
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
|
||||||
const visualizationName = "Pivot";
|
const visualizationName = "Pivot";
|
||||||
@@ -76,7 +68,7 @@ describe("Pivot", () => {
|
|||||||
.should("be.not.visible");
|
.should("be.not.visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the visualization when results change", function() {
|
it("updates the visualization when results change", function () {
|
||||||
const options = {
|
const options = {
|
||||||
aggregatorName: "Count",
|
aggregatorName: "Count",
|
||||||
data: [], // force it to have a data object, although it shouldn't
|
data: [], // force it to have a data object, although it shouldn't
|
||||||
@@ -86,8 +78,9 @@ describe("Pivot", () => {
|
|||||||
vals: ["value"],
|
vals: ["value"],
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.createVisualization(this.queryId, "PIVOT", "Pivot", options).then(visualization => {
|
cy.createVisualization(this.queryId, "PIVOT", "Pivot", options).then((visualization) => {
|
||||||
cy.visit(`queries/${this.queryId}/source#${visualization.id}`);
|
cy.visit(`queries/${this.queryId}/source#${visualization.id}`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
|
||||||
// assert number of rows is 11
|
// assert number of rows is 11
|
||||||
@@ -104,16 +97,14 @@ describe("Pivot", () => {
|
|||||||
cy.wait(200);
|
cy.wait(200);
|
||||||
|
|
||||||
cy.getByTestId("SaveButton").click();
|
cy.getByTestId("SaveButton").click();
|
||||||
cy.getByTestId("ExecuteButton")
|
cy.getByTestId("ExecuteButton").should("be.enabled").click();
|
||||||
.should("be.enabled")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// assert number of rows is 12
|
// assert number of rows is 12
|
||||||
cy.getByTestId("PivotTableVisualization").contains(".pvtGrandTotal", "12");
|
cy.getByTestId("PivotTableVisualization").contains(".pvtGrandTotal", "12");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("takes a snapshot with different configured Pivots", function() {
|
it("takes a snapshot with different configured Pivots", function () {
|
||||||
const options = {
|
const options = {
|
||||||
aggregatorName: "Sum",
|
aggregatorName: "Sum",
|
||||||
controls: { enabled: true },
|
controls: { enabled: true },
|
||||||
@@ -142,19 +133,20 @@ describe("Pivot", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
cy.createDashboard("Pivot Visualization")
|
cy.createDashboard("Pivot Visualization")
|
||||||
.then(dashboard => {
|
.then((dashboard) => {
|
||||||
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
||||||
return cy.all(
|
return cy.all(
|
||||||
pivotTables.map(pivot => () =>
|
pivotTables.map(
|
||||||
|
(pivot) => () =>
|
||||||
cy
|
cy
|
||||||
.createVisualization(this.queryId, "PIVOT", pivot.name, pivot.options)
|
.createVisualization(this.queryId, "PIVOT", pivot.name, pivot.options)
|
||||||
.then(visualization => cy.addWidget(dashboard.id, visualization.id, { position: pivot.position }))
|
.then((visualization) => cy.addWidget(dashboard.id, visualization.id, { position: pivot.position }))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.then(widgets => {
|
.then((widgets) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
widgets.forEach(widget => {
|
widgets.forEach((widget) => {
|
||||||
cy.getByTestId(getWidgetTestId(widget)).within(() =>
|
cy.getByTestId(getWidgetTestId(widget)).within(() =>
|
||||||
cy.getByTestId("PivotTableVisualization").should("exist")
|
cy.getByTestId("PivotTableVisualization").should("exist")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ describe("Sankey and Sunburst", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.SUNBURST_SEQUENCE");
|
cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.SUNBURST_SEQUENCE");
|
||||||
@@ -34,37 +35,21 @@ describe("Sankey and Sunburst", () => {
|
|||||||
it("creates Sunburst", () => {
|
it("creates Sunburst", () => {
|
||||||
const visualizationName = "Sunburst";
|
const visualizationName = "Sunburst";
|
||||||
|
|
||||||
cy.getByTestId("VisualizationName")
|
cy.getByTestId("VisualizationName").clear().type(visualizationName);
|
||||||
.clear()
|
cy.getByTestId("VisualizationPreview").find("svg").should("exist");
|
||||||
.type(visualizationName);
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
|
||||||
.find("svg")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.getByTestId("EditVisualizationDialog")
|
cy.getByTestId("EditVisualizationDialog").contains("button", "Save").click();
|
||||||
.contains("button", "Save")
|
cy.getByTestId("QueryPageVisualizationTabs").contains("span", visualizationName).should("exist");
|
||||||
.click();
|
|
||||||
cy.getByTestId("QueryPageVisualizationTabs")
|
|
||||||
.contains("span", visualizationName)
|
|
||||||
.should("exist");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates Sankey", () => {
|
it("creates Sankey", () => {
|
||||||
const visualizationName = "Sankey";
|
const visualizationName = "Sankey";
|
||||||
|
|
||||||
cy.getByTestId("VisualizationName")
|
cy.getByTestId("VisualizationName").clear().type(visualizationName);
|
||||||
.clear()
|
cy.getByTestId("VisualizationPreview").find("svg").should("exist");
|
||||||
.type(visualizationName);
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
|
||||||
.find("svg")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.getByTestId("EditVisualizationDialog")
|
cy.getByTestId("EditVisualizationDialog").contains("button", "Save").click();
|
||||||
.contains("button", "Save")
|
cy.getByTestId("QueryPageVisualizationTabs").contains("span", visualizationName).should("exist");
|
||||||
.click();
|
|
||||||
cy.getByTestId("QueryPageVisualizationTabs")
|
|
||||||
.contains("span", visualizationName)
|
|
||||||
.should("exist");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,21 +77,22 @@ describe("Sankey and Sunburst", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
it("takes a snapshot with Sunburst (1 - 5 stages)", function() {
|
it("takes a snapshot with Sunburst (1 - 5 stages)", function () {
|
||||||
cy.createDashboard("Sunburst Visualization").then(dashboard => {
|
cy.createDashboard("Sunburst Visualization").then((dashboard) => {
|
||||||
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
||||||
return cy
|
return cy
|
||||||
.all(
|
.all(
|
||||||
STAGES_WIDGETS.map(sunburst => () =>
|
STAGES_WIDGETS.map(
|
||||||
|
(sunburst) => () =>
|
||||||
cy
|
cy
|
||||||
.createQuery({ name: `Sunburst with ${sunburst.name}`, query: sunburst.query })
|
.createQuery({ name: `Sunburst with ${sunburst.name}`, query: sunburst.query })
|
||||||
.then(queryData => cy.createVisualization(queryData.id, "SUNBURST_SEQUENCE", "Sunburst", {}))
|
.then((queryData) => cy.createVisualization(queryData.id, "SUNBURST_SEQUENCE", "Sunburst", {}))
|
||||||
.then(visualization => cy.addWidget(dashboard.id, visualization.id, { position: sunburst.position }))
|
.then((visualization) => cy.addWidget(dashboard.id, visualization.id, { position: sunburst.position }))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.then(widgets => {
|
.then((widgets) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
widgets.forEach(widget => {
|
widgets.forEach((widget) => {
|
||||||
cy.getByTestId(getWidgetTestId(widget)).within(() => cy.get("svg").should("exist"));
|
cy.getByTestId(getWidgetTestId(widget)).within(() => cy.get("svg").should("exist"));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,21 +103,22 @@ describe("Sankey and Sunburst", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("takes a snapshot with Sankey (1 - 5 stages)", function() {
|
it("takes a snapshot with Sankey (1 - 5 stages)", function () {
|
||||||
cy.createDashboard("Sankey Visualization").then(dashboard => {
|
cy.createDashboard("Sankey Visualization").then((dashboard) => {
|
||||||
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
||||||
return cy
|
return cy
|
||||||
.all(
|
.all(
|
||||||
STAGES_WIDGETS.map(sankey => () =>
|
STAGES_WIDGETS.map(
|
||||||
|
(sankey) => () =>
|
||||||
cy
|
cy
|
||||||
.createQuery({ name: `Sankey with ${sankey.name}`, query: sankey.query })
|
.createQuery({ name: `Sankey with ${sankey.name}`, query: sankey.query })
|
||||||
.then(queryData => cy.createVisualization(queryData.id, "SANKEY", "Sankey", {}))
|
.then((queryData) => cy.createVisualization(queryData.id, "SANKEY", "Sankey", {}))
|
||||||
.then(visualization => cy.addWidget(dashboard.id, visualization.id, { position: sankey.position }))
|
.then((visualization) => cy.addWidget(dashboard.id, visualization.id, { position: sankey.position }))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.then(widgets => {
|
.then((widgets) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
widgets.forEach(widget => {
|
widgets.forEach((widget) => {
|
||||||
cy.getByTestId(getWidgetTestId(widget)).within(() => cy.get("svg").should("exist"));
|
cy.getByTestId(getWidgetTestId(widget)).within(() => cy.get("svg").should("exist"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ describe("Word Cloud", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
cy.document().then(injectFont);
|
cy.document().then(injectFont);
|
||||||
@@ -80,9 +81,7 @@ describe("Word Cloud", () => {
|
|||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("svg text").should("have.length", 11);
|
||||||
.find("svg text")
|
|
||||||
.should("have.length", 11);
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Word Cloud (Automatic word frequencies)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Word Cloud (Automatic word frequencies)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
@@ -99,9 +98,7 @@ describe("Word Cloud", () => {
|
|||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("svg text").should("have.length", 5);
|
||||||
.find("svg text")
|
|
||||||
.should("have.length", 5);
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Word Cloud (Frequencies from another column)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Word Cloud (Frequencies from another column)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
@@ -125,9 +122,7 @@ describe("Word Cloud", () => {
|
|||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("svg text").should("have.length", 2);
|
||||||
.find("svg text")
|
|
||||||
.should("have.length", 2);
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Word Cloud (With filters)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Word Cloud (With filters)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
postgres:
|
postgres:
|
||||||
image: pgautoupgrade/pgautoupgrade:latest
|
image: postgres:18-alpine
|
||||||
ports:
|
ports:
|
||||||
- "15432:5432"
|
- "15432:5432"
|
||||||
# The following turns the DB into less durable, but gains significant performance improvements for the tests run (x3
|
# The following turns the DB into less durable, but gains significant performance improvements for the tests run (x3
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "redash-client",
|
"name": "redash-client",
|
||||||
"version": "25.10.0-dev",
|
"version": "25.12.0-dev",
|
||||||
"description": "The frontend part of Redash.",
|
"description": "The frontend part of Redash.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.2.1",
|
"@ant-design/icons": "^4.2.1",
|
||||||
"@redash/viz": "file:viz-lib",
|
"@redash/viz": "file:viz-lib",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.43.3",
|
||||||
"antd": "4.4.3",
|
"antd": "4.4.3",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"axios-auth-refresh": "3.3.6",
|
"axios-auth-refresh": "3.3.6",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"prop-types": "^15.6.1",
|
"prop-types": "^15.6.1",
|
||||||
"query-string": "^6.9.0",
|
"query-string": "^6.9.0",
|
||||||
"react": "16.14.0",
|
"react": "16.14.0",
|
||||||
"react-ace": "^9.1.1",
|
"react-ace": "^14.0.1",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"react-grid-layout": "^0.18.2",
|
"react-grid-layout": "^0.18.2",
|
||||||
"react-resizable": "^1.10.1",
|
"react-resizable": "^1.10.1",
|
||||||
|
|||||||
100
poetry.lock
generated
100
poetry.lock
generated
@@ -1195,6 +1195,52 @@ idna = ["idna (>=3.6)"]
|
|||||||
trio = ["trio (>=0.23)"]
|
trio = ["trio (>=0.23)"]
|
||||||
wmi = ["wmi (>=1.5.1)"]
|
wmi = ["wmi (>=1.5.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "duckdb"
|
||||||
|
version = "1.3.2"
|
||||||
|
description = "DuckDB in-process database"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7.0"
|
||||||
|
groups = ["all_ds"]
|
||||||
|
files = [
|
||||||
|
{file = "duckdb-1.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:14676651b86f827ea10bf965eec698b18e3519fdc6266d4ca849f5af7a8c315e"},
|
||||||
|
{file = "duckdb-1.3.2-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:e584f25892450757919639b148c2410402b17105bd404017a57fa9eec9c98919"},
|
||||||
|
{file = "duckdb-1.3.2-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:84a19f185ee0c5bc66d95908c6be19103e184b743e594e005dee6f84118dc22c"},
|
||||||
|
{file = "duckdb-1.3.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:186fc3f98943e97f88a1e501d5720b11214695571f2c74745d6e300b18bef80e"},
|
||||||
|
{file = "duckdb-1.3.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b7e6bb613b73745f03bff4bb412f362d4a1e158bdcb3946f61fd18e9e1a8ddf"},
|
||||||
|
{file = "duckdb-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1c90646b52a0eccda1f76b10ac98b502deb9017569e84073da00a2ab97763578"},
|
||||||
|
{file = "duckdb-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:4cdffb1e60defbfa75407b7f2ccc322f535fd462976940731dfd1644146f90c6"},
|
||||||
|
{file = "duckdb-1.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e1872cf63aae28c3f1dc2e19b5e23940339fc39fb3425a06196c5d00a8d01040"},
|
||||||
|
{file = "duckdb-1.3.2-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:db256c206056468ae6a9e931776bdf7debaffc58e19a0ff4fa9e7e1e82d38b3b"},
|
||||||
|
{file = "duckdb-1.3.2-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1d57df2149d6e4e0bd5198689316c5e2ceec7f6ac0a9ec11bc2b216502a57b34"},
|
||||||
|
{file = "duckdb-1.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f76c8b1e2a19dfe194027894209ce9ddb073fd9db69af729a524d2860e4680"},
|
||||||
|
{file = "duckdb-1.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45bea70b3e93c6bf766ce2f80fc3876efa94c4ee4de72036417a7bd1e32142fe"},
|
||||||
|
{file = "duckdb-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:003f7d36f0d8a430cb0e00521f18b7d5ee49ec98aaa541914c6d0e008c306f1a"},
|
||||||
|
{file = "duckdb-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:0eb210cedf08b067fa90c666339688f1c874844a54708562282bc54b0189aac6"},
|
||||||
|
{file = "duckdb-1.3.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2455b1ffef4e3d3c7ef8b806977c0e3973c10ec85aa28f08c993ab7f2598e8dd"},
|
||||||
|
{file = "duckdb-1.3.2-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:9d0ae509713da3461c000af27496d5413f839d26111d2a609242d9d17b37d464"},
|
||||||
|
{file = "duckdb-1.3.2-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:72ca6143d23c0bf6426396400f01fcbe4785ad9ceec771bd9a4acc5b5ef9a075"},
|
||||||
|
{file = "duckdb-1.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b49a11afba36b98436db83770df10faa03ebded06514cb9b180b513d8be7f392"},
|
||||||
|
{file = "duckdb-1.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36abdfe0d1704fe09b08d233165f312dad7d7d0ecaaca5fb3bb869f4838a2d0b"},
|
||||||
|
{file = "duckdb-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3380aae1c4f2af3f37b0bf223fabd62077dd0493c84ef441e69b45167188e7b6"},
|
||||||
|
{file = "duckdb-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:11af73963ae174aafd90ea45fb0317f1b2e28a7f1d9902819d47c67cc957d49c"},
|
||||||
|
{file = "duckdb-1.3.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a3418c973b06ac4e97f178f803e032c30c9a9f56a3e3b43a866f33223dfbf60b"},
|
||||||
|
{file = "duckdb-1.3.2-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:2a741eae2cf110fd2223eeebe4151e22c0c02803e1cfac6880dbe8a39fecab6a"},
|
||||||
|
{file = "duckdb-1.3.2-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:51e62541341ea1a9e31f0f1ade2496a39b742caf513bebd52396f42ddd6525a0"},
|
||||||
|
{file = "duckdb-1.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e519de5640e5671f1731b3ae6b496e0ed7e4de4a1c25c7a2f34c991ab64d71"},
|
||||||
|
{file = "duckdb-1.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4732fb8cc60566b60e7e53b8c19972cb5ed12d285147a3063b16cc64a79f6d9f"},
|
||||||
|
{file = "duckdb-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97f7a22dcaa1cca889d12c3dc43a999468375cdb6f6fe56edf840e062d4a8293"},
|
||||||
|
{file = "duckdb-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:cd3d717bf9c49ef4b1016c2216517572258fa645c2923e91c5234053defa3fb5"},
|
||||||
|
{file = "duckdb-1.3.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:18862e3b8a805f2204543d42d5f103b629cb7f7f2e69f5188eceb0b8a023f0af"},
|
||||||
|
{file = "duckdb-1.3.2-cp39-cp39-macosx_12_0_universal2.whl", hash = "sha256:75ed129761b6159f0b8eca4854e496a3c4c416e888537ec47ff8eb35fda2b667"},
|
||||||
|
{file = "duckdb-1.3.2-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:875193ae9f718bc80ab5635435de5b313e3de3ec99420a9b25275ddc5c45ff58"},
|
||||||
|
{file = "duckdb-1.3.2-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09b5fd8a112301096668903781ad5944c3aec2af27622bd80eae54149de42b42"},
|
||||||
|
{file = "duckdb-1.3.2-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10cb87ad964b989175e7757d7ada0b1a7264b401a79be2f828cf8f7c366f7f95"},
|
||||||
|
{file = "duckdb-1.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4389fc3812e26977034fe3ff08d1f7dbfe6d2d8337487b4686f2b50e254d7ee3"},
|
||||||
|
{file = "duckdb-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:07952ec6f45dd3c7db0f825d231232dc889f1f2490b97a4e9b7abb6830145a19"},
|
||||||
|
{file = "duckdb-1.3.2.tar.gz", hash = "sha256:c658df8a1bc78704f702ad0d954d82a1edd4518d7a04f00027ec53e40f591ff5"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "e6data-python-connector"
|
name = "e6data-python-connector"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
@@ -2032,6 +2078,58 @@ http2 = ["h2 (>=3,<5)"]
|
|||||||
socks = ["socksio (==1.*)"]
|
socks = ["socksio (==1.*)"]
|
||||||
zstd = ["zstandard (>=0.18.0)"]
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ibm-db"
|
||||||
|
version = "3.2.7"
|
||||||
|
description = "Python DBI driver for DB2 (LUW, zOS, i5)"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "platform_machine == \"x86_64\" or platform_machine == \"AMD64\""
|
||||||
|
files = [
|
||||||
|
{file = "ibm_db-3.2.7-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:7c2b451ffe67be602e93d94b2d2042dd051ec0757cfd6e4d7344cb594f2d3508"},
|
||||||
|
{file = "ibm_db-3.2.7-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:9a1b139a9c21ff7216aac83ba29dceb6c8a9df3d6aee44ff1fe845cb60d3caed"},
|
||||||
|
{file = "ibm_db-3.2.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5e60297b4680cc566caa67f513aa68883ef48b0c612028a38883620807b09c"},
|
||||||
|
{file = "ibm_db-3.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf1c30e67e9e573e33524c393a1426e0dffa2da34ba42a0ec510e0f75766976f"},
|
||||||
|
{file = "ibm_db-3.2.7-cp310-cp310-win32.whl", hash = "sha256:171014c2caa0419055943ff3badae5118cc3a191360f03b80c8366ef374d5c28"},
|
||||||
|
{file = "ibm_db-3.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:3425c158a65dd43e4b09dc968c18042a656ed6ef2e1db0164f032e97681823b7"},
|
||||||
|
{file = "ibm_db-3.2.7-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:ba493d07e1845b8b1169ad27ace92f0ff540cc9a623f2753b8c68dc66c59d7df"},
|
||||||
|
{file = "ibm_db-3.2.7-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:abed0a7c644b9ddf2c49bf5c0938f936f0b2dffd1703c9819440021be141716e"},
|
||||||
|
{file = "ibm_db-3.2.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cabd3d3e8c879ef60d91e1fe1356cf8603f8b4b69cc7dda39d4a8698a055044"},
|
||||||
|
{file = "ibm_db-3.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aab5dceec45d69b0bbd333be66597dbaedf663c6c56a0fbd6196ecd1836e4095"},
|
||||||
|
{file = "ibm_db-3.2.7-cp311-cp311-win32.whl", hash = "sha256:16272ad07912051d9ab5cbe3a9e2d3d888365d071334f9620d8e0b2ed69ee4f9"},
|
||||||
|
{file = "ibm_db-3.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:4b479e92b6954ab7f65c9d247a65fb0cde6a48899f71a8881b58023c0ace1f49"},
|
||||||
|
{file = "ibm_db-3.2.7-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:24e8a538475997f20569f221247808507b63349df51119fe9b2f8e48a0bf6f9b"},
|
||||||
|
{file = "ibm_db-3.2.7-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:24a53fb8e3c200bf2a55095f1ae4c065f2136f8be87ca1db89a874bd82d88ea5"},
|
||||||
|
{file = "ibm_db-3.2.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91f68be7bd0d2940023da43d0a94f196fe267ca825df7874b8174583c8678ea0"},
|
||||||
|
{file = "ibm_db-3.2.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d39fe5001c078f2b824d1805ca9737060203a00a9dd9a8fe4b6f6b32b271cb5"},
|
||||||
|
{file = "ibm_db-3.2.7-cp312-cp312-win32.whl", hash = "sha256:20388753f52050e07e845b74146dbbe3f892dcfdfb015638e8f57c2fb2e056b8"},
|
||||||
|
{file = "ibm_db-3.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:6e507ddf93b8406b0b88ff6bf07658a3100ce98cb1e735e5ec8e0a56e30ea856"},
|
||||||
|
{file = "ibm_db-3.2.7-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:5fead45b6e1a448d90d7bc4fd8a28783988915a7598418f53191a17f4ddac173"},
|
||||||
|
{file = "ibm_db-3.2.7-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c6462dfd79a23824ce726696531a41a6861555ef27e9f050436bf42ad000734d"},
|
||||||
|
{file = "ibm_db-3.2.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c133f3ed5fae6065a8ccd386fd08d8d07d783343635e5d7c0b7a704419a398dc"},
|
||||||
|
{file = "ibm_db-3.2.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb2d36cfcb1c8b7c25cbdd278ac7be381f7f0c0c8bc330349db166ffd0cf3c5"},
|
||||||
|
{file = "ibm_db-3.2.7-cp313-cp313-win32.whl", hash = "sha256:3dc814d7824b4917f73e35c3c050ed1286ccccc1c3433a7c37984a3069664ac2"},
|
||||||
|
{file = "ibm_db-3.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:ff07632b4514f3af8a64e5c8c38b8aef0833642182a737119e5866a320dd0392"},
|
||||||
|
{file = "ibm_db-3.2.7-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0dd69c71df87776a6fbb0612e559dd4bfdb447f5222d2e2aa81bf2ba4f445491"},
|
||||||
|
{file = "ibm_db-3.2.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de1052e11eee5c62fd66c47ee1e6d19c3c7c3690a06c25e8e5c1fcca508f2f5"},
|
||||||
|
{file = "ibm_db-3.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb41cdd4e7456a44ccb5f943f0b37876d36ee96e1e01d9db8b4d13158e2358af"},
|
||||||
|
{file = "ibm_db-3.2.7-cp37-cp37m-win32.whl", hash = "sha256:37139d0d9c690ca1c951fc2367e2a23bcf2fa4bc57f8f8a744d1abd48caacc4d"},
|
||||||
|
{file = "ibm_db-3.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c3f7c4a53fa0fc1dbd85347459af1c50d76d938023a83aea4339599aed1bbc"},
|
||||||
|
{file = "ibm_db-3.2.7-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:156dfb41b4604c5cdaecc4b308a21b2d03017dcd41a0574bb471f1f842c44577"},
|
||||||
|
{file = "ibm_db-3.2.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d0f51e6664e8440dee88b9f1ce3f6a99012bf7631b44bd0c7caac5bbdf2dc0e"},
|
||||||
|
{file = "ibm_db-3.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6e8e8bb410ed51574faea97e3ede181c210648cc25301b95839dd938572ff64"},
|
||||||
|
{file = "ibm_db-3.2.7-cp38-cp38-win32.whl", hash = "sha256:6be996ee77d60dec0ee5790e83694d34ef749e03b8f8c53d5c7613ca149e6d1f"},
|
||||||
|
{file = "ibm_db-3.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:0fb423623c409e3ca9dbed87773f3916928619361c38fcc635b2a0111cdbe916"},
|
||||||
|
{file = "ibm_db-3.2.7-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:92455cbc68702fa29e857dca8997b900efc4bc29a96fc73a0aa6431c2cfa8fcb"},
|
||||||
|
{file = "ibm_db-3.2.7-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:b4b1fb230d7a9653b0c362010b731e0e606fe50410111fdbd1fb68ea6b62fab0"},
|
||||||
|
{file = "ibm_db-3.2.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf68fc63ab201a651a6c3fc47259c72dd502da841832fe96da2f48e292a698b8"},
|
||||||
|
{file = "ibm_db-3.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb22acdbb9f1d9bf82875a848e771bf2c007fa6e91e0fa9ed43ec7490ac72ba"},
|
||||||
|
{file = "ibm_db-3.2.7-cp39-cp39-win32.whl", hash = "sha256:3d0d0fe235e0c16b1b66d11f636c347ec2665cd4d84930bf6153b294b4000bc3"},
|
||||||
|
{file = "ibm_db-3.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:2f8937f42a9b90847bafdd373867d19293e6f3b5d44f6ff0ff8016a387b64920"},
|
||||||
|
{file = "ibm_db-3.2.7.tar.gz", hash = "sha256:b3c3b4550364a43bf1daa4519b668e6e00e7c3935291f8c444c4ec989417e861"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "identify"
|
name = "identify"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -5966,4 +6064,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.8,<3.11"
|
python-versions = ">=3.8,<3.11"
|
||||||
content-hash = "ee33f4494c213575cfae29a43d3a9e76deabae423c84779286a768f1a2b15278"
|
content-hash = "84271dccdac5067fb1940204ec74f10c4063119efd16107327090dcbb5b1b8c5"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "redash"
|
name = "redash"
|
||||||
version = "25.10.0-dev"
|
version = "25.12.0-dev"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
description = "Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data."
|
description = "Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data."
|
||||||
authors = [
|
authors = [
|
||||||
@@ -90,6 +90,7 @@ pyodbc = "5.1.0"
|
|||||||
debugpy = "^1.8.9"
|
debugpy = "^1.8.9"
|
||||||
paramiko = "3.4.1"
|
paramiko = "3.4.1"
|
||||||
oracledb = "2.5.1"
|
oracledb = "2.5.1"
|
||||||
|
ibm-db = { version = "^3.2.7", markers = "platform_machine == 'x86_64' or platform_machine == 'AMD64'" }
|
||||||
|
|
||||||
[tool.poetry.group.all_ds]
|
[tool.poetry.group.all_ds]
|
||||||
optional = true
|
optional = true
|
||||||
@@ -104,6 +105,7 @@ certifi = ">=2019.9.11"
|
|||||||
cmem-cmempy = "21.2.3"
|
cmem-cmempy = "21.2.3"
|
||||||
databend-py = "0.4.6"
|
databend-py = "0.4.6"
|
||||||
databend-sqlalchemy = "0.2.4"
|
databend-sqlalchemy = "0.2.4"
|
||||||
|
duckdb = "1.3.2"
|
||||||
google-api-python-client = "1.7.11"
|
google-api-python-client = "1.7.11"
|
||||||
gspread = "5.11.2"
|
gspread = "5.11.2"
|
||||||
impyla = "0.16.0"
|
impyla = "0.16.0"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from redash.app import create_app # noqa
|
|||||||
from redash.destinations import import_destinations
|
from redash.destinations import import_destinations
|
||||||
from redash.query_runner import import_query_runners
|
from redash.query_runner import import_query_runners
|
||||||
|
|
||||||
__version__ = "25.10.0-dev"
|
__version__ = "25.12.0-dev"
|
||||||
|
|
||||||
|
|
||||||
if os.environ.get("REMOTE_DEBUG"):
|
if os.environ.get("REMOTE_DEBUG"):
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class Webhook(BaseDestination):
|
|||||||
auth = HTTPBasicAuth(options.get("username"), options.get("password")) if options.get("username") else None
|
auth = HTTPBasicAuth(options.get("username"), options.get("password")) if options.get("username") else None
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
options.get("url"),
|
options.get("url"),
|
||||||
data=json_dumps(data),
|
data=json_dumps(data).encode("utf-8"),
|
||||||
auth=auth,
|
auth=auth,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=5.0,
|
timeout=5.0,
|
||||||
|
|||||||
@@ -241,6 +241,8 @@ class QueryListResource(BaseQueryListResource):
|
|||||||
query = models.Query.create(**query_def)
|
query = models.Query.create(**query_def)
|
||||||
models.db.session.add(query)
|
models.db.session.add(query)
|
||||||
models.db.session.commit()
|
models.db.session.commit()
|
||||||
|
query.update_latest_result_by_query_hash()
|
||||||
|
models.db.session.commit()
|
||||||
|
|
||||||
self.record_event({"action": "create", "object_id": query.id, "object_type": "query"})
|
self.record_event({"action": "create", "object_id": query.id, "object_type": "query"})
|
||||||
|
|
||||||
@@ -364,6 +366,8 @@ class QueryResource(BaseResource):
|
|||||||
try:
|
try:
|
||||||
self.update_model(query, query_def)
|
self.update_model(query, query_def)
|
||||||
models.db.session.commit()
|
models.db.session.commit()
|
||||||
|
query.update_latest_result_by_query_hash()
|
||||||
|
models.db.session.commit()
|
||||||
except StaleDataError:
|
except StaleDataError:
|
||||||
abort(409)
|
abort(409)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import calendar
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import numbers
|
import numbers
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
@@ -644,6 +645,43 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
|||||||
|
|
||||||
return list(outdated_queries.values())
|
return list(outdated_queries.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _do_multi_byte_search(cls, all_queries, term, limit=None):
|
||||||
|
# term examples:
|
||||||
|
# - word
|
||||||
|
# - name:word
|
||||||
|
# - query:word
|
||||||
|
# - "multiple words"
|
||||||
|
# - name:"multiple words"
|
||||||
|
# - word1 word2 word3
|
||||||
|
# - word1 "multiple word" query:"select foo"
|
||||||
|
tokens = re.findall(r'(?:([^:\s]+):)?(?:"([^"]+)"|(\S+))', term)
|
||||||
|
conditions = []
|
||||||
|
for token in tokens:
|
||||||
|
key = None
|
||||||
|
if token[0]:
|
||||||
|
key = token[0]
|
||||||
|
|
||||||
|
if token[1]:
|
||||||
|
value = token[1]
|
||||||
|
else:
|
||||||
|
value = token[2]
|
||||||
|
|
||||||
|
pattern = f"%{value}%"
|
||||||
|
|
||||||
|
if key == "id" and value.isdigit():
|
||||||
|
conditions.append(cls.id.equal(int(value)))
|
||||||
|
elif key == "name":
|
||||||
|
conditions.append(cls.name.ilike(pattern))
|
||||||
|
elif key == "query":
|
||||||
|
conditions.append(cls.query_text.ilike(pattern))
|
||||||
|
elif key == "description":
|
||||||
|
conditions.append(cls.description.ilike(pattern))
|
||||||
|
else:
|
||||||
|
conditions.append(or_(cls.name.ilike(pattern), cls.description.ilike(pattern)))
|
||||||
|
|
||||||
|
return all_queries.filter(and_(*conditions)).order_by(Query.id).limit(limit)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search(
|
def search(
|
||||||
cls,
|
cls,
|
||||||
@@ -664,12 +702,7 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
|||||||
|
|
||||||
if multi_byte_search:
|
if multi_byte_search:
|
||||||
# Since tsvector doesn't work well with CJK languages, use `ilike` too
|
# Since tsvector doesn't work well with CJK languages, use `ilike` too
|
||||||
pattern = "%{}%".format(term)
|
return cls._do_multi_byte_search(all_queries, term, limit)
|
||||||
return (
|
|
||||||
all_queries.filter(or_(cls.name.ilike(pattern), cls.description.ilike(pattern)))
|
|
||||||
.order_by(Query.id)
|
|
||||||
.limit(limit)
|
|
||||||
)
|
|
||||||
|
|
||||||
# sort the result using the weight as defined in the search vector column
|
# sort the result using the weight as defined in the search vector column
|
||||||
return all_queries.search(term, sort=True).limit(limit)
|
return all_queries.search(term, sort=True).limit(limit)
|
||||||
@@ -678,13 +711,7 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
|||||||
def search_by_user(cls, term, user, limit=None, multi_byte_search=False):
|
def search_by_user(cls, term, user, limit=None, multi_byte_search=False):
|
||||||
if multi_byte_search:
|
if multi_byte_search:
|
||||||
# Since tsvector doesn't work well with CJK languages, use `ilike` too
|
# Since tsvector doesn't work well with CJK languages, use `ilike` too
|
||||||
pattern = "%{}%".format(term)
|
return cls._do_multi_byte_search(cls.by_user(user), term, limit)
|
||||||
return (
|
|
||||||
cls.by_user(user)
|
|
||||||
.filter(or_(cls.name.ilike(pattern), cls.description.ilike(pattern)))
|
|
||||||
.order_by(Query.id)
|
|
||||||
.limit(limit)
|
|
||||||
)
|
|
||||||
|
|
||||||
return cls.by_user(user).search(term, sort=True).limit(limit)
|
return cls.by_user(user).search(term, sort=True).limit(limit)
|
||||||
|
|
||||||
@@ -726,6 +753,23 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
|||||||
|
|
||||||
return db.session.execute(query, {"ids": tuple(query_ids)}).fetchall()
|
return db.session.execute(query, {"ids": tuple(query_ids)}).fetchall()
|
||||||
|
|
||||||
|
def update_latest_result_by_query_hash(self):
|
||||||
|
query_hash = self.query_hash
|
||||||
|
data_source_id = self.data_source_id
|
||||||
|
query_result = (
|
||||||
|
QueryResult.query.options(load_only("id"))
|
||||||
|
.filter(
|
||||||
|
QueryResult.query_hash == query_hash,
|
||||||
|
QueryResult.data_source_id == data_source_id,
|
||||||
|
)
|
||||||
|
.order_by(QueryResult.retrieved_at.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if query_result:
|
||||||
|
latest_query_data_id = query_result.id
|
||||||
|
self.latest_query_data_id = latest_query_data_id
|
||||||
|
db.session.add(self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_latest_result(cls, query_result):
|
def update_latest_result(cls, query_result):
|
||||||
# TODO: Investigate how big an impact this select-before-update makes.
|
# TODO: Investigate how big an impact this select-before-update makes.
|
||||||
|
|||||||
174
redash/query_runner/duckdb.py
Normal file
174
redash/query_runner/duckdb.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from redash.query_runner import (
|
||||||
|
TYPE_BOOLEAN,
|
||||||
|
TYPE_DATE,
|
||||||
|
TYPE_DATETIME,
|
||||||
|
TYPE_FLOAT,
|
||||||
|
TYPE_INTEGER,
|
||||||
|
TYPE_STRING,
|
||||||
|
BaseSQLQueryRunner,
|
||||||
|
InterruptException,
|
||||||
|
register,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import duckdb
|
||||||
|
|
||||||
|
enabled = True
|
||||||
|
except ImportError:
|
||||||
|
enabled = False
|
||||||
|
|
||||||
|
# Map DuckDB types to Redash column types
|
||||||
|
TYPES_MAP = {
|
||||||
|
"BOOLEAN": TYPE_BOOLEAN,
|
||||||
|
"TINYINT": TYPE_INTEGER,
|
||||||
|
"SMALLINT": TYPE_INTEGER,
|
||||||
|
"INTEGER": TYPE_INTEGER,
|
||||||
|
"BIGINT": TYPE_INTEGER,
|
||||||
|
"HUGEINT": TYPE_INTEGER,
|
||||||
|
"REAL": TYPE_FLOAT,
|
||||||
|
"DOUBLE": TYPE_FLOAT,
|
||||||
|
"DECIMAL": TYPE_FLOAT,
|
||||||
|
"VARCHAR": TYPE_STRING,
|
||||||
|
"BLOB": TYPE_STRING,
|
||||||
|
"DATE": TYPE_DATE,
|
||||||
|
"TIMESTAMP": TYPE_DATETIME,
|
||||||
|
"TIMESTAMP WITH TIME ZONE": TYPE_DATETIME,
|
||||||
|
"TIME": TYPE_DATETIME,
|
||||||
|
"INTERVAL": TYPE_STRING,
|
||||||
|
"UUID": TYPE_STRING,
|
||||||
|
"JSON": TYPE_STRING,
|
||||||
|
"STRUCT": TYPE_STRING,
|
||||||
|
"MAP": TYPE_STRING,
|
||||||
|
"UNION": TYPE_STRING,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DuckDB(BaseSQLQueryRunner):
|
||||||
|
noop_query = "SELECT 1"
|
||||||
|
|
||||||
|
def __init__(self, configuration):
|
||||||
|
super().__init__(configuration)
|
||||||
|
self.dbpath = configuration.get("dbpath", ":memory:")
|
||||||
|
exts = configuration.get("extensions", "")
|
||||||
|
self.extensions = [e.strip() for e in exts.split(",") if e.strip()]
|
||||||
|
self._connect()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def configuration_schema(cls):
|
||||||
|
return {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"dbpath": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Database Path",
|
||||||
|
"default": ":memory:",
|
||||||
|
},
|
||||||
|
"extensions": {"type": "string", "title": "Extensions (comma separated)"},
|
||||||
|
},
|
||||||
|
"order": ["dbpath", "extensions"],
|
||||||
|
"required": ["dbpath"],
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enabled(cls) -> bool:
|
||||||
|
return enabled
|
||||||
|
|
||||||
|
def _connect(self) -> None:
|
||||||
|
self.con = duckdb.connect(self.dbpath)
|
||||||
|
for ext in self.extensions:
|
||||||
|
try:
|
||||||
|
if "." in ext:
|
||||||
|
prefix, name = ext.split(".", 1)
|
||||||
|
if prefix == "community":
|
||||||
|
self.con.execute(f"INSTALL {name} FROM community")
|
||||||
|
self.con.execute(f"LOAD {name}")
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown extension prefix.")
|
||||||
|
else:
|
||||||
|
self.con.execute(f"INSTALL {ext}")
|
||||||
|
self.con.execute(f"LOAD {ext}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to load extension %s: %s", ext, e)
|
||||||
|
|
||||||
|
def run_query(self, query, user) -> tuple:
|
||||||
|
try:
|
||||||
|
cursor = self.con.cursor()
|
||||||
|
cursor.execute(query)
|
||||||
|
columns = self.fetch_columns(
|
||||||
|
[(d[0], TYPES_MAP.get(d[1].upper(), TYPE_STRING)) for d in cursor.description]
|
||||||
|
)
|
||||||
|
rows = [dict(zip((col["name"] for col in columns), row)) for row in cursor.fetchall()]
|
||||||
|
data = {"columns": columns, "rows": rows}
|
||||||
|
return data, None
|
||||||
|
except duckdb.InterruptException:
|
||||||
|
raise InterruptException("Query cancelled by user.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error running query: %s", e)
|
||||||
|
return None, str(e)
|
||||||
|
|
||||||
|
def get_schema(self, get_stats=False) -> list:
|
||||||
|
tables_query = """
|
||||||
|
SELECT table_schema, table_name FROM information_schema.tables
|
||||||
|
WHERE table_schema NOT IN ('information_schema', 'pg_catalog');
|
||||||
|
"""
|
||||||
|
tables_results, error = self.run_query(tables_query, None)
|
||||||
|
if error:
|
||||||
|
raise Exception(f"Failed to get tables: {error}")
|
||||||
|
|
||||||
|
schema = {}
|
||||||
|
for table_row in tables_results["rows"]:
|
||||||
|
full_table_name = f"{table_row['table_schema']}.{table_row['table_name']}"
|
||||||
|
schema[full_table_name] = {"name": full_table_name, "columns": []}
|
||||||
|
|
||||||
|
describe_query = f'DESCRIBE "{table_row["table_schema"]}"."{table_row["table_name"]}";'
|
||||||
|
columns_results, error = self.run_query(describe_query, None)
|
||||||
|
if error:
|
||||||
|
logger.warning("Failed to describe table %s: %s", full_table_name, error)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for col_row in columns_results["rows"]:
|
||||||
|
col = {"name": col_row["column_name"], "type": col_row["column_type"]}
|
||||||
|
schema[full_table_name]["columns"].append(col)
|
||||||
|
|
||||||
|
if col_row["column_type"].startswith("STRUCT("):
|
||||||
|
schema[full_table_name]["columns"].extend(
|
||||||
|
self._expand_struct_fields(col["name"], col_row["column_type"])
|
||||||
|
)
|
||||||
|
|
||||||
|
return list(schema.values())
|
||||||
|
|
||||||
|
def _expand_struct_fields(self, base_name: str, struct_type: str) -> list:
|
||||||
|
"""Recursively expand STRUCT(...) definitions into pseudo-columns."""
|
||||||
|
fields = []
|
||||||
|
# strip STRUCT( ... )
|
||||||
|
inner = struct_type[len("STRUCT(") : -1].strip()
|
||||||
|
# careful: nested structs, so parse comma-separated parts properly
|
||||||
|
depth, current, parts = 0, [], []
|
||||||
|
for c in inner:
|
||||||
|
if c == "(":
|
||||||
|
depth += 1
|
||||||
|
elif c == ")":
|
||||||
|
depth -= 1
|
||||||
|
if c == "," and depth == 0:
|
||||||
|
parts.append("".join(current).strip())
|
||||||
|
current = []
|
||||||
|
else:
|
||||||
|
current.append(c)
|
||||||
|
if current:
|
||||||
|
parts.append("".join(current).strip())
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
# each part looks like: "fieldname TYPE"
|
||||||
|
fname, ftype = part.split(" ", 1)
|
||||||
|
colname = f"{base_name}.{fname}"
|
||||||
|
fields.append({"name": colname, "type": ftype})
|
||||||
|
if ftype.startswith("STRUCT("):
|
||||||
|
fields.extend(self._expand_struct_fields(colname, ftype))
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
register(DuckDB)
|
||||||
@@ -138,6 +138,15 @@ def _get_ssl_config(configuration):
|
|||||||
return ssl_config
|
return ssl_config
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_dsn(configuration):
|
||||||
|
standard_params = {"user", "password", "host", "port", "dbname"}
|
||||||
|
params = psycopg2.extensions.parse_dsn(configuration.get("dsn", ""))
|
||||||
|
overlap = standard_params.intersection(params.keys())
|
||||||
|
if overlap:
|
||||||
|
raise ValueError("Extra parameters may not contain {}".format(overlap))
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
class PostgreSQL(BaseSQLQueryRunner):
|
class PostgreSQL(BaseSQLQueryRunner):
|
||||||
noop_query = "SELECT 1"
|
noop_query = "SELECT 1"
|
||||||
|
|
||||||
@@ -151,6 +160,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
|||||||
"host": {"type": "string", "default": "127.0.0.1"},
|
"host": {"type": "string", "default": "127.0.0.1"},
|
||||||
"port": {"type": "number", "default": 5432},
|
"port": {"type": "number", "default": 5432},
|
||||||
"dbname": {"type": "string", "title": "Database Name"},
|
"dbname": {"type": "string", "title": "Database Name"},
|
||||||
|
"dsn": {"type": "string", "default": "application_name=redash", "title": "Parameters"},
|
||||||
"sslmode": {
|
"sslmode": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "SSL Mode",
|
"title": "SSL Mode",
|
||||||
@@ -223,7 +233,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
|||||||
AND a.attnum > 0
|
AND a.attnum > 0
|
||||||
AND NOT a.attisdropped
|
AND NOT a.attisdropped
|
||||||
WHERE c.relkind = 'm'
|
WHERE c.relkind = 'm'
|
||||||
AND has_table_privilege(s.nspname || '.' || c.relname, 'select')
|
AND has_table_privilege(quote_ident(s.nspname) || '.' || quote_ident(c.relname), 'select')
|
||||||
AND has_schema_privilege(s.nspname, 'usage')
|
AND has_schema_privilege(s.nspname, 'usage')
|
||||||
|
|
||||||
UNION
|
UNION
|
||||||
@@ -234,7 +244,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
|||||||
data_type
|
data_type
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||||
AND has_table_privilege(table_schema || '.' || table_name, 'select')
|
AND has_table_privilege(quote_ident(table_schema) || '.' || quote_ident(table_name), 'select')
|
||||||
AND has_schema_privilege(table_schema, 'usage')
|
AND has_schema_privilege(table_schema, 'usage')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -244,6 +254,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
|||||||
|
|
||||||
def _get_connection(self):
|
def _get_connection(self):
|
||||||
self.ssl_config = _get_ssl_config(self.configuration)
|
self.ssl_config = _get_ssl_config(self.configuration)
|
||||||
|
self.dsn = _parse_dsn(self.configuration)
|
||||||
connection = psycopg2.connect(
|
connection = psycopg2.connect(
|
||||||
user=self.configuration.get("user"),
|
user=self.configuration.get("user"),
|
||||||
password=self.configuration.get("password"),
|
password=self.configuration.get("password"),
|
||||||
@@ -252,6 +263,7 @@ class PostgreSQL(BaseSQLQueryRunner):
|
|||||||
dbname=self.configuration.get("dbname"),
|
dbname=self.configuration.get("dbname"),
|
||||||
async_=True,
|
async_=True,
|
||||||
**self.ssl_config,
|
**self.ssl_config,
|
||||||
|
**self.dsn,
|
||||||
)
|
)
|
||||||
|
|
||||||
return connection
|
return connection
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ default_query_runners = [
|
|||||||
"redash.query_runner.oracle",
|
"redash.query_runner.oracle",
|
||||||
"redash.query_runner.e6data",
|
"redash.query_runner.e6data",
|
||||||
"redash.query_runner.risingwave",
|
"redash.query_runner.risingwave",
|
||||||
|
"redash.query_runner.duckdb",
|
||||||
]
|
]
|
||||||
|
|
||||||
enabled_query_runners = array_from_string(
|
enabled_query_runners = array_from_string(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from redash.models.parameterized_query import (
|
|||||||
QueryDetachedFromDataSourceError,
|
QueryDetachedFromDataSourceError,
|
||||||
)
|
)
|
||||||
from redash.monitor import rq_job_ids
|
from redash.monitor import rq_job_ids
|
||||||
|
from redash.query_runner import NotSupported
|
||||||
from redash.tasks.failure_report import track_failure
|
from redash.tasks.failure_report import track_failure
|
||||||
from redash.utils import json_dumps, sentry
|
from redash.utils import json_dumps, sentry
|
||||||
from redash.worker import get_job_logger, job
|
from redash.worker import get_job_logger, job
|
||||||
@@ -177,6 +178,8 @@ def refresh_schema(data_source_id):
|
|||||||
time.time() - start_time,
|
time.time() - start_time,
|
||||||
)
|
)
|
||||||
statsd_client.incr("refresh_schema.timeout")
|
statsd_client.incr("refresh_schema.timeout")
|
||||||
|
except NotSupported:
|
||||||
|
logger.debug("Datasource %s does not support schema refresh", ds.name)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed refreshing schema for the data source: %s", ds.name, exc_info=1)
|
logger.warning("Failed refreshing schema for the data source: %s", ds.name, exc_info=1)
|
||||||
statsd_client.incr("refresh_schema.error")
|
statsd_client.incr("refresh_schema.error")
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ def collect_parameters_from_request(args):
|
|||||||
|
|
||||||
def base_url(org):
|
def base_url(org):
|
||||||
if settings.MULTI_ORG:
|
if settings.MULTI_ORG:
|
||||||
return "https://{}/{}".format(settings.HOST, org.slug)
|
return "{}/{}".format(settings.HOST, org.slug)
|
||||||
|
|
||||||
return settings.HOST
|
return settings.HOST
|
||||||
|
|
||||||
|
|||||||
50
tests/destinations/test_webhook.py
Normal file
50
tests/destinations/test_webhook.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import json
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from redash.destinations.webhook import Webhook
|
||||||
|
from redash.models import Alert
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_notify_handles_unicode():
|
||||||
|
# Create a mock alert with all the properties needed by serialize_alert
|
||||||
|
alert = mock.Mock()
|
||||||
|
alert.id = 1
|
||||||
|
alert.name = "Test Alert"
|
||||||
|
alert.custom_subject = "Test Subject With Unicode: 晨"
|
||||||
|
alert.custom_body = "Test Body"
|
||||||
|
alert.options = {}
|
||||||
|
alert.state = "ok"
|
||||||
|
alert.last_triggered_at = None
|
||||||
|
alert.updated_at = "2025-12-02T08:00:00Z"
|
||||||
|
alert.created_at = "2025-12-02T08:00:00Z"
|
||||||
|
alert.rearm = None
|
||||||
|
alert.query_id = 10
|
||||||
|
alert.user_id = 20
|
||||||
|
|
||||||
|
query = mock.Mock()
|
||||||
|
user = mock.Mock()
|
||||||
|
app = mock.Mock()
|
||||||
|
host = "http://redash.local"
|
||||||
|
options = {"url": "https://example.com/webhook", "username": "user", "password": "password"}
|
||||||
|
metadata = {}
|
||||||
|
new_state = Alert.TRIGGERED_STATE
|
||||||
|
destination = Webhook(options)
|
||||||
|
|
||||||
|
with mock.patch("redash.destinations.webhook.requests.post") as mock_post:
|
||||||
|
mock_response = mock.Mock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
destination.notify(alert, query, user, new_state, app, host, metadata, options)
|
||||||
|
|
||||||
|
# Get the data passed to the mock
|
||||||
|
call_args, call_kwargs = mock_post.call_args
|
||||||
|
sent_data = call_kwargs["data"]
|
||||||
|
|
||||||
|
# 1. Make sure we send bytes
|
||||||
|
assert isinstance(sent_data, bytes)
|
||||||
|
|
||||||
|
# 2. Make sure the bytes are the correct UTF-8 encoded JSON
|
||||||
|
decoded_data = json.loads(sent_data.decode("utf-8"))
|
||||||
|
assert decoded_data["alert"]["title"] == alert.custom_subject
|
||||||
|
assert "Test Subject With Unicode: 晨" in sent_data.decode("utf-8")
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import textwrap
|
import textwrap
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from redash import settings
|
||||||
from redash.models import OPERATORS, Alert, db, next_state
|
from redash.models import OPERATORS, Alert, db, next_state
|
||||||
from tests import BaseTestCase
|
from tests import BaseTestCase
|
||||||
|
|
||||||
@@ -176,16 +177,18 @@ class TestAlertRenderTemplate(BaseTestCase):
|
|||||||
ALERT_CONDITION equals
|
ALERT_CONDITION equals
|
||||||
ALERT_THRESHOLD 5
|
ALERT_THRESHOLD 5
|
||||||
ALERT_NAME %s
|
ALERT_NAME %s
|
||||||
ALERT_URL https:///default/alerts/%d
|
ALERT_URL %s/default/alerts/%d
|
||||||
QUERY_NAME Query
|
QUERY_NAME Query
|
||||||
QUERY_URL https:///default/queries/%d
|
QUERY_URL %s/default/queries/%d
|
||||||
QUERY_RESULT_VALUE 1
|
QUERY_RESULT_VALUE 1
|
||||||
QUERY_RESULT_ROWS [{'foo': 1}]
|
QUERY_RESULT_ROWS [{'foo': 1}]
|
||||||
QUERY_RESULT_COLS [{'name': 'foo', 'type': 'STRING'}]
|
QUERY_RESULT_COLS [{'name': 'foo', 'type': 'STRING'}]
|
||||||
</pre>
|
</pre>
|
||||||
""" % (
|
""" % (
|
||||||
alert.name,
|
alert.name,
|
||||||
|
settings.HOST,
|
||||||
alert.id,
|
alert.id,
|
||||||
|
settings.HOST,
|
||||||
alert.query_id,
|
alert.query_id,
|
||||||
)
|
)
|
||||||
result = alert.render_template(textwrap.dedent(custom_alert))
|
result = alert.render_template(textwrap.dedent(custom_alert))
|
||||||
|
|||||||
107
tests/query_runner/test_duckdb.py
Normal file
107
tests/query_runner/test_duckdb.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from redash.query_runner.duckdb import DuckDB
|
||||||
|
|
||||||
|
|
||||||
|
class TestDuckDBSchema(TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.runner = DuckDB({"dbpath": ":memory:"})
|
||||||
|
|
||||||
|
@patch.object(DuckDB, "run_query")
|
||||||
|
def test_simple_schema_build(self, mock_run_query) -> None:
|
||||||
|
# Simulate queries: first for tables, then for DESCRIBE
|
||||||
|
mock_run_query.side_effect = [
|
||||||
|
(
|
||||||
|
{"rows": [{"table_schema": "main", "table_name": "users"}]},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
{"column_name": "id", "column_type": "INTEGER"},
|
||||||
|
{"column_name": "name", "column_type": "VARCHAR"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
schema = self.runner.get_schema()
|
||||||
|
self.assertEqual(len(schema), 1)
|
||||||
|
self.assertEqual(schema[0]["name"], "main.users")
|
||||||
|
self.assertListEqual(
|
||||||
|
schema[0]["columns"],
|
||||||
|
[{"name": "id", "type": "INTEGER"}, {"name": "name", "type": "VARCHAR"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch.object(DuckDB, "run_query")
|
||||||
|
def test_struct_column_expansion(self, mock_run_query) -> None:
|
||||||
|
# First call to run_query -> tables list
|
||||||
|
mock_run_query.side_effect = [
|
||||||
|
(
|
||||||
|
{"rows": [{"table_schema": "main", "table_name": "events"}]},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
# Second call -> DESCRIBE output
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"column_name": "payload",
|
||||||
|
"column_type": "STRUCT(a INTEGER, b VARCHAR)",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
schema_list = self.runner.get_schema()
|
||||||
|
self.assertEqual(len(schema_list), 1)
|
||||||
|
schema = schema_list[0]
|
||||||
|
|
||||||
|
# Ensure both raw and expanded struct fields are present
|
||||||
|
self.assertIn("main.events", schema["name"])
|
||||||
|
self.assertListEqual(
|
||||||
|
schema["columns"],
|
||||||
|
[
|
||||||
|
{"name": "payload", "type": "STRUCT(a INTEGER, b VARCHAR)"},
|
||||||
|
{"name": "payload.a", "type": "INTEGER"},
|
||||||
|
{"name": "payload.b", "type": "VARCHAR"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_nested_struct_expansion(self) -> None:
|
||||||
|
runner = DuckDB({"dbpath": ":memory:"})
|
||||||
|
runner.con.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE sample_struct_table (
|
||||||
|
id INTEGER,
|
||||||
|
info STRUCT(
|
||||||
|
name VARCHAR,
|
||||||
|
metrics STRUCT(score DOUBLE, rank INTEGER),
|
||||||
|
tags STRUCT(primary_tag VARCHAR, secondary_tag VARCHAR)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = runner.get_schema()
|
||||||
|
table = next(t for t in schema if t["name"] == "main.sample_struct_table")
|
||||||
|
colnames = [c["name"] for c in table["columns"]]
|
||||||
|
|
||||||
|
assert "info" in colnames
|
||||||
|
assert 'info."name"' in colnames
|
||||||
|
assert "info.metrics" in colnames
|
||||||
|
assert "info.metrics.score" in colnames
|
||||||
|
assert "info.metrics.rank" in colnames
|
||||||
|
assert "info.tags.primary_tag" in colnames
|
||||||
|
assert "info.tags.secondary_tag" in colnames
|
||||||
|
|
||||||
|
@patch.object(DuckDB, "run_query")
|
||||||
|
def test_error_propagation(self, mock_run_query) -> None:
|
||||||
|
mock_run_query.return_value = (None, "boom")
|
||||||
|
with self.assertRaises(Exception) as ctx:
|
||||||
|
self.runner.get_schema()
|
||||||
|
self.assertIn("boom", str(ctx.exception))
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from redash.query_runner.pg import build_schema
|
from redash.query_runner.pg import _parse_dsn, build_schema
|
||||||
|
|
||||||
|
|
||||||
|
class TestParameters(TestCase):
|
||||||
|
def test_parse_dsn(self):
|
||||||
|
configuration = {"dsn": "application_name=redash connect_timeout=5"}
|
||||||
|
self.assertDictEqual(_parse_dsn(configuration), {"application_name": "redash", "connect_timeout": "5"})
|
||||||
|
|
||||||
|
def test_parse_dsn_not_permitted(self):
|
||||||
|
configuration = {"dsn": "password=xyz"}
|
||||||
|
self.assertRaises(ValueError, _parse_dsn, configuration)
|
||||||
|
|
||||||
|
|
||||||
class TestBuildSchema(TestCase):
|
class TestBuildSchema(TestCase):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { EditorPropTypes } from "@/visualizations/prop-types";
|
|||||||
const defaultCustomCode = trimStart(`
|
const defaultCustomCode = trimStart(`
|
||||||
// Available variables are x, ys, element, and Plotly
|
// Available variables are x, ys, element, and Plotly
|
||||||
// Type console.log(x, ys); for more info about x and ys
|
// Type console.log(x, ys); for more info about x and ys
|
||||||
// To plot your graph call Plotly.plot(element, ...)
|
// To plot your graph call Plotly.newPlot(element, ...)
|
||||||
// Plotly examples and docs: https://plot.ly/javascript/
|
// Plotly examples and docs: https://plot.ly/javascript/
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|||||||
@@ -336,6 +336,38 @@ export default function GeneralSettings({ options, data, onOptionsChange }: any)
|
|||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{includes(["line", "area"], options.globalSeriesType) && (
|
||||||
|
// @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="Line Shape"
|
||||||
|
data-test="Chart.LineShape"
|
||||||
|
defaultValue={options.lineShape}
|
||||||
|
onChange={(val: any) => onOptionsChange({ lineShape: val })}>
|
||||||
|
{/* @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 value="linear" data-test="Chart.LineShape.Linear">
|
||||||
|
Linear
|
||||||
|
{/* @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>
|
||||||
|
{/* @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 value="spline" data-test="Chart.LineShape.Spline">
|
||||||
|
Spline
|
||||||
|
{/* @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>
|
||||||
|
{/* @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 value="hv" data-test="Chart.LineShape.HorizontalVertical">
|
||||||
|
Horizontal-Vertical
|
||||||
|
{/* @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>
|
||||||
|
{/* @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 value="vh" data-test="Chart.LineShape.VerticalHorizontal">
|
||||||
|
Vertical-Horizontal
|
||||||
|
{/* @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>
|
||||||
|
)}
|
||||||
|
|
||||||
{!includes(["custom", "heatmap", "bubble"], options.globalSeriesType) && (
|
{!includes(["custom", "heatmap", "bubble"], options.globalSeriesType) && (
|
||||||
// @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
|
// @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>
|
<Section>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const DEFAULT_OPTIONS = {
|
|||||||
coefficient: 1,
|
coefficient: 1,
|
||||||
piesort: true,
|
piesort: true,
|
||||||
color_scheme: "Redash",
|
color_scheme: "Redash",
|
||||||
|
lineShape: "linear",
|
||||||
|
|
||||||
// showDataLabels: false, // depends on chart type
|
// showDataLabels: false, // depends on chart type
|
||||||
numberFormat: "0,0[.]00000",
|
numberFormat: "0,0[.]00000",
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
"x": "x",
|
"x": "x",
|
||||||
"y1": "y"
|
"y1": "y"
|
||||||
},
|
},
|
||||||
"missingValuesAsZero": true
|
"missingValuesAsZero": true,
|
||||||
|
"lineShape": "linear"
|
||||||
},
|
},
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "red" },
|
"marker": { "color": "red" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"x": "x",
|
"x": "x",
|
||||||
"y1": "y"
|
"y1": "y"
|
||||||
},
|
},
|
||||||
"missingValuesAsZero": false
|
"missingValuesAsZero": false,
|
||||||
|
"lineShape": "linear"
|
||||||
},
|
},
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "red" },
|
"marker": { "color": "red" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
@@ -68,6 +70,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["", "2 ± 0", "", "4 ± 0"],
|
"text": ["", "2 ± 0", "", "4 ± 0"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "blue" },
|
"marker": { "color": "blue" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"x": "x",
|
"x": "x",
|
||||||
"y1": "y"
|
"y1": "y"
|
||||||
},
|
},
|
||||||
"missingValuesAsZero": true
|
"missingValuesAsZero": true,
|
||||||
|
"lineShape": "linear"
|
||||||
},
|
},
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "red" },
|
"marker": { "color": "red" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
@@ -68,6 +70,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["0", "2 ± 0", "0", "4 ± 0"],
|
"text": ["0", "2 ± 0", "0", "4 ± 0"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "blue" },
|
"marker": { "color": "blue" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"x": "x",
|
"x": "x",
|
||||||
"y1": "y"
|
"y1": "y"
|
||||||
},
|
},
|
||||||
"missingValuesAsZero": true
|
"missingValuesAsZero": true,
|
||||||
|
"lineShape": "linear"
|
||||||
},
|
},
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
|
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "red" },
|
"marker": { "color": "red" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
@@ -70,6 +72,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
|
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "blue" },
|
"marker": { "color": "blue" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"x": "x",
|
"x": "x",
|
||||||
"y1": "y"
|
"y1": "y"
|
||||||
},
|
},
|
||||||
"missingValuesAsZero": true
|
"missingValuesAsZero": true,
|
||||||
|
"lineShape": "linear"
|
||||||
},
|
},
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
|
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "red" },
|
"marker": { "color": "red" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
@@ -70,6 +72,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
|
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "blue" },
|
"marker": { "color": "blue" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
"x": "x",
|
"x": "x",
|
||||||
"y1": "y"
|
"y1": "y"
|
||||||
},
|
},
|
||||||
"missingValuesAsZero": true
|
"missingValuesAsZero": true,
|
||||||
|
"lineShape": "linear"
|
||||||
},
|
},
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "red" },
|
"marker": { "color": "red" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
@@ -70,6 +72,7 @@
|
|||||||
"hoverinfo": "text+x+name",
|
"hoverinfo": "text+x+name",
|
||||||
"hover": [],
|
"hover": [],
|
||||||
"text": ["1 ± 0", "2 ± 0", "3 ± 0", "4 ± 0"],
|
"text": ["1 ± 0", "2 ± 0", "3 ± 0", "4 ± 0"],
|
||||||
|
"line": { "shape": "linear" },
|
||||||
"marker": { "color": "blue" },
|
"marker": { "color": "blue" },
|
||||||
"insidetextfont": { "color": "#ffffff" },
|
"insidetextfont": { "color": "#ffffff" },
|
||||||
"yaxis": "y"
|
"yaxis": "y"
|
||||||
|
|||||||
@@ -39,11 +39,17 @@ function prepareBarSeries(series: any, options: any, additionalOptions: any) {
|
|||||||
|
|
||||||
function prepareLineSeries(series: any, options: any) {
|
function prepareLineSeries(series: any, options: any) {
|
||||||
series.mode = "lines" + (options.showDataLabels ? "+text" : "");
|
series.mode = "lines" + (options.showDataLabels ? "+text" : "");
|
||||||
|
series.line = {
|
||||||
|
shape: options.lineShape,
|
||||||
|
}
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareAreaSeries(series: any, options: any) {
|
function prepareAreaSeries(series: any, options: any) {
|
||||||
series.mode = "lines" + (options.showDataLabels ? "+text" : "");
|
series.mode = "lines" + (options.showDataLabels ? "+text" : "");
|
||||||
|
series.line = {
|
||||||
|
shape: options.lineShape,
|
||||||
|
}
|
||||||
series.fill = options.series.stacking ? "tonexty" : "tozeroy";
|
series.fill = options.series.stacking ? "tonexty" : "tozeroy";
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
173
yarn.lock
173
yarn.lock
@@ -1955,6 +1955,11 @@
|
|||||||
parse-rect "^1.2.0"
|
parse-rect "^1.2.0"
|
||||||
pick-by-alias "^1.2.0"
|
pick-by-alias "^1.2.0"
|
||||||
|
|
||||||
|
"@plotly/regl@^2.1.2":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@plotly/regl/-/regl-2.1.2.tgz#fd31e3e820ed8824d59a67ab5e766bb101b810b6"
|
||||||
|
integrity sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==
|
||||||
|
|
||||||
"@pmmmwh/react-refresh-webpack-plugin@^0.5.10":
|
"@pmmmwh/react-refresh-webpack-plugin@^0.5.10":
|
||||||
version "0.5.10"
|
version "0.5.10"
|
||||||
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz#2eba163b8e7dbabb4ce3609ab5e32ab63dda3ef8"
|
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz#2eba163b8e7dbabb4ce3609ab5e32ab63dda3ef8"
|
||||||
@@ -1994,7 +1999,7 @@
|
|||||||
leaflet.markercluster "^1.1.0"
|
leaflet.markercluster "^1.1.0"
|
||||||
lodash "^4.17.10"
|
lodash "^4.17.10"
|
||||||
numeral "^2.0.6"
|
numeral "^2.0.6"
|
||||||
plotly.js "2.35.3"
|
plotly.js "3.1.0"
|
||||||
react-pivottable "^0.9.0"
|
react-pivottable "^0.9.0"
|
||||||
react-sortable-hoc "^1.10.1"
|
react-sortable-hoc "^1.10.1"
|
||||||
tinycolor2 "^1.4.1"
|
tinycolor2 "^1.4.1"
|
||||||
@@ -2691,10 +2696,10 @@ accepts@~1.3.8:
|
|||||||
mime-types "~2.1.34"
|
mime-types "~2.1.34"
|
||||||
negotiator "0.6.3"
|
negotiator "0.6.3"
|
||||||
|
|
||||||
ace-builds@^1.4.12, ace-builds@^1.4.6:
|
ace-builds@^1.36.3, ace-builds@^1.43.3:
|
||||||
version "1.4.12"
|
version "1.43.3"
|
||||||
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.12.tgz#888efa386e36f4345f40b5233fcc4fe4c588fae7"
|
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.43.3.tgz#ca4120fa763827dfd53c2b65a2d6b8c86f531928"
|
||||||
integrity sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg==
|
integrity sha512-MCl9rALmXwIty/4Qboijo/yNysx1r6hBTzG+6n/TiOm5LFhZpEvEIcIITPFiEOEFDfgBOEmxu+a4f54LEFM6Sg==
|
||||||
|
|
||||||
acorn-globals@^4.1.0:
|
acorn-globals@^4.1.0:
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
@@ -4176,6 +4181,11 @@ color-name@^1.0.0, color-name@~1.1.4:
|
|||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
|
color-name@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.0.0.tgz#03ff6b1b5aec9bb3cf1ed82400c2790dfcd01d2d"
|
||||||
|
integrity sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow==
|
||||||
|
|
||||||
color-normalize@1.5.0, color-normalize@^1.5.0:
|
color-normalize@1.5.0, color-normalize@^1.5.0:
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/color-normalize/-/color-normalize-1.5.0.tgz#ee610af9acb15daf73e77a945a847b18e40772da"
|
resolved "https://registry.yarnpkg.com/color-normalize/-/color-normalize-1.5.0.tgz#ee610af9acb15daf73e77a945a847b18e40772da"
|
||||||
@@ -4201,7 +4211,22 @@ color-parse@^1.3.8:
|
|||||||
defined "^1.0.0"
|
defined "^1.0.0"
|
||||||
is-plain-obj "^1.1.0"
|
is-plain-obj "^1.1.0"
|
||||||
|
|
||||||
color-rgba@2.1.1, color-rgba@^2.1.1:
|
color-parse@^2.0.0:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-parse/-/color-parse-2.0.2.tgz#37b46930424924060988edf25b24e6ffb4a1dc3f"
|
||||||
|
integrity sha512-eCtOz5w5ttWIUcaKLiktF+DxZO1R9KLNY/xhbV6CkhM7sR3GhVghmt6X6yOnzeaM24po+Z9/S1apbXMwA3Iepw==
|
||||||
|
dependencies:
|
||||||
|
color-name "^2.0.0"
|
||||||
|
|
||||||
|
color-rgba@3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-3.0.0.tgz#77090bdcdb2951c1735e20099ddd50401675375b"
|
||||||
|
integrity sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==
|
||||||
|
dependencies:
|
||||||
|
color-parse "^2.0.0"
|
||||||
|
color-space "^2.0.0"
|
||||||
|
|
||||||
|
color-rgba@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-2.1.1.tgz#4633b83817c7406c90b3d7bf4d1acfa48dde5c83"
|
resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-2.1.1.tgz#4633b83817c7406c90b3d7bf4d1acfa48dde5c83"
|
||||||
integrity sha512-VaX97wsqrMwLSOR6H7rU1Doa2zyVdmShabKrPEIFywLlHoibgD3QW9Dw6fSqM4+H/LfjprDNAUUW31qEQcGzNw==
|
integrity sha512-VaX97wsqrMwLSOR6H7rU1Doa2zyVdmShabKrPEIFywLlHoibgD3QW9Dw6fSqM4+H/LfjprDNAUUW31qEQcGzNw==
|
||||||
@@ -4218,6 +4243,11 @@ color-space@^1.14.6:
|
|||||||
hsluv "^0.0.3"
|
hsluv "^0.0.3"
|
||||||
mumath "^3.3.4"
|
mumath "^3.3.4"
|
||||||
|
|
||||||
|
color-space@^2.0.0:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-space/-/color-space-2.3.2.tgz#d8c72bab09ef26b98abebc58bc1586ce3073033d"
|
||||||
|
integrity sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==
|
||||||
|
|
||||||
color-string@^1.5.2:
|
color-string@^1.5.2:
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
|
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
|
||||||
@@ -4565,20 +4595,6 @@ css-loader@^5.2.7:
|
|||||||
schema-utils "^3.0.0"
|
schema-utils "^3.0.0"
|
||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
|
|
||||||
css-loader@^7.1.2:
|
|
||||||
version "7.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-7.1.2.tgz#64671541c6efe06b0e22e750503106bdd86880f8"
|
|
||||||
integrity sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==
|
|
||||||
dependencies:
|
|
||||||
icss-utils "^5.1.0"
|
|
||||||
postcss "^8.4.33"
|
|
||||||
postcss-modules-extract-imports "^3.1.0"
|
|
||||||
postcss-modules-local-by-default "^4.0.5"
|
|
||||||
postcss-modules-scope "^3.2.0"
|
|
||||||
postcss-modules-values "^4.0.0"
|
|
||||||
postcss-value-parser "^4.2.0"
|
|
||||||
semver "^7.5.4"
|
|
||||||
|
|
||||||
css-select@^4.1.3:
|
css-select@^4.1.3:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
|
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
|
||||||
@@ -5007,7 +5023,7 @@ devtools-protocol@0.0.818844:
|
|||||||
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e"
|
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e"
|
||||||
integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==
|
integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==
|
||||||
|
|
||||||
diff-match-patch@^1.0.4:
|
diff-match-patch@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
|
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
|
||||||
integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
|
integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
|
||||||
@@ -9386,7 +9402,7 @@ map-visit@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
maplibre-gl@^4.5.2:
|
maplibre-gl@^4.7.1:
|
||||||
version "4.7.1"
|
version "4.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-4.7.1.tgz#06a524438ee2aafbe8bcd91002a4e01468ea5486"
|
resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-4.7.1.tgz#06a524438ee2aafbe8bcd91002a4e01468ea5486"
|
||||||
integrity sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==
|
integrity sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==
|
||||||
@@ -9776,7 +9792,7 @@ nan@^2.12.1:
|
|||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||||
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
||||||
|
|
||||||
nanoid@^3.3.6, nanoid@^3.3.8:
|
nanoid@^3.3.6:
|
||||||
version "3.3.8"
|
version "3.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||||
@@ -10694,15 +10710,16 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^4.0.0"
|
find-up "^4.0.0"
|
||||||
|
|
||||||
plotly.js@2.35.3:
|
plotly.js@3.1.0:
|
||||||
version "2.35.3"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/plotly.js/-/plotly.js-2.35.3.tgz#6a7787d63b4d334948c281aa9c8df7fb941b425e"
|
resolved "https://registry.yarnpkg.com/plotly.js/-/plotly.js-3.1.0.tgz#a095a37d0f1b04bb0e9686853df54a4e6437af2e"
|
||||||
integrity sha512-7RaC6FxmCUhpD6H4MpD+QLUu3hCn76I11rotRefrh3m1iDvWqGnVqVk9dSaKmRAhFD3vsNsYea0OxnR1rc2IzQ==
|
integrity sha512-vx+CyzApL9tquFpwoPHOGSIWDbFPsA4om/tXZcnsygGUejXideDF9R5VwkltEIDG7Xuof45quVPyz1otv6Aqjw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@plotly/d3" "3.8.2"
|
"@plotly/d3" "3.8.2"
|
||||||
"@plotly/d3-sankey" "0.7.2"
|
"@plotly/d3-sankey" "0.7.2"
|
||||||
"@plotly/d3-sankey-circular" "0.33.1"
|
"@plotly/d3-sankey-circular" "0.33.1"
|
||||||
"@plotly/mapbox-gl" "1.13.4"
|
"@plotly/mapbox-gl" "1.13.4"
|
||||||
|
"@plotly/regl" "^2.1.2"
|
||||||
"@turf/area" "^7.1.0"
|
"@turf/area" "^7.1.0"
|
||||||
"@turf/bbox" "^7.1.0"
|
"@turf/bbox" "^7.1.0"
|
||||||
"@turf/centroid" "^7.1.0"
|
"@turf/centroid" "^7.1.0"
|
||||||
@@ -10711,9 +10728,8 @@ plotly.js@2.35.3:
|
|||||||
color-alpha "1.0.4"
|
color-alpha "1.0.4"
|
||||||
color-normalize "1.5.0"
|
color-normalize "1.5.0"
|
||||||
color-parse "2.0.0"
|
color-parse "2.0.0"
|
||||||
color-rgba "2.1.1"
|
color-rgba "3.0.0"
|
||||||
country-regex "^1.1.0"
|
country-regex "^1.1.0"
|
||||||
css-loader "^7.1.2"
|
|
||||||
d3-force "^1.2.1"
|
d3-force "^1.2.1"
|
||||||
d3-format "^1.4.5"
|
d3-format "^1.4.5"
|
||||||
d3-geo "^1.12.1"
|
d3-geo "^1.12.1"
|
||||||
@@ -10728,7 +10744,7 @@ plotly.js@2.35.3:
|
|||||||
has-hover "^1.0.1"
|
has-hover "^1.0.1"
|
||||||
has-passive-events "^1.0.0"
|
has-passive-events "^1.0.0"
|
||||||
is-mobile "^4.0.0"
|
is-mobile "^4.0.0"
|
||||||
maplibre-gl "^4.5.2"
|
maplibre-gl "^4.7.1"
|
||||||
mouse-change "^1.4.0"
|
mouse-change "^1.4.0"
|
||||||
mouse-event-offset "^3.0.2"
|
mouse-event-offset "^3.0.2"
|
||||||
mouse-wheel "^1.2.0"
|
mouse-wheel "^1.2.0"
|
||||||
@@ -10737,20 +10753,18 @@ plotly.js@2.35.3:
|
|||||||
point-in-polygon "^1.1.0"
|
point-in-polygon "^1.1.0"
|
||||||
polybooljs "^1.2.2"
|
polybooljs "^1.2.2"
|
||||||
probe-image-size "^7.2.3"
|
probe-image-size "^7.2.3"
|
||||||
regl "npm:@plotly/regl@^2.1.2"
|
|
||||||
regl-error2d "^2.0.12"
|
regl-error2d "^2.0.12"
|
||||||
regl-line2d "^3.1.3"
|
regl-line2d "^3.1.3"
|
||||||
regl-scatter2d "^3.3.1"
|
regl-scatter2d "^3.3.1"
|
||||||
regl-splom "^1.0.14"
|
regl-splom "^1.0.14"
|
||||||
strongly-connected-components "^1.0.1"
|
strongly-connected-components "^1.0.1"
|
||||||
style-loader "^4.0.0"
|
|
||||||
superscript-text "^1.0.0"
|
superscript-text "^1.0.0"
|
||||||
svg-path-sdf "^1.1.3"
|
svg-path-sdf "^1.1.3"
|
||||||
tinycolor2 "^1.4.2"
|
tinycolor2 "^1.4.2"
|
||||||
to-px "1.0.1"
|
to-px "1.0.1"
|
||||||
topojson-client "^3.1.0"
|
topojson-client "^3.1.0"
|
||||||
webgl-context "^2.2.0"
|
webgl-context "^2.2.0"
|
||||||
world-calendars "^1.0.3"
|
world-calendars "^1.0.4"
|
||||||
|
|
||||||
pn@^1.1.0:
|
pn@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
@@ -10782,11 +10796,6 @@ postcss-modules-extract-imports@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
|
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
|
||||||
integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
|
integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
|
||||||
|
|
||||||
postcss-modules-extract-imports@^3.1.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002"
|
|
||||||
integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==
|
|
||||||
|
|
||||||
postcss-modules-local-by-default@^4.0.0:
|
postcss-modules-local-by-default@^4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524"
|
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524"
|
||||||
@@ -10796,15 +10805,6 @@ postcss-modules-local-by-default@^4.0.0:
|
|||||||
postcss-selector-parser "^6.0.2"
|
postcss-selector-parser "^6.0.2"
|
||||||
postcss-value-parser "^4.1.0"
|
postcss-value-parser "^4.1.0"
|
||||||
|
|
||||||
postcss-modules-local-by-default@^4.0.5:
|
|
||||||
version "4.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368"
|
|
||||||
integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==
|
|
||||||
dependencies:
|
|
||||||
icss-utils "^5.0.0"
|
|
||||||
postcss-selector-parser "^7.0.0"
|
|
||||||
postcss-value-parser "^4.1.0"
|
|
||||||
|
|
||||||
postcss-modules-scope@^3.0.0:
|
postcss-modules-scope@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
|
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
|
||||||
@@ -10812,13 +10812,6 @@ postcss-modules-scope@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss-selector-parser "^6.0.4"
|
postcss-selector-parser "^6.0.4"
|
||||||
|
|
||||||
postcss-modules-scope@^3.2.0:
|
|
||||||
version "3.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c"
|
|
||||||
integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==
|
|
||||||
dependencies:
|
|
||||||
postcss-selector-parser "^7.0.0"
|
|
||||||
|
|
||||||
postcss-modules-values@^4.0.0:
|
postcss-modules-values@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
|
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
|
||||||
@@ -10834,20 +10827,12 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
|
|||||||
cssesc "^3.0.0"
|
cssesc "^3.0.0"
|
||||||
util-deprecate "^1.0.2"
|
util-deprecate "^1.0.2"
|
||||||
|
|
||||||
postcss-selector-parser@^7.0.0:
|
|
||||||
version "7.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz#4d6af97eba65d73bc4d84bcb343e865d7dd16262"
|
|
||||||
integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==
|
|
||||||
dependencies:
|
|
||||||
cssesc "^3.0.0"
|
|
||||||
util-deprecate "^1.0.2"
|
|
||||||
|
|
||||||
postcss-value-parser@^3.2.3:
|
postcss-value-parser@^3.2.3:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
||||||
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
|
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
|
||||||
|
|
||||||
postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
postcss-value-parser@^4.1.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
@@ -10870,15 +10855,6 @@ postcss@^8.2.15:
|
|||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
source-map-js "^1.0.2"
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
postcss@^8.4.33:
|
|
||||||
version "8.5.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb"
|
|
||||||
integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
|
|
||||||
dependencies:
|
|
||||||
nanoid "^3.3.8"
|
|
||||||
picocolors "^1.1.1"
|
|
||||||
source-map-js "^1.2.1"
|
|
||||||
|
|
||||||
potpack@^1.0.1:
|
potpack@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.1.tgz#d1b1afd89e4c8f7762865ec30bd112ab767e2ebf"
|
resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.1.tgz#d1b1afd89e4c8f7762865ec30bd112ab767e2ebf"
|
||||||
@@ -10984,6 +10960,15 @@ prop-types@15.x, prop-types@>=15.0.0, prop-types@^15.0.0, prop-types@^15.5.10, p
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.8.1"
|
react-is "^16.8.1"
|
||||||
|
|
||||||
|
prop-types@^15.8.1:
|
||||||
|
version "15.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
react-is "^16.13.1"
|
||||||
|
|
||||||
protocol-buffers-schema@^3.3.1:
|
protocol-buffers-schema@^3.3.1:
|
||||||
version "3.4.0"
|
version "3.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.4.0.tgz#2f0ea31ca96627d680bf2fefae7ebfa2b6453eae"
|
resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.4.0.tgz#2f0ea31ca96627d680bf2fefae7ebfa2b6453eae"
|
||||||
@@ -11476,16 +11461,16 @@ rc-virtual-list@^1.1.0, rc-virtual-list@^1.1.2:
|
|||||||
raf "^3.4.1"
|
raf "^3.4.1"
|
||||||
rc-util "^5.0.0"
|
rc-util "^5.0.0"
|
||||||
|
|
||||||
react-ace@^9.1.1:
|
react-ace@^14.0.1:
|
||||||
version "9.1.1"
|
version "14.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.1.1.tgz#fe27e1c668b0186dc1609c422198d1c2df34d2bf"
|
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-14.0.1.tgz#dba5761cee92a1e522be7e7dab7584df7e1aa1c7"
|
||||||
integrity sha512-dL0w6GwtnS1opsOoWhJaF7rF7xCM+NOEOfePmDfiaeU+EyZQ6nRWDBgyzKsuiB3hyXH3G9D6FX37ur/LKUdKjA==
|
integrity sha512-z6YAZ20PNf/FqmYEic//G/UK6uw0rn21g58ASgHJHl9rfE4nITQLqthr9rHMVQK4ezwohJbp2dGrZpkq979PYQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
ace-builds "^1.4.6"
|
ace-builds "^1.36.3"
|
||||||
diff-match-patch "^1.0.4"
|
diff-match-patch "^1.0.5"
|
||||||
lodash.get "^4.4.2"
|
lodash.get "^4.4.2"
|
||||||
lodash.isequal "^4.5.0"
|
lodash.isequal "^4.5.0"
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
react-dom@^16.14.0:
|
react-dom@^16.14.0:
|
||||||
version "16.14.0"
|
version "16.14.0"
|
||||||
@@ -11524,7 +11509,7 @@ react-grid-layout@^0.18.2:
|
|||||||
react-draggable "^4.0.0"
|
react-draggable "^4.0.0"
|
||||||
react-resizable "^1.9.0"
|
react-resizable "^1.9.0"
|
||||||
|
|
||||||
react-is@^16.12.0, react-is@^16.8.4, react-is@^16.8.6:
|
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.8.4, react-is@^16.8.6:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
@@ -11952,11 +11937,6 @@ regl@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/regl/-/regl-2.1.1.tgz#fb3eecbc684031ec6172f68aaab2cbe9c3aa3148"
|
resolved "https://registry.yarnpkg.com/regl/-/regl-2.1.1.tgz#fb3eecbc684031ec6172f68aaab2cbe9c3aa3148"
|
||||||
integrity sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==
|
integrity sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==
|
||||||
|
|
||||||
"regl@npm:@plotly/regl@^2.1.2":
|
|
||||||
version "2.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@plotly/regl/-/regl-2.1.2.tgz#fd31e3e820ed8824d59a67ab5e766bb101b810b6"
|
|
||||||
integrity sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==
|
|
||||||
|
|
||||||
relateurl@^0.2.7:
|
relateurl@^0.2.7:
|
||||||
version "0.2.7"
|
version "0.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||||
@@ -12429,11 +12409,6 @@ semver@^7.3.2, semver@^7.3.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^6.0.0"
|
lru-cache "^6.0.0"
|
||||||
|
|
||||||
semver@^7.5.4:
|
|
||||||
version "7.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
|
|
||||||
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
|
|
||||||
|
|
||||||
send@0.19.0:
|
send@0.19.0:
|
||||||
version "0.19.0"
|
version "0.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
|
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
|
||||||
@@ -12764,11 +12739,6 @@ source-map-js@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||||
|
|
||||||
source-map-js@^1.2.1:
|
|
||||||
version "1.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
|
||||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
|
||||||
|
|
||||||
source-map-loader@^1.1.3:
|
source-map-loader@^1.1.3:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.1.3.tgz#7dbc2fe7ea09d3e43c51fd9fc478b7f016c1f820"
|
resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.1.3.tgz#7dbc2fe7ea09d3e43c51fd9fc478b7f016c1f820"
|
||||||
@@ -13261,11 +13231,6 @@ style-loader@^2.0.0:
|
|||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
schema-utils "^3.0.0"
|
schema-utils "^3.0.0"
|
||||||
|
|
||||||
style-loader@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-4.0.0.tgz#0ea96e468f43c69600011e0589cb05c44f3b17a5"
|
|
||||||
integrity sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==
|
|
||||||
|
|
||||||
supercluster@^7.1.0:
|
supercluster@^7.1.0:
|
||||||
version "7.1.5"
|
version "7.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3"
|
resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.5.tgz#65a6ce4a037a972767740614c19051b64b8be5a3"
|
||||||
@@ -14549,10 +14514,10 @@ word-wrap@~1.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||||
|
|
||||||
world-calendars@^1.0.3:
|
world-calendars@^1.0.4:
|
||||||
version "1.0.3"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/world-calendars/-/world-calendars-1.0.3.tgz#b25c5032ba24128ffc41d09faf4a5ec1b9c14335"
|
resolved "https://registry.yarnpkg.com/world-calendars/-/world-calendars-1.0.4.tgz#2a12bcbd796b6c99aef2e52f281229faad8fa96c"
|
||||||
integrity sha1-slxQMrokEo/8QdCfr0pewbnBQzU=
|
integrity sha512-VGRnLJS+xJmGDPodgJRnGIDwGu0s+Cr9V2HB3EzlDZ5n0qb8h5SJtGUEkjrphZYAglEiXZ6kiXdmk0H/h/uu/w==
|
||||||
dependencies:
|
dependencies:
|
||||||
object-assign "^4.1.0"
|
object-assign "^4.1.0"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user