Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/api/projectReports.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,13 @@ export function getProjectSummary(projectId) {
export function getProjectReportUrl(projectId, reportName) {
return axios.get(`${PROJECTS_API_URL}/v5/projects/${projectId}/reports/embed?reportName=${reportName}`)
.then(resp => resp.data)
}

/**
* Gets signed URL for embeding the requested report.
* @param {*} reportName unique name of the report
*/
export function getUserReportUrl(reportName) {
return axios.get(`${PROJECTS_API_URL}/v5/projects/reports/embed?reportName=${reportName}`)
.then(resp => resp.data)
}
7 changes: 7 additions & 0 deletions src/components/UserSidebar/UserSidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import MenuList from '../MenuList/MenuList'
import NotificationsIcon from '../../assets/icons/ui-bell.svg'
import AllProjectsIcon from '../../assets/icons/v.2.5/icon-all-projects.svg'
import MyProfileIcon from '../../assets/icons/v.2.5/icon-my-profile.svg'
import ReportsIcon from '../../assets/icons/v.2.5/icon-reports.svg'
import NotificationSettingsIcon from '../../assets/icons/v.2.5/icon-notification-setting.svg'
import AccountSecurityIcon from '../../assets/icons/v.2.5/icon-account-security.svg'

Expand All @@ -20,6 +21,12 @@ const navLinks = [{
Icon: AllProjectsIcon,
iconClassName: 'fill',
exact: false,
}, {
label: 'REPORTS',
to: '/reports',
Icon: ReportsIcon,
iconClassName: 'stroke',
exact: false,
}, {
label: 'MY PROFILE',
to: '/settings/profile',
Expand Down
6 changes: 6 additions & 0 deletions src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,12 @@ export const LOAD_PROJECT_SUMMARY_SUCCESS = 'LOAD_PROJECT_SUMMARY_SUCCESS'
export const LOAD_PROJECT_SUMMARY_FAILURE = 'LOAD_PROJECT_SUMMARY_FAILURE'
export const SET_LOOKER_SESSION_EXPIRED = 'SET_LOOKER_SESSION_EXPIRED'

// User Reports
export const LOAD_USER_REPORTS = 'LOAD_USER_REPORTS'
export const LOAD_USER_REPORTS_PENDING = 'LOAD_USER_REPORTS_PENDING'
export const LOAD_USER_REPORTS_SUCCESS = 'LOAD_USER_REPORTS_SUCCESS'
export const LOAD_USER_REPORTS_FAILURE = 'LOAD_USER_REPORTS_FAILURE'

// Product attachments
export const ADD_PRODUCT_ATTACHMENT = 'ADD_PRODUCT_ATTACHMENT'
export const ADD_PRODUCT_ATTACHMENT_PENDING = 'ADD_PRODUCT_ATTACHMENT_PENDING'
Expand Down
2 changes: 2 additions & 0 deletions src/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { projectTopics } from '../projects/reducers/projectTopics'
import { topics } from './topics'
import { productsTimelines } from '../projects/reducers/productsTimelines'
import { projectReports } from '../projects/reducers/projectReports'
import { userReports } from '../routes/reports/reducers'
import navSearch from './navSearch'
import projectSearch from '../projects/reducers/projectSearch'
import projectSearchSuggestions from '../projects/reducers/projectSearchSuggestions'
Expand All @@ -33,4 +34,5 @@ export default combineReducers({
settings,
templates,
productsTimelines,
userReports,
})
3 changes: 2 additions & 1 deletion src/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import projectRoutes from './projects/routes.jsx'
import notificationsRoutes from './routes/notifications/routes.jsx'
import settingsRoutes from './routes/settings/routes.jsx'
import metaDataRoutes from './routes/metadata/routes.jsx'
import reportsListRoutes from './routes/reports/routes'
import TopBarContainer from './components/TopBar/TopBarContainer'
import ProjectsToolBar from './components/TopBar/ProjectsToolBar'
import RedirectComponent from './components/RedirectComponent'
Expand Down Expand Up @@ -154,7 +155,7 @@ class Routes extends React.Component {

{/* Handle /projects/* routes */}
{projectRoutes}
{/* {reportsListRoutes} */}
{reportsListRoutes}
{notificationsRoutes}
{settingsRoutes}
{metaDataRoutes}
Expand Down
34 changes: 34 additions & 0 deletions src/routes/reports/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
LOAD_USER_REPORTS,
SET_LOOKER_SESSION_EXPIRED,
} from '../../../config/constants'
import {
getUserReportUrl,
} from '../../../api/projectReports'

/**
* Redux action to start fetching the signed URL for embeding the given report
* @param {*} reportName unique name of the report
*/
export function loadUserReportsUrls(reportName) {
return (dispatch) => {
return dispatch({
type: LOAD_USER_REPORTS,
payload: getUserReportUrl(reportName),
})
}
}

/**
* Redux action set the flag `lookerSessionExpired`
*
* @param {Boolean} isExpired true to indicate that looker session is expired
*/
export function setLookerSessionExpired(isExpired) {
return (dispatch) => {
return dispatch({
type: SET_LOOKER_SESSION_EXPIRED,
payload: { lookerSessionExpired: isExpired }
})
}
}
125 changes: 125 additions & 0 deletions src/routes/reports/containers/UserReportsContainer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, { Component } from 'react'
import MediaQuery from 'react-responsive'
import Modal from 'react-modal'
import Sticky from 'react-stickynode'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { loadUserReportsUrls, setLookerSessionExpired } from '../actions'
import { SCREEN_BREAKPOINT_MD, PROJECT_REPORTS, REPORT_SESSION_LENGTH } from '../../../config/constants'
import TwoColsLayout from '../../../components/TwoColsLayout'
import UserSidebar from '../../../components/UserSidebar/UserSidebar'
import spinnerWhileLoading from '../../../components/LoadingSpinner'

const LookerEmbedReport = (props) => {
return (<iframe width="100%" src={props.userReportsEmbedUrl} onLoad={props.onLoad} />)
}

const EnhancedLookerEmbedReport = spinnerWhileLoading(props => {
return !props.isLoading
})(LookerEmbedReport)

class UserReportsContainer extends Component {
constructor(props) {
super(props)

this.timer = null
this.setLookerSessionTimer = this.setLookerSessionTimer.bind(this)
this.reloadProjectReport = this.reloadProjectReport.bind(this)
}

reloadProjectReport() {
this.props.loadUserReportsUrls(PROJECT_REPORTS.PROJECT_SUMMARY)
// don't have to set session expire timer here, it would be set of iframe load
}

componentWillMount() {
this.reloadProjectReport()
// don't have to set session expire timer here, it would be set of iframe load
}

componentWillUnmount() {
if (this.timer) {
clearTimeout(this.timer)
}
}

setLookerSessionTimer() {
console.log('Setting Looker Session Timer')

if (this.timer) {
clearTimeout(this.timer)
}

// set timeout for raising alert to refresh the token when session expires
this.timer = setTimeout(() => {
console.log('Looker Session is expired by timer')
this.props.setLookerSessionExpired(true)
window.analytics && window.analytics.track('Looker Session Expired')
}, REPORT_SESSION_LENGTH * 1000)
}

render() {
const { user, isLoading, userReportsEmbedUrl, lookerSessionExpired } = this.props

return (
<TwoColsLayout noPadding>
<TwoColsLayout.Sidebar>
<MediaQuery minWidth={SCREEN_BREAKPOINT_MD}>
{(matches) => {
if (matches) {
return (
<Sticky top={60} bottomBoundary="#wrapper-main">
<UserSidebar user={user}/>
</Sticky>
)
} else {
return <UserSidebar user={user}/>
}
}}
</MediaQuery>
</TwoColsLayout.Sidebar>
<TwoColsLayout.Content>
<Modal
isOpen={lookerSessionExpired && !isLoading}
className="delete-post-dialog"
overlayClassName="delete-post-dialog-overlay"
contentLabel=""
>
<div className="modal-title">
Report sessions expired
</div>

<div className="modal-body">
To keep the data up to date, please, hit "Refresh" button to reload the report.
</div>

<div className="button-area flex center action-area">
<button className="tc-btn tc-btn-primary tc-btn-sm" onClick={this.reloadProjectReport}>Refresh</button>
</div>
</Modal>
<EnhancedLookerEmbedReport
isLoading={isLoading}
userReportsEmbedUrl={userReportsEmbedUrl}
onLoad={this.setLookerSessionTimer}
/>
</TwoColsLayout.Content>
</TwoColsLayout>
)
}
}
const mapStateToProps = ({ loadUser, userReports }) => {

return {
user: loadUser.user,
isLoading: userReports.isLoading,
lookerSessionExpired: userReports.lookerSessionExpired,
userReportsEmbedUrl: userReports.userReportsEmbedUrl,
}
}

const mapDispatchToProps = {
loadUserReportsUrls,
setLookerSessionExpired,
}

export default connect(mapStateToProps, mapDispatchToProps)(withRouter(UserReportsContainer))
51 changes: 51 additions & 0 deletions src/routes/reports/reducers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
LOAD_USER_REPORTS_PENDING,
LOAD_USER_REPORTS_SUCCESS,
LOAD_USER_REPORTS_FAILURE,
SET_LOOKER_SESSION_EXPIRED,
} from '../../../config/constants'

const initialState = {
isLoading: false,
error: false,
userReports: null,
userReportsEmbedUrl: null,
lookerSessionExpired: false,
}

export const userReports = function (state=initialState, action) {
const payload = action.payload

switch (action.type) {
case LOAD_USER_REPORTS_PENDING:
return Object.assign({}, state, {
isLoading: true,
error: false,
})

case LOAD_USER_REPORTS_SUCCESS:
return Object.assign({}, state, {
isLoading: false,
error: false,
userReportsEmbedUrl: payload,
lookerSessionExpired: false,
})

case LOAD_USER_REPORTS_FAILURE: {
return Object.assign({}, state, {
isLoading: false,
error: payload
})
}

case SET_LOOKER_SESSION_EXPIRED: {
return Object.assign({}, state, {
lookerSessionExpired: payload
})
}

default:
return state
}
}

15 changes: 15 additions & 0 deletions src/routes/reports/routes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Settings routes
*/
import React from 'react'
import { Route } from 'react-router-dom'
import { renderApp } from '../../components/App/App'
import TopBarContainer from '../../components/TopBar/TopBarContainer'
import ProjectsToolBar from '../../components/TopBar/ProjectsToolBar'
import { requiresAuthentication } from '../../components/AuthenticatedComponent'
import UserReportsContainer from './containers/UserReportsContainer'
const UserReportsContainerWithAuth = requiresAuthentication(UserReportsContainer)
export default [
<Route key="reports" exact path="/reports" render={renderApp(<TopBarContainer toolbar={ProjectsToolBar} />, <UserReportsContainerWithAuth />)} />,

]