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
......@@ -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