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