CEC-2970: Multiple Superset Dashboards (#229)
Co-authored-by: Alexander Andrews <aandrews@fiskerinc.com>
This commit is contained in:
committed by
GitHub
parent
843fddd0b7
commit
2d298368c5
@@ -5,8 +5,10 @@ jest.mock("../Contexts/ManifestsContext");
|
||||
jest.mock("../Contexts/UserContext");
|
||||
jest.mock("../../services/monitoring");
|
||||
jest.mock("../../services/vehiclesAPI");
|
||||
jest.mock("../../services/superset")
|
||||
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
screen,
|
||||
cleanup,
|
||||
@@ -30,7 +32,10 @@ const renderRoute = async (route) => {
|
||||
};
|
||||
|
||||
const check = async (path, selector, compare) => {
|
||||
const container = await renderRoute(path);
|
||||
let container
|
||||
await act(async () => {
|
||||
container = await renderRoute(path);
|
||||
})
|
||||
expect(container.querySelector(selector).innerHTML).toEqual(compare);
|
||||
expect(container).toMatchSnapshot();
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,10 +4,12 @@ import { useStatusContext } from "../Contexts/StatusContext";
|
||||
import { useUserContext } from "../Contexts/UserContext";
|
||||
import './index.css'
|
||||
import { embedDashboard } from "@superset-ui/embedded-sdk";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
|
||||
const Dashboard = () => {
|
||||
const { setTitle, setSitePath } = useStatusContext();
|
||||
const history = useHistory()
|
||||
|
||||
const {
|
||||
token: {
|
||||
@@ -16,17 +18,19 @@ const Dashboard = () => {
|
||||
} = useUserContext();
|
||||
|
||||
useEffect(() => {
|
||||
const urlsplit = window.location.href.split("/")
|
||||
const id = urlsplit[urlsplit.length - 1]
|
||||
setTitle("Datascope");
|
||||
setSitePath([]);
|
||||
embedDashboard({
|
||||
id: api.SupersetDashboardID(), // given by the Superset embedding UI
|
||||
id: id, // given by the Superset embedding UI
|
||||
supersetDomain: api.SupersetDashboardURL(),
|
||||
mountPoint: document.getElementById("my-superset-container"), // any html element that can contain an iframe
|
||||
fetchGuestToken: () => api.getGuestToken(token),
|
||||
dashboardUiConfig: { hideTab: true, hideTitle: true }, // dashboard UI config: hideTitle, hideTab, hideChartControls (optional)
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [history.location]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { List } from "@material-ui/core";
|
||||
import HomeIcon from "@material-ui/icons/Home";
|
||||
import DirectionsCarIcon from "@material-ui/icons/DirectionsCar";
|
||||
@@ -13,6 +13,8 @@ import ListItemExternalLink from "../ListItemExternalLink";
|
||||
import { useUserContext } from "../Contexts/UserContext";
|
||||
import { Roles, hasRole } from "../../utils/roles";
|
||||
|
||||
import supersetAPI from "../../services/superset";
|
||||
|
||||
const menuData = [
|
||||
{
|
||||
label: "Home",
|
||||
@@ -38,12 +40,6 @@ const menuData = [
|
||||
icon: <CommuteIcon />,
|
||||
roles: [Roles.READ, Roles.CREATE],
|
||||
},
|
||||
{
|
||||
label: "Datascope",
|
||||
to: "/datascope",
|
||||
icon: <AssessmentIcon />,
|
||||
roles: [Roles.READ, Roles.CREATE],
|
||||
},
|
||||
{
|
||||
label: "Suppliers",
|
||||
to: "/suppliers",
|
||||
@@ -103,13 +99,30 @@ const ExpandableSideMenuItem = ({ item }) => (
|
||||
|
||||
const SideMenu = () => {
|
||||
const { groups } = useUserContext();
|
||||
const menu = menuData.reduce((result, item) => {
|
||||
if (hasRole(item.roles, groups)) {
|
||||
result.push(item);
|
||||
const [menu, setMenu] = useState(menuData)
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
},
|
||||
} = useUserContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (groups && token) {
|
||||
const internalEffect = async (token) => {
|
||||
const datasscopeitem = await SupersetItemList(token)
|
||||
menuData.push(datasscopeitem)
|
||||
FilterAccessible(groups, setMenu)
|
||||
}
|
||||
|
||||
internalEffect(token)
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}, [groups, token])
|
||||
|
||||
useEffect(() => {
|
||||
FilterAccessible(groups, setMenu)
|
||||
}, [groups])
|
||||
|
||||
|
||||
return (
|
||||
<List>
|
||||
@@ -123,4 +136,36 @@ const SideMenu = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const FilterAccessible = (groups, setMenu) => {
|
||||
|
||||
const filteredMenu = menuData.reduce((result, item) => {
|
||||
if (hasRole(item.roles, groups)) {
|
||||
result.push(item);
|
||||
}
|
||||
return result;
|
||||
}, [])
|
||||
setMenu(filteredMenu)
|
||||
}
|
||||
|
||||
// Will get the set of superset embeddable dashboards, and put it in the submenu style
|
||||
const SupersetItemList = async (token) => {
|
||||
const embeddedDashboards = await supersetAPI.getEmbeddedDashboards(token)
|
||||
|
||||
const submenus = embeddedDashboards.map((dashboard) => {
|
||||
return {
|
||||
label: dashboard.title,
|
||||
to: "/datascope/" + dashboard.embedded_id,
|
||||
roles: [],
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
label: "Datascope",
|
||||
to: "/datascope",
|
||||
icon: <AssessmentIcon />,
|
||||
roles: [Roles.READ, Roles.CREATE],
|
||||
submenus: submenus
|
||||
}
|
||||
}
|
||||
|
||||
export default SideMenu;
|
||||
@@ -1,6 +1,7 @@
|
||||
jest.mock("../Contexts/UserContext");
|
||||
jest.mock("../../services/superset")
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { render, waitFor, screen } from "@testing-library/react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { UserProvider, setToken } from "../Contexts/UserContext";
|
||||
import { TEST_AUTH_OBJECT } from "../../utils/testing";
|
||||
@@ -22,17 +23,15 @@ const renderMenu = async () => {
|
||||
describe("SideMenu", () => {
|
||||
beforeAll(() => {
|
||||
addSnapshotSerializer(expect);
|
||||
});
|
||||
|
||||
it("Unauthenticated", async () => {
|
||||
setToken(null);
|
||||
const container = await renderMenu();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("Authenticated", async () => {
|
||||
setToken(TEST_AUTH_OBJECT);
|
||||
const container = await renderMenu();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Datascope')).toBeInTheDocument()
|
||||
})
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,42 +152,6 @@ exports[`SideMenu Authenticated 1`] = `
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||
href="/datascope"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="MuiListItemIcon-root"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
Datascope
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<span>
|
||||
<li>
|
||||
<a
|
||||
@@ -274,55 +238,70 @@ exports[`SideMenu Authenticated 1`] = `
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SideMenu Unauthenticated 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<ul
|
||||
class="MuiList-root MuiList-padding"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||
href="/home"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="MuiListItemIcon-root"
|
||||
<span>
|
||||
<li>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||
href="/datascope"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
<div
|
||||
class="MuiListItemIcon-root"
|
||||
>
|
||||
<path
|
||||
d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="MuiListItemText-root"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="MuiListItemText-root"
|
||||
>
|
||||
<span
|
||||
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
Datascope
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</span>
|
||||
<ul
|
||||
style="margin-left: 50px;"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
aria-disabled="false"
|
||||
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
|
||||
href="/datascope/00000000-0000-0000-0000-000000000000"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="MuiListItemText-root"
|
||||
>
|
||||
Home
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<span
|
||||
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
||||
>
|
||||
test title
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="MuiTouchRipple-root"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
13
src/services/__mocks__/superset.js
Normal file
13
src/services/__mocks__/superset.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const SupersetAPI = {
|
||||
getEmbeddedDashboards: async () => {
|
||||
return [{
|
||||
title: "test title",
|
||||
embedded_id: "00000000-0000-0000-0000-000000000000"
|
||||
}]
|
||||
},
|
||||
SupersetDashboardID: () => {
|
||||
return "11111100-0000-1111-1111-000000000000"
|
||||
}
|
||||
}
|
||||
|
||||
export default SupersetAPI
|
||||
@@ -21,15 +21,32 @@ const supersetAPI = {
|
||||
let q = r["token"]
|
||||
return q
|
||||
},
|
||||
getEmbeddedDashboards: async(token) => {
|
||||
const u = addQueryParams(`${API_ENDPOINT}/dashboard/embedded-dashboards`);
|
||||
let res = await fetch(u, {
|
||||
method: "GET",
|
||||
headers: Object.assign(
|
||||
getAuthHeaderOptions(token)
|
||||
),
|
||||
})
|
||||
if(res.status !== 200){
|
||||
return [{title: "dashboard", embedded_id: GetSupersetDashboardID()}]
|
||||
}
|
||||
let r = await res.json()
|
||||
return r
|
||||
},
|
||||
SupersetDashboardURL: () => {
|
||||
const SUPERSET_BASE_URL = process.env.REACT_APP_SUPERSET_URL;
|
||||
return SUPERSET_BASE_URL
|
||||
},
|
||||
SupersetDashboardID: () => {
|
||||
const SUPERSET_BASE_ID = process.env.REACT_APP_SUPERSET_KEYS_LIST;
|
||||
return SUPERSET_BASE_ID
|
||||
return GetSupersetDashboardID()
|
||||
}
|
||||
}
|
||||
|
||||
const GetSupersetDashboardID = () => {
|
||||
const SUPERSET_BASE_ID = process.env.REACT_APP_SUPERSET_KEYS_LIST;
|
||||
return SUPERSET_BASE_ID
|
||||
}
|
||||
|
||||
export default supersetAPI;
|
||||
15
src/services/superset.test.js
Normal file
15
src/services/superset.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import superset from "./superset"
|
||||
|
||||
describe("Superset tests", () => {
|
||||
it("Outdated API doesn't cause problems", async () => {
|
||||
process.env.REACT_APP_SUPERSET_KEYS_LIST = "11111100-0000-1111-1111-000000000000"
|
||||
global.fetch = jest.fn(() => {
|
||||
return { status: 404 }
|
||||
}
|
||||
);
|
||||
|
||||
const res = await superset.getEmbeddedDashboards()
|
||||
expect(res).toHaveLength(1)
|
||||
expect(res[0].embedded_id).toBe("11111100-0000-1111-1111-000000000000")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user