Unverified Commit 8d5bda39 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

fix: Updates the Dashboard Page URL (#473)

* Modifying DashboardPage component, tests passing

* Cleanups and updating Dashboard resource links

* Flattens folder structure on the component folders

* Extract helpers for building URI and Dashboard Detail Page URL, implement on search results component

* Adding types and cleaning up

* Changing URL schema to use the URI directly on the URL

* Cleanup and basic test for url helper
parent 234a53de
import * as React from 'react'; import * as React from "react";
import * as History from 'history'; import * as History from "history";
import { shallow } from 'enzyme'; import { shallow } from "enzyme";
import { DashboardPage, DashboardPageProps, RouteProps } from './'; import AvatarLabel from "components/common/AvatarLabel";
import { getMockRouterProps } from '../../fixtures/mockRouter'; import LoadingSpinner from "components/common/LoadingSpinner";
import AvatarLabel from 'components/common/AvatarLabel'; import Breadcrumb from "components/common/Breadcrumb";
import LoadingSpinner from 'components/common/LoadingSpinner'; import BookmarkIcon from "components/common/Bookmark/BookmarkIcon";
import Breadcrumb from 'components/common/Breadcrumb'; import Flag from "components/common/Flag";
import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon'; import ResourceList from "components/common/ResourceList";
import Flag from 'components/common/Flag'; import TabsComponent from "components/common/TabsComponent";
import ResourceList from 'components/common/ResourceList'; import ChartList from "./ChartList";
import TabsComponent from 'components/common/TabsComponent'; import ImagePreview from "./ImagePreview";
import ChartList from './ChartList';
import ImagePreview from './ImagePreview'; import { DashboardPage, DashboardPageProps, MatchProps } from "./";
import * as Constants from './constants'; import { getMockRouterProps } from "../../fixtures/mockRouter";
import { dashboardMetadata } from "fixtures/metadata/dashboard";
import * as ConfigUtils from 'config/config-utils';
import { dashboardMetadata } from 'fixtures/metadata/dashboard'; import { NO_TIMESTAMP_TEXT } from "components/constants";
import * as LogUtils from 'utils/logUtils'; import * as Constants from "./constants";
import { ResourceType } from 'interfaces'; import * as LogUtils from "utils/logUtils";
import { NO_TIMESTAMP_TEXT } from 'components/constants'; import { ResourceType } from "interfaces";
const MOCK_DISPLAY_NAME = 'displayName'; const MOCK_DISPLAY_NAME = "displayName";
const MOCK_ICON_CLASS = 'dashboard-icon'; const MOCK_ICON_CLASS = "dashboard-icon";
jest.mock('config/config-utils', () => ( jest.mock("config/config-utils", () => ({
{ getSourceDisplayName: jest.fn(() => {
getSourceDisplayName: jest.fn(() => { return MOCK_DISPLAY_NAME }), return MOCK_DISPLAY_NAME;
getSourceIconClass: jest.fn(() => { return MOCK_ICON_CLASS }), }),
} getSourceIconClass: jest.fn(() => {
)); return MOCK_ICON_CLASS;
})
describe('DashboardPage', () => { }));
const setStateSpy = jest.spyOn(DashboardPage.prototype, 'setState'); const setStateSpy = jest.spyOn(DashboardPage.prototype, "setState");
const setup = (propOverrides?: Partial<DashboardPageProps>, location?: Partial<History.Location>) => {
const routerProps = getMockRouterProps<RouteProps>(null, location); const TEST_CLUSTER = "gold";
const props = { const TEST_PRODUCT = "mode";
isLoading: false, const TEST_GROUP = "234testGroupID";
statusCode: 200, const TEST_DASHBOARD = "123DashboardID";
dashboard: dashboardMetadata,
getDashboard: jest.fn(), const setup = (
...routerProps, propOverrides?: Partial<DashboardPageProps>,
...propOverrides, location?: Partial<History.Location>
}; ) => {
const routerProps = getMockRouterProps<MatchProps>(
const wrapper = shallow<DashboardPage>(<DashboardPage {...props} />) {
return { props, wrapper }; uri: "mode_dashboard://gold.234testGroupID/123DashboardID"
},
location
);
const props = {
isLoading: false,
statusCode: 200,
dashboard: dashboardMetadata,
getDashboard: jest.fn(),
...routerProps,
...propOverrides
}; };
const wrapper = shallow<DashboardPage>(<DashboardPage {...props} />);
return { props, wrapper };
};
describe("DashboardPage", () => {
describe("componentDidMount", () => {
it("calls getDashboard", () => {
const { props, wrapper } = setup();
describe('componentDidMount', () => {
it('calls loadDashboard with uri from state', () => {
const wrapper = setup().wrapper;
const loadDashboardSpy = jest.spyOn(wrapper.instance(), 'loadDashboard');
wrapper.instance().componentDidMount(); wrapper.instance().componentDidMount();
expect(loadDashboardSpy).toHaveBeenCalledWith(wrapper.state().uri);
expect(props.getDashboard).toHaveBeenCalled();
});
it("calls getDashboard with the right parameters", () => {
const { props, wrapper } = setup();
const expectedURI = `${TEST_PRODUCT}_dashboard://${TEST_CLUSTER}.${TEST_GROUP}/${TEST_DASHBOARD}`;
const expectedArguments = {
source: undefined,
searchIndex: undefined,
uri: expectedURI
};
wrapper.instance().componentDidMount();
expect(props.getDashboard).toHaveBeenCalledWith(expectedArguments);
}); });
}); });
describe('componentDidUpdate', () => { describe("componentDidUpdate", () => {
let props; let props;
let wrapper; let wrapper;
let loadDashboardSpy; let getDashboardSpy;
beforeEach(() => { beforeEach(() => {
const setupResult = setup(null, { ({ props, wrapper } = setup());
search: '/dashboard?uri=testUri',
});
props = setupResult.props;
wrapper = setupResult.wrapper;
loadDashboardSpy = jest.spyOn(wrapper.instance(), 'loadDashboard');
});
it('calls loadDashboard when uri has changes', () => { getDashboardSpy = props.getDashboard;
loadDashboardSpy.mockClear();
setStateSpy.mockClear();
wrapper.setProps({ location: { search: '/dashboard?uri=newUri'}});
expect(loadDashboardSpy).toHaveBeenCalledWith('newUri');
expect(setStateSpy).toHaveBeenCalledWith({ uri: 'newUri'});
}); });
it('does not call loadDashboard when uri has not changed', () => { describe('when params change', () => {
loadDashboardSpy.mockClear(); it("calls getDashboard", () => {
setStateSpy.mockClear(); getDashboardSpy.mockClear();
wrapper.instance().componentDidUpdate(); setStateSpy.mockClear();
expect(loadDashboardSpy).not.toHaveBeenCalled(); const newParams = {
expect(setStateSpy).not.toHaveBeenCalled(); uri: "testProduct_dashboard://testCluster.testGroupID/testDashboardID"
};
const expectedURI = `testProduct_dashboard://testCluster.testGroupID/testDashboardID`;
const expectedArguments = {
searchIndex: undefined,
source: undefined,
uri: expectedURI,
};
wrapper.setProps({ match: { params: newParams } });
expect(getDashboardSpy).toHaveBeenCalledWith(expectedArguments);
expect(setStateSpy).toHaveBeenCalledWith({ uri: expectedURI });
});
}); });
});
describe('when params do not change', () => {
it("does not call getDashboard", () => {
getDashboardSpy.mockClear();
setStateSpy.mockClear();
describe('loadDashboard', () => { wrapper.instance().componentDidUpdate();
let getLoggingParamsSpy;
let props; expect(getDashboardSpy).not.toHaveBeenCalled();
let wrapper; expect(setStateSpy).not.toHaveBeenCalled();
beforeAll(() => { });
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
getLoggingParamsSpy = jest.spyOn(LogUtils, 'getLoggingParams');
wrapper.instance().loadDashboard('testUri');
})
it('calls getLoggingParams', () => {
expect(getLoggingParamsSpy).toHaveBeenCalledWith(props.location.search);
});
it('calls props.getDashboard', () => {
expect(props.getDashboard).toHaveBeenCalled();
}); });
}); });
describe('mapStatusToStyle', () => { describe("mapStatusToStyle", () => {
let wrapper; let wrapper;
beforeAll(() => { beforeAll(() => {
wrapper = setup().wrapper ({ wrapper } = setup());
}); });
it('returns success if status === LAST_RUN_SUCCEEDED', () => {
expect(wrapper.instance().mapStatusToStyle(Constants.LAST_RUN_SUCCEEDED)).toBe('success'); it("returns success if status === LAST_RUN_SUCCEEDED", () => {
expect(
wrapper.instance().mapStatusToStyle(Constants.LAST_RUN_SUCCEEDED)
).toBe("success");
}); });
it('returns danger if status !== LAST_RUN_SUCCEEDED', () => {
expect(wrapper.instance().mapStatusToStyle('anythingelse')).toBe('danger'); it("returns danger if status !== LAST_RUN_SUCCEEDED", () => {
expect(wrapper.instance().mapStatusToStyle("anythingelse")).toBe(
"danger"
);
}); });
}); });
describe('render', () => { describe("render", () => {
const { props, wrapper } = setup(); const { props, wrapper } = setup();
it('renders the loading spinner when loading', () => { it("renders the loading spinner when loading", () => {
const { props, wrapper } = setup({ isLoading: true }) const { wrapper } = setup({ isLoading: true });
expect(wrapper.find(LoadingSpinner).exists()).toBeTruthy(); expect(wrapper.find(LoadingSpinner).exists()).toBeTruthy();
}); });
it('renders a breadcrumb component', () => { it("renders a breadcrumb component", () => {
expect(wrapper.find(Breadcrumb).exists()).toBeTruthy(); expect(wrapper.find(Breadcrumb).exists()).toBeTruthy();
}); });
it('renders a the dashboard title', () => { it("renders a the dashboard title", () => {
const headerText = wrapper.find('.header-title-text').text(); const headerText = wrapper.find(".header-title-text").text();
expect(headerText).toEqual(props.dashboard.name); expect(headerText).toEqual(props.dashboard.name);
}); });
it('renders a bookmark icon with correct props', () => { it("renders a bookmark icon with correct props", () => {
const elementProps = wrapper.find(BookmarkIcon).props(); const elementProps = wrapper.find(BookmarkIcon).props();
expect(elementProps.bookmarkKey).toBe(props.dashboard.uri); expect(elementProps.bookmarkKey).toBe(props.dashboard.uri);
expect(elementProps.resourceType).toBe(ResourceType.dashboard); expect(elementProps.resourceType).toBe(ResourceType.dashboard);
}); });
describe('renders description', () => { describe("renders description", () => {
it('with link to add description if none exists', () => { it("with link to add description if none exists", () => {
const wrapper = setup({ const { wrapper } = setup({
dashboard: { dashboard: {
...dashboardMetadata, ...dashboardMetadata,
description: '', description: ""
} }
}).wrapper; });
const link = wrapper.find('a.edit-link'); const link = wrapper.find("a.edit-link");
expect(link.props().href).toBe(props.dashboard.url); expect(link.props().href).toBe(props.dashboard.url);
expect(link.text()).toBe(`${Constants.ADD_DESC_TEXT} ${MOCK_DISPLAY_NAME}`); expect(link.text()).toBe(
`${Constants.ADD_DESC_TEXT} ${MOCK_DISPLAY_NAME}`
);
}); });
}); });
describe('renders owners', () => { describe("renders owners", () => {
it('with correct AvatarLabel if no owners exist', () => { it("with correct AvatarLabel if no owners exist", () => {
const wrapper = setup({ const { wrapper } = setup({
dashboard: { dashboard: {
...dashboardMetadata, ...dashboardMetadata,
owners: [], owners: []
} }
}).wrapper; });
expect(wrapper.find(AvatarLabel).props().label).toBe(Constants.NO_OWNER_TEXT)
expect(wrapper.find(AvatarLabel).props().label).toBe(
Constants.NO_OWNER_TEXT
);
}); });
}); });
it('renders a Flag for last run state', () => { it("renders a Flag for last run state", () => {
const mapStatusToStyleSpy = jest.spyOn(wrapper.instance(), 'mapStatusToStyle').mockImplementationOnce(() => 'testStyle'); const mapStatusToStyleSpy = jest
.spyOn(wrapper.instance(), "mapStatusToStyle")
.mockImplementationOnce(() => "testStyle");
wrapper.instance().forceUpdate(); wrapper.instance().forceUpdate();
const element = wrapper.find('.last-run-state').find(Flag); const element = wrapper.find(".last-run-state").find(Flag);
expect(element.props().text).toBe(props.dashboard.last_run_state); expect(element.props().text).toBe(props.dashboard.last_run_state);
expect(mapStatusToStyleSpy).toHaveBeenCalledWith(props.dashboard.last_run_state); expect(mapStatusToStyleSpy).toHaveBeenCalledWith(
expect(element.props().labelStyle).toBe('testStyle'); props.dashboard.last_run_state
}) );
expect(element.props().labelStyle).toBe("testStyle");
});
it('renders an ImagePreview with correct props', () => { it("renders an ImagePreview with correct props", () => {
expect(wrapper.find(ImagePreview).props().uri).toBe(wrapper.state().uri); expect(wrapper.find(ImagePreview).props().uri).toBe(wrapper.state().uri);
}) });
describe('renders timestamps correctly when unavailable', () => { describe("renders timestamps correctly when unavailable", () => {
const { wrapper } = setup({ const { wrapper } = setup({
dashboard: { dashboard: {
...dashboardMetadata, ...dashboardMetadata,
...@@ -196,38 +241,46 @@ describe('DashboardPage', () => { ...@@ -196,38 +241,46 @@ describe('DashboardPage', () => {
} }
}); });
it('last_run_timestamp', () => { it("last_run_timestamp", () => {
expect(wrapper.find('.last-run-timestamp').text()).toEqual(NO_TIMESTAMP_TEXT) expect(wrapper.find(".last-run-timestamp").text()).toEqual(
NO_TIMESTAMP_TEXT
);
}); });
it('last_successful_run_timestamp', () => { it("last_successful_run_timestamp", () => {
expect(wrapper.find('.last-successful-run-timestamp').text()).toEqual(NO_TIMESTAMP_TEXT) expect(wrapper.find(".last-successful-run-timestamp").text()).toEqual(
NO_TIMESTAMP_TEXT
);
}); });
}); });
}); });
describe('renderTabs', () => { describe("renderTabs", () => {
const { props, wrapper } = setup(); const { props, wrapper } = setup();
it('returns a ResourceList', () => {
it("returns a ResourceList", () => {
const result = shallow(wrapper.instance().renderTabs()); const result = shallow(wrapper.instance().renderTabs());
const element = result.find(ResourceList); const element = result.find(ResourceList);
expect(element.exists()).toBe(true); expect(element.exists()).toBe(true);
expect(element.props().allItems).toEqual(props.dashboard.tables); expect(element.props().allItems).toEqual(props.dashboard.tables);
}); });
it('returns a Tabs component', () => { it("returns a Tabs component", () => {
const result = wrapper.instance().renderTabs(); const result = wrapper.instance().renderTabs();
expect(result.type).toEqual(TabsComponent); expect(result.type).toEqual(TabsComponent);
}); });
it('does not render ChartList if no charts', () => { it("does not render ChartList if no charts", () => {
const wrapper = setup({ const { wrapper } = setup({
dashboard: { dashboard: {
...dashboardMetadata, ...dashboardMetadata,
chart_names: [], chart_names: []
} }
}).wrapper; });
const result = shallow(wrapper.instance().renderTabs()); const result = shallow(wrapper.instance().renderTabs());
expect(result.find(ChartList).exists()).toBe(false); expect(result.find(ChartList).exists()).toBe(false);
}); });
}); });
......
...@@ -42,9 +42,8 @@ import { NO_TIMESTAMP_TEXT } from 'components/constants'; ...@@ -42,9 +42,8 @@ import { NO_TIMESTAMP_TEXT } from 'components/constants';
import './styles.scss'; import './styles.scss';
export interface RouteProps { const STATUS_SUCCESS = 'success';
uri: string; const STATUS_DANGER = 'danger';
}
interface DashboardPageState { interface DashboardPageState {
uri: string; uri: string;
...@@ -56,42 +55,48 @@ export interface StateFromProps { ...@@ -56,42 +55,48 @@ export interface StateFromProps {
dashboard: DashboardMetadata; dashboard: DashboardMetadata;
} }
export interface MatchProps {
uri: string;
}
export interface DispatchFromProps { export interface DispatchFromProps {
getDashboard: (payload: { uri: string, searchIndex?: string, source?: string }) => GetDashboardRequest; getDashboard: (payload: { uri: string, searchIndex?: string, source?: string }) => GetDashboardRequest;
} }
export type DashboardPageProps = RouteComponentProps<RouteProps> & StateFromProps & DispatchFromProps; export type DashboardPageProps = RouteComponentProps<MatchProps> & StateFromProps & DispatchFromProps;
export class DashboardPage extends React.Component<DashboardPageProps, DashboardPageState> { export class DashboardPage extends React.Component<DashboardPageProps, DashboardPageState> {
constructor(props) { constructor(props) {
super(props); super(props);
const { uri } = this.props.match.params;
const { uri } = qs.parse(this.props.location.search);
this.state = { uri }; this.state = { uri };
} }
componentDidMount() { componentDidMount() {
this.loadDashboard(this.state.uri);
}
loadDashboard(uri: string) {
const { index, source } = getLoggingParams(this.props.location.search); const { index, source } = getLoggingParams(this.props.location.search);
const { uri } = this.props.match.params;
this.props.getDashboard({ source, uri, searchIndex: index }); this.props.getDashboard({ source, uri, searchIndex: index });
this.setState({ uri });
} }
componentDidUpdate() { componentDidUpdate() {
const { uri } = qs.parse(this.props.location.search); const { uri } = this.props.match.params;
if (this.state.uri !== uri) { if (this.state.uri !== uri) {
const { index, source } = getLoggingParams(this.props.location.search);
this.setState({ uri }); this.setState({ uri });
this.loadDashboard(uri); this.props.getDashboard({ source, uri, searchIndex: index });
} }
}; };
mapStatusToStyle = (status: string): string => { mapStatusToStyle = (status: string): string => {
if (status === LAST_RUN_SUCCEEDED) { if (status === LAST_RUN_SUCCEEDED) {
return 'success'; return STATUS_SUCCESS;
} }
return 'danger'; return STATUS_DANGER;
}; };
renderTabs() { renderTabs() {
......
...@@ -10,27 +10,27 @@ import LoadingSpinner from '../common/LoadingSpinner'; ...@@ -10,27 +10,27 @@ import LoadingSpinner from '../common/LoadingSpinner';
import { TableDetail, TableDetailProps, MatchProps } from './'; import { TableDetail, TableDetailProps, MatchProps } from './';
describe('TableDetail', () => { const setup = (propOverrides?: Partial<TableDetailProps>, location?: Partial<History.Location>) => {
const routerProps = getMockRouterProps<MatchProps>({
const setup = (propOverrides?: Partial<TableDetailProps>, location?: Partial<History.Location>) => { "cluster":"gold",
const routerProps = getMockRouterProps<MatchProps>({ "database":"hive",
"cluster":"gold", "schema":"base",
"database":"hive", "table":"rides"
"schema":"base", }, location);
"table":"rides" const props = {
}, location); isLoading: false,
const props = { statusCode: 200,
isLoading: false, tableData: tableMetadata,
statusCode: 200, getTableData: jest.fn(),
tableData: tableMetadata, ...routerProps,
getTableData: jest.fn(), ...propOverrides,
...routerProps,
...propOverrides,
};
const wrapper = mount<TableDetail>(<TableDetail {...props} />);
return { props, wrapper };
}; };
const wrapper = mount<TableDetail>(<TableDetail {...props} />);
return { props, wrapper };
};
describe('TableDetail', () => {
describe('render', () => { describe('render', () => {
......
...@@ -88,7 +88,7 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone ...@@ -88,7 +88,7 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone
this.didComponentMount = true; this.didComponentMount = true;
} }
componentDidUpdate(prevProps) { componentDidUpdate() {
const newKey = this.getTableKey(); const newKey = this.getTableKey();
if (this.key !== newKey) { if (this.key !== newKey) {
...@@ -111,6 +111,7 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone ...@@ -111,6 +111,7 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone
DO NOT CHANGE DO NOT CHANGE
*/ */
const params = this.props.match.params; const params = this.props.match.params;
return `${params.database}://${params.cluster}.${params.schema}/${params.table}`; return `${params.database}://${params.cluster}.${params.schema}/${params.table}`;
} }
......
...@@ -3,7 +3,7 @@ import * as Avatar from 'react-avatar'; ...@@ -3,7 +3,7 @@ import * as Avatar from 'react-avatar';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import AvatarLabel, { AvatarLabelProps } from '../'; import AvatarLabel, { AvatarLabelProps } from '.';
describe('AvatarLabel', () => { describe('AvatarLabel', () => {
const setup = (propOverrides?: Partial<AvatarLabelProps>) => { const setup = (propOverrides?: Partial<AvatarLabelProps>) => {
......
import * as React from 'react'; import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import BadgeList from '../' import BadgeList from '.'
import Flag from 'components/common/Flag'; import Flag from 'components/common/Flag';
import { BadgeStyle } from 'config/config-types'; import { BadgeStyle } from 'config/config-types';
import * as ConfigUtils from 'config/config-utils'; import * as ConfigUtils from 'config/config-utils';
......
...@@ -6,7 +6,7 @@ import globalState from 'fixtures/globalState'; ...@@ -6,7 +6,7 @@ import globalState from 'fixtures/globalState';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import { BookmarkIcon, BookmarkIconProps, mapDispatchToProps, mapStateToProps } from "../"; import { BookmarkIcon, BookmarkIconProps, mapDispatchToProps, mapStateToProps } from ".";
describe('BookmarkIcon', () => { describe('BookmarkIcon', () => {
......
...@@ -3,7 +3,7 @@ import * as React from 'react'; ...@@ -3,7 +3,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Breadcrumb, BreadcrumbProps, mapDispatchToProps } from '../'; import { Breadcrumb, BreadcrumbProps, mapDispatchToProps } from '.';
describe('Breadcrumb', () => { describe('Breadcrumb', () => {
const setup = (propOverrides?: Partial<BreadcrumbProps>) => { const setup = (propOverrides?: Partial<BreadcrumbProps>) => {
......
...@@ -3,7 +3,7 @@ import * as ReactMarkdown from 'react-markdown'; ...@@ -3,7 +3,7 @@ import * as ReactMarkdown from 'react-markdown';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import EditableText, { EditableTextProps } from '../'; import EditableText, { EditableTextProps } from '.';
import { import {
CANCEL_BUTTON_TEXT, CANCEL_BUTTON_TEXT,
REFRESH_BUTTON_TEXT, REFRESH_BUTTON_TEXT,
......
...@@ -3,7 +3,7 @@ import * as React from 'react'; ...@@ -3,7 +3,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import InfoButton from 'components/common/InfoButton'; import InfoButton from 'components/common/InfoButton';
import EntityCardSection, { EntityCardSectionProps } from '../'; import EntityCardSection, { EntityCardSectionProps } from '.';
describe('EntityCardSection', () => { describe('EntityCardSection', () => {
let props: EntityCardSectionProps; let props: EntityCardSectionProps;
......
...@@ -2,8 +2,8 @@ import * as React from 'react'; ...@@ -2,8 +2,8 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import EntityCard, { EntityCardProps } from '../'; import EntityCard, { EntityCardProps } from '.';
import EntityCardSection from '../EntityCardSection'; import EntityCardSection from './EntityCardSection';
describe('EntityCard', () => { describe('EntityCard', () => {
let props: EntityCardProps; let props: EntityCardProps;
......
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import Flag, { CaseType, FlagProps, convertText } from '../'; import Flag, { CaseType, FlagProps, convertText } from '.';
describe('Flag', () => { describe('Flag', () => {
let props: FlagProps; let props: FlagProps;
......
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import FlashMessage, { FlashMessageProps } from '../'; import FlashMessage, { FlashMessageProps } from '.';
describe('FlashMessage', () => { describe('FlashMessage', () => {
const setup = (propOverrides?: Partial<FlashMessageProps>) => { const setup = (propOverrides?: Partial<FlashMessageProps>) => {
......
import * as React from 'react'; import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import CheckBoxItem, { CheckBoxItemProps } from '../'; import CheckBoxItem, { CheckBoxItemProps } from '.';
describe('CheckBoxItem', () => { describe('CheckBoxItem', () => {
const expectedChild = (<span>I am a child</span>); const expectedChild = (<span>I am a child</span>);
......
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import LoadingSpinner from '../'; import LoadingSpinner from '.';
describe('LoadingSpinner', () => { describe('LoadingSpinner', () => {
let subject; let subject;
......
import * as React from 'react'; import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Preloader, PreloaderProps, mapDispatchToProps } from '../'; import { Preloader, PreloaderProps, mapDispatchToProps } from '.';
describe('Preloader', () => { describe('Preloader', () => {
const setup = (propOverrides?: Partial<PreloaderProps>) => { const setup = (propOverrides?: Partial<PreloaderProps>) => {
......
...@@ -45,8 +45,10 @@ describe('DashboardListItem', () => { ...@@ -45,8 +45,10 @@ describe('DashboardListItem', () => {
describe('getLink', () => { describe('getLink', () => {
it('getLink returns correct string', () => { it('getLink returns correct string', () => {
const { props, wrapper } = setup(); const { props, wrapper } = setup();
const { dashboard, logging } = props; const expectedURL = "/dashboard/mode_dashboard%3A%2F%2Fcluster.group%2Fname?index=0&source=src";
expect(wrapper.instance().getLink()).toEqual(`/dashboard?uri=${dashboard.uri}&index=${logging.index}&source=${logging.source}`); const actual = wrapper.instance().getLink();
expect(actual).toEqual(expectedURL);
}); });
}); });
......
...@@ -7,11 +7,11 @@ import { LoggingParams } from '../types'; ...@@ -7,11 +7,11 @@ import { LoggingParams } from '../types';
import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon'; import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
import { getSourceDisplayName, getSourceIconClass } from 'config/config-utils'; import { getSourceDisplayName, getSourceIconClass } from 'config/config-utils';
import { buildDashboardURL } from 'utils/navigationUtils';
import { formatDate } from 'utils/dateUtils';
import { ResourceType, DashboardResource } from 'interfaces'; import { ResourceType, DashboardResource } from 'interfaces';
import { formatDate } from 'utils/dateUtils';
import * as Constants from './constants'; import * as Constants from './constants';
import { NO_TIMESTAMP_TEXT } from 'components/constants'; import { NO_TIMESTAMP_TEXT } from 'components/constants';
...@@ -21,9 +21,11 @@ export interface DashboardListItemProps { ...@@ -21,9 +21,11 @@ export interface DashboardListItemProps {
} }
class DashboardListItem extends React.Component<DashboardListItemProps, {}> { class DashboardListItem extends React.Component<DashboardListItemProps, {}> {
getLink = () => { getLink = () => {
const { dashboard, logging } = this.props; const { dashboard, logging } = this.props;
return `/dashboard?uri=${dashboard.uri}&index=${logging.index}&source=${logging.source}`;
return `${buildDashboardURL(dashboard.uri)}?index=${logging.index}&source=${logging.source}`;
}; };
generateResourceIconClass = (dashboardId: string, dashboardType: ResourceType): string => { generateResourceIconClass = (dashboardId: string, dashboardType: ResourceType): string => {
......
...@@ -4,7 +4,7 @@ import { shallow } from 'enzyme'; ...@@ -4,7 +4,7 @@ import { shallow } from 'enzyme';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon'; import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
import TableListItem, { TableListItemProps } from '../'; import TableListItem, { TableListItemProps } from '.';
import { ResourceType, Badge, TagType } from 'interfaces'; import { ResourceType, Badge, TagType } from 'interfaces';
import * as ConfigUtils from 'config/config-utils'; import * as ConfigUtils from 'config/config-utils';
......
...@@ -6,7 +6,7 @@ import * as Avatar from 'react-avatar'; ...@@ -6,7 +6,7 @@ import * as Avatar from 'react-avatar';
import Flag from 'components/common/Flag'; import Flag from 'components/common/Flag';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import UserListItem, { UserListItemProps } from '../'; import UserListItem, { UserListItemProps } from '.';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
describe('UserListItem', () => { describe('UserListItem', () => {
......
...@@ -2,10 +2,10 @@ import * as React from 'react'; ...@@ -2,10 +2,10 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import DashboardListItem from '../DashboardListItem'; import DashboardListItem from './DashboardListItem';
import TableListItem from '../TableListItem'; import TableListItem from './TableListItem';
import UserListItem from '../UserListItem'; import UserListItem from './UserListItem';
import ResourceListItem, { ListItemProps } from '../'; import ResourceListItem, { ListItemProps } from '.';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
describe('ResourceListItem', () => { describe('ResourceListItem', () => {
......
...@@ -5,6 +5,7 @@ import SearchItemList from './SearchItemList'; ...@@ -5,6 +5,7 @@ import SearchItemList from './SearchItemList';
import ResultItemList from './ResultItemList'; import ResultItemList from './ResultItemList';
import { getSourceDisplayName, getSourceIconClass, indexDashboardsEnabled, indexUsersEnabled } from 'config/config-utils'; import { getSourceDisplayName, getSourceIconClass, indexDashboardsEnabled, indexUsersEnabled } from 'config/config-utils';
import { buildDashboardURL } from 'utils/navigationUtils';
import { GlobalState } from 'ducks/rootReducer' import { GlobalState } from 'ducks/rootReducer'
import { DashboardSearchResults, TableSearchResults, UserSearchResults } from 'ducks/search/types'; import { DashboardSearchResults, TableSearchResults, UserSearchResults } from 'ducks/search/types';
...@@ -93,15 +94,19 @@ export class InlineSearchResults extends React.Component<InlineSearchResultsProp ...@@ -93,15 +94,19 @@ export class InlineSearchResults extends React.Component<InlineSearchResultsProp
getSuggestedResultHref = (resourceType: ResourceType, result: Resource, index: number): string => { getSuggestedResultHref = (resourceType: ResourceType, result: Resource, index: number): string => {
const logParams = `source=inline_search&index=${index}`; const logParams = `source=inline_search&index=${index}`;
switch (resourceType) { switch (resourceType) {
case ResourceType.dashboard: case ResourceType.dashboard:
const dashboard = result as DashboardResource; const dashboard = result as DashboardResource;
return `/dashboard?uri=${dashboard.uri}&${logParams}`;
return `${buildDashboardURL(dashboard.uri)}?${logParams}`;
case ResourceType.table: case ResourceType.table:
const table = result as TableResource; const table = result as TableResource;
return `/table_detail/${table.cluster}/${table.database}/${table.schema}/${table.name}?${logParams}`; return `/table_detail/${table.cluster}/${table.database}/${table.schema}/${table.name}?${logParams}`;
case ResourceType.user: case ResourceType.user:
const user = result as UserResource; const user = result as UserResource;
return `/user/${user.user_id}?${logParams}`; return `/user/${user.user_id}?${logParams}`;
default: default:
return ''; return '';
......
...@@ -187,8 +187,10 @@ describe('InlineSearchResults', () => { ...@@ -187,8 +187,10 @@ describe('InlineSearchResults', () => {
it('returns the correct href for ResourceType.dashboard', () => { it('returns the correct href for ResourceType.dashboard', () => {
const index = 0; const index = 0;
const givenDashboard = props.dashboards.results[index]; const givenDashboard = props.dashboards.results[index];
const expected = "/dashboard/product_dashboard%3A%2F%2Fcluster.group%2Fname?source=inline_search&index=0";
const output = wrapper.instance().getSuggestedResultHref(ResourceType.dashboard, givenDashboard, index); const output = wrapper.instance().getSuggestedResultHref(ResourceType.dashboard, givenDashboard, index);
expect(output).toEqual(`/dashboard?uri=${givenDashboard.uri}&source=inline_search&index=${index}`);
expect(output).toEqual(expected);
}); });
it('returns the correct href for ResourceType.table', () => { it('returns the correct href for ResourceType.table', () => {
const index = 0; const index = 0;
......
...@@ -3,7 +3,7 @@ import * as History from 'history'; ...@@ -3,7 +3,7 @@ import * as History from 'history';
import { mount, shallow } from 'enzyme'; import { mount, shallow } from 'enzyme';
import { mapStateToProps, mapDispatchToProps, SearchBar, SearchBarProps } from '../'; import { mapStateToProps, mapDispatchToProps, SearchBar, SearchBarProps } from '.';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { getMockRouterProps } from 'fixtures/mockRouter'; import { getMockRouterProps } from 'fixtures/mockRouter';
......
...@@ -3,7 +3,7 @@ import * as React from 'react'; ...@@ -3,7 +3,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Tab, Tabs } from 'react-bootstrap'; import { Tab, Tabs } from 'react-bootstrap';
import TabsComponent, { TabsProps } from '../'; import TabsComponent, { TabsProps } from '.';
describe('Tabs', () => { describe('Tabs', () => {
let props: TabsProps; let props: TabsProps;
......
...@@ -3,7 +3,7 @@ import * as React from 'react'; ...@@ -3,7 +3,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import LoadingSpinner from 'components/common/LoadingSpinner'; import LoadingSpinner from 'components/common/LoadingSpinner';
import { TagsList, TagsListProps, mapDispatchToProps, mapStateToProps } from '../'; import { TagsList, TagsListProps, mapDispatchToProps, mapStateToProps } from '.';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
......
...@@ -7,7 +7,7 @@ export const dashboardSummary: DashboardResource = { ...@@ -7,7 +7,7 @@ export const dashboardSummary: DashboardResource = {
product: 'mode', product: 'mode',
type: ResourceType.dashboard, type: ResourceType.dashboard,
description: 'I am a dashboard', description: 'I am a dashboard',
uri: 'product_dashboard://cluster.group/name', uri: 'mode_dashboard://cluster.group/name',
url: 'product/name', url: 'product/name',
cluster: 'cluster', cluster: 'cluster',
last_successful_run_timestamp: 1585062593 last_successful_run_timestamp: 1585062593
......
...@@ -43,7 +43,7 @@ ReactDOM.render( ...@@ -43,7 +43,7 @@ ReactDOM.render(
<Switch> <Switch>
<Route path="/announcements" component={AnnouncementPage} /> <Route path="/announcements" component={AnnouncementPage} />
<Route path="/browse" component={BrowsePage} /> <Route path="/browse" component={BrowsePage} />
<Route path="/dashboard" component={DashboardPage} /> <Route path="/dashboard/:uri" component={DashboardPage} />
<Route path="/search" component={SearchPage} /> <Route path="/search" component={SearchPage} />
<Route path="/table_detail/:cluster/:database/:schema/:table" component={TableDetail} /> <Route path="/table_detail/:cluster/:database/:schema/:table" component={TableDetail} />
<Route path="/user/:userId" component={ProfilePage} /> <Route path="/user/:userId" component={ProfilePage} />
......
import * as DateUtils from 'utils/dateUtils';
import * as LogUtils from 'utils/logUtils';
import * as NavigationUtils from 'utils/navigationUtils';
import * as qs from 'simple-query-string'; import * as qs from 'simple-query-string';
import * as DateUtils from './dateUtils';
import * as LogUtils from './logUtils';
import * as NavigationUtils from './navigationUtils';
import { ResourceType } from 'interfaces/Resources'; import { ResourceType } from 'interfaces/Resources';
...@@ -122,6 +124,16 @@ describe('navigationUtils', () => { ...@@ -122,6 +124,16 @@ describe('navigationUtils', () => {
expect(url).toEqual(expectedUrl); expect(url).toEqual(expectedUrl);
}); });
}); });
describe('buildDashboardURL', () => {
it('encodes the passed URI for safe use on the URL bar', () => {
const testURI = 'product_dashboard://cluster.groupID/dashboardID';
const expected = '/dashboard/product_dashboard%3A%2F%2Fcluster.groupID%2FdashboardID';
const actual = NavigationUtils.buildDashboardURL(testURI);
expect(actual).toEqual(expected);
});
});
}); });
...@@ -206,4 +218,4 @@ describe('logUtils', () => { ...@@ -206,4 +218,4 @@ describe('logUtils', () => {
expect(replaceStateSpy).not.toHaveBeenCalled() expect(replaceStateSpy).not.toHaveBeenCalled()
}); });
}); });
}); });
\ No newline at end of file
...@@ -47,3 +47,12 @@ export const updateSearchUrl = (searchParams: SearchParams, replace: boolean = f ...@@ -47,3 +47,12 @@ export const updateSearchUrl = (searchParams: SearchParams, replace: boolean = f
BrowserHistory.push(newUrl); BrowserHistory.push(newUrl);
} }
}; };
/**
* Creates the dashboard detail URL from the URI
* @param URI String URI of the dashboard, it has this shape: uri = "<product>_dashboard://<cluster>.<groupID>/<dashboardID>"
* @return String Dashboard Detail page URL
*/
export const buildDashboardURL = (URI: string) => {
return `/dashboard/${encodeURIComponent(URI)}`;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment