diff --git a/console/src/Endpoints.ts b/console/src/Endpoints.ts index bcce3d404df..b38b23a49be 100644 --- a/console/src/Endpoints.ts +++ b/console/src/Endpoints.ts @@ -20,8 +20,9 @@ const Endpoints = { hasuractlMetadata: `${hasuractlUrl}/apis/metadata`, hasuractlMigrateSettings: `${hasuractlUrl}/apis/migrate/settings`, telemetryServer: 'wss://telemetry.hasura.io/v1/ws', - consoleNotificationsStg: 'https://data.hasura-stg.hasura-app.io/v1/query', - consoleNotificationsProd: 'https://data.hasura.io/v1/query', + consoleNotificationsStg: + 'https://notifications.hasura-stg.hasura-app.io/v1/graphql', + consoleNotificationsProd: 'https://notifications.hasura.io/v1/graphql', }; const globalCookiePolicy = 'same-origin'; diff --git a/console/src/components/Common/utils/v1QueryUtils.ts b/console/src/components/Common/utils/v1QueryUtils.ts index ceb99750bf9..11044dca96d 100644 --- a/console/src/components/Common/utils/v1QueryUtils.ts +++ b/console/src/components/Common/utils/v1QueryUtils.ts @@ -795,47 +795,34 @@ export const getConsoleNotificationQuery = ( time: Date | string | number, userType?: Nullable ) => { - let consoleUserScope = { - $ilike: `%${userType}%`, - }; + let consoleUserScopeVar = `%${userType}%`; if (!userType) { - consoleUserScope = { - $ilike: '%OSS%', - }; + consoleUserScopeVar = '%OSS%'; } - return { - args: { - table: 'console_notification', - columns: ['*'], - where: { - $or: [ - { - expiry_date: { - $gte: time, - }, - }, - { - expiry_date: { - $eq: null, - }, - }, - ], - scope: consoleUserScope, - start_date: { $lte: time }, - }, - order_by: [ - { - type: 'asc', - nulls: 'last', - column: 'priority', - }, - { - type: 'desc', - column: 'start_date', - }, - ], - }, - type: 'select', + const query = `query fetchNotifications($currentTime: timestamptz, $userScope: String) { + console_notifications( + where: {start_date: {_lte: $currentTime}, scope: {_ilike: $userScope}, _or: [{expiry_date: {_gte: $currentTime}}, {expiry_date: {_eq: null}}]}, + order_by: {priority: asc_nulls_last, start_date: desc} + ) { + content + created_at + external_link + expiry_date + id + is_active + priority + scope + start_date + subject + type + } + }`; + + const variables = { + userScope: consoleUserScopeVar, + currentTime: time, }; + + return { query, variables }; }; diff --git a/console/src/components/Main/Actions.js b/console/src/components/Main/Actions.js index bffb7f1522b..775b86fb969 100644 --- a/console/src/components/Main/Actions.js +++ b/console/src/components/Main/Actions.js @@ -79,8 +79,12 @@ const fetchConsoleNotifications = () => (dispatch, getState) => { const consoleId = window.__env.consoleId; const consoleScope = getConsoleScope(serverVersion, consoleId); let userType = 'admin'; - if (headers.hasOwnProperty(HASURA_COLLABORATOR_TOKEN)) { - const collabToken = headers[HASURA_COLLABORATOR_TOKEN]; + const headerHasCollabToken = Object.keys(headers).find( + header => header.toLowerCase() === HASURA_COLLABORATOR_TOKEN + ); + + if (headerHasCollabToken) { + const collabToken = headers[headerHasCollabToken]; userType = getUserType(collabToken); } @@ -94,12 +98,14 @@ const fetchConsoleNotifications = () => (dispatch, getState) => { } const now = new Date().toISOString(); - const query = getConsoleNotificationQuery(now, consoleScope); + const payload = getConsoleNotificationQuery(now, consoleScope); const options = { - body: JSON.stringify(query), + body: JSON.stringify(payload), method: 'POST', headers: { 'content-type': 'application/json', + // temp. change until Auth is added + 'x-hasura-role': 'user', }, }; @@ -108,94 +114,131 @@ const fetchConsoleNotifications = () => (dispatch, getState) => { const lastSeenNotifications = JSON.parse( window.localStorage.getItem('notifications:lastSeen') ); - if (!data.length) { - dispatch({ type: FETCH_CONSOLE_NOTIFICATIONS_SET_DEFAULT }); - dispatch( - updateConsoleNotificationsState({ - read: 'default', - date: now, - showBadge: false, - }) - ); - if (!lastSeenNotifications) { + if (data.data.console_notifications) { + const fetchedData = data.data.console_notifications; + + if (!fetchedData.length) { + dispatch({ type: FETCH_CONSOLE_NOTIFICATIONS_SET_DEFAULT }); + dispatch( + updateConsoleNotificationsState({ + read: 'default', + date: now, + showBadge: false, + }) + ); + if (!lastSeenNotifications) { + window.localStorage.setItem( + 'notifications:lastSeen', + JSON.stringify(0) + ); + } + return; + } + + // NOTE: these 2 steps may not be required if the table in the DB + // enforces the usage of `enums` and we're sure that the notification scope + // is only from the allowed permutations of scope. We aren't doing that yet + // because within the GQL query, I can't be using the `_ilike` operator during + // filtering. Hence I'm keeping it here since this is a new feature and + // mistakes can happen while adding data into the DB. + + // TODO: is to remove these once things are more streamlined + const uppercaseScopedData = makeUppercaseScopes(fetchedData); + let filteredData = filterScope(uppercaseScopedData, consoleScope); + + if ( + lastSeenNotifications && + lastSeenNotifications > filteredData.length + ) { window.localStorage.setItem( 'notifications:lastSeen', - JSON.stringify(0) + JSON.stringify(filteredData.length) + ); + } + + if (previousRead) { + if (!consoleStateDB.console_notifications) { + dispatch( + updateConsoleNotificationsState({ + read: [], + date: now, + showBadge: true, + }) + ); + } else { + let newReadValue; + if (previousRead === 'default' || previousRead === 'error') { + newReadValue = []; + toShowBadge = false; + } else if (previousRead === 'all') { + const previousList = JSON.parse( + localStorage.getItem('notifications:data') + ); + if (!previousList) { + // we don't have a record of the IDs that were marked as read previously + newReadValue = []; + toShowBadge = true; + } else if (previousList.length) { + const readNotificationsDiff = filteredData.filter( + newNotif => + !previousList.find(oldNotif => oldNotif.id === newNotif.id) + ); + if (!readNotificationsDiff.length) { + // since the data hasn't changed since the last call + newReadValue = previousRead; + toShowBadge = false; + } else { + newReadValue = [...previousList.map(notif => `${notif.id}`)]; + toShowBadge = true; + filteredData = [...readNotificationsDiff, ...previousList]; + } + } + } else { + newReadValue = previousRead; + if ( + previousRead.length && + lastSeenNotifications >= filteredData.length + ) { + toShowBadge = false; + } else if (lastSeenNotifications < filteredData.length) { + toShowBadge = true; + } + } + dispatch( + updateConsoleNotificationsState({ + read: newReadValue, + date: consoleStateDB.console_notifications[userType].date, + showBadge: toShowBadge, + }) + ); + } + } + + dispatch({ + type: FETCH_CONSOLE_NOTIFICATIONS_SUCCESS, + data: filteredData, + }); + + // update/set the lastSeen value upon data is set + if ( + !lastSeenNotifications || + lastSeenNotifications !== filteredData.length + ) { + window.localStorage.setItem( + 'notifications:lastSeen', + JSON.stringify(filteredData.length) ); } return; } - - const uppercaseScopedData = makeUppercaseScopes(data); - let filteredData = filterScope(uppercaseScopedData, consoleScope); - - if ( - !lastSeenNotifications || - lastSeenNotifications !== filteredData.length - ) { - window.localStorage.setItem( - 'notifications:lastSeen', - JSON.stringify(filteredData.length) - ); - } - - if (previousRead) { - if (!consoleStateDB.console_notifications) { - dispatch( - updateConsoleNotificationsState({ - read: [], - date: now, - showBadge: true, - }) - ); - } else { - let newReadValue; - - if (previousRead === 'default' || previousRead === 'error') { - newReadValue = []; - toShowBadge = false; - } else if (previousRead === 'all') { - const previousList = JSON.parse( - localStorage.getItem('notifications:data') - ); - if (previousList.length) { - const resDiff = filteredData.filter( - newNotif => - !previousList.find(oldNotif => oldNotif.id === newNotif.id) - ); - if (!resDiff.length) { - // since the data hasn't changed since the last call - newReadValue = previousRead; - toShowBadge = false; - } else { - newReadValue = [...previousList.map(notif => `${notif.id}`)]; - toShowBadge = true; - filteredData = [...resDiff, ...previousList]; - } - } - } else { - newReadValue = previousRead; - if ( - previousRead.length && - lastSeenNotifications === filteredData.length - ) { - toShowBadge = false; - } - } - dispatch( - updateConsoleNotificationsState({ - read: newReadValue, - date: consoleStateDB.console_notifications[userType].date, - showBadge: toShowBadge, - }) - ); - } - } - - dispatch({ - type: FETCH_CONSOLE_NOTIFICATIONS_SUCCESS, - data: filteredData, - }); + dispatch({ type: FETCH_CONSOLE_NOTIFICATIONS_ERROR }); + dispatch( + updateConsoleNotificationsState({ + read: 'error', + date: now, + showBadge: false, + }) + ); }) .catch(err => { console.error(err); diff --git a/console/src/components/Main/ConsoleNotification.ts b/console/src/components/Main/ConsoleNotification.ts index 26b0c851c05..ee2341eb0e0 100644 --- a/console/src/components/Main/ConsoleNotification.ts +++ b/console/src/components/Main/ConsoleNotification.ts @@ -31,7 +31,6 @@ export type ConsoleNotification = { scope?: NotificationScope; }; -// FIXME? : we may have to remove this export const defaultNotification: ConsoleNotification = { subject: 'No updates available at the moment', created_at: Date.now(), diff --git a/console/src/components/Main/Main.js b/console/src/components/Main/Main.js index 9eb1d8269ab..40533658f2c 100644 --- a/console/src/components/Main/Main.js +++ b/console/src/components/Main/Main.js @@ -35,10 +35,40 @@ import { setProClickState, getLoveConsentState, setLoveConsentState, + getUserType, } from './utils'; import { getSchemaBaseRoute } from '../Common/utils/routesUtils'; import LoveSection from './LoveSection'; import { Help, ProPopup } from './components/'; +import { HASURA_COLLABORATOR_TOKEN } from '../../constants'; +import { UPDATE_CONSOLE_NOTIFICATIONS } from '../../telemetry/Actions'; + +const updateRequestHeaders = props => { + const { requestHeaders, dispatch } = props; + + const collabTokenKey = Object.keys(requestHeaders).find( + hdr => hdr.toLowerCase() === HASURA_COLLABORATOR_TOKEN + ); + + if (collabTokenKey) { + const userID = getUserType(requestHeaders[collabTokenKey]); + if (props.console_opts && props.console_opts.console_notifications) { + if (!props.console_opts.console_notifications[userID]) { + dispatch({ + type: UPDATE_CONSOLE_NOTIFICATIONS, + data: { + ...props.console_opts.console_notifications, + [userID]: { + read: [], + date: null, + showBadge: true, + }, + }, + }); + } + } + } +}; class Main extends React.Component { constructor(props) { @@ -57,6 +87,7 @@ class Main extends React.Component { componentDidMount() { const { dispatch } = this.props; + updateRequestHeaders(this.props); dispatch(loadServerVersion()).then(() => { dispatch(featureCompatibilityInit()); @@ -74,6 +105,18 @@ class Main extends React.Component { dispatch(fetchServerConfig); } + componentDidUpdate(prevProps) { + const prevHeaders = Object.keys(prevProps.requestHeaders); + const currHeaders = Object.keys(this.props.requestHeaders); + + if ( + prevHeaders.length !== currHeaders.length || + prevHeaders.filter(hdr => !currHeaders.includes(hdr)).length + ) { + updateRequestHeaders(this.props); + } + } + toggleProPopup = () => { const { dispatch } = this.props; dispatch(emitProClickedEvent({ open: !this.state.isPopUpOpen })); @@ -368,6 +411,7 @@ const mapStateToProps = (state, ownProps) => { currentSchema: state.tables.currentSchema, metadata: state.metadata, console_opts: state.telemetry.console_opts, + requestHeaders: state.tables.dataHeaders, }; }; diff --git a/console/src/components/Main/Main.scss b/console/src/components/Main/Main.scss index c3a1579e6af..ca79ae41339 100644 --- a/console/src/components/Main/Main.scss +++ b/console/src/components/Main/Main.scss @@ -1367,7 +1367,7 @@ position: absolute; width: 17px; top: 16px; - right: 8px; + right: 0.8rem; border-radius: 50%; user-select: none; visibility: visible; @@ -1536,10 +1536,6 @@ .secureSectionText { display: none; } - - .shareSection { - display: none; - } } @media (max-width: 1050px) { diff --git a/console/src/components/Main/NotificationSection.tsx b/console/src/components/Main/NotificationSection.tsx index 7e1744d6c95..26122e76df9 100644 --- a/console/src/components/Main/NotificationSection.tsx +++ b/console/src/components/Main/NotificationSection.tsx @@ -511,15 +511,19 @@ const HasuraNotifications: React.FC< let userType = 'admin'; - if (dataHeaders?.[HASURA_COLLABORATOR_TOKEN]) { - const collabToken = dataHeaders[HASURA_COLLABORATOR_TOKEN]; + const headerHasCollabToken = Object.keys(dataHeaders).find( + header => header.toLowerCase() === HASURA_COLLABORATOR_TOKEN + ); + + if (headerHasCollabToken) { + const collabToken = dataHeaders[headerHasCollabToken]; userType = getUserType(collabToken); } const previouslyReadState = React.useMemo( () => console_opts?.console_notifications && - console_opts?.console_notifications[userType].read, + console_opts?.console_notifications[userType]?.read, [console_opts?.console_notifications, userType] ); const showBadge = React.useMemo( @@ -639,7 +643,7 @@ const HasuraNotifications: React.FC< useOnClickOutside([dropDownRef, wrapperRef], onClickOutside); - const onClickShareSection = () => { + const onClickNotificationButton = () => { if (showBadge) { if (console_opts?.console_notifications) { let updatedState = {}; @@ -718,11 +722,11 @@ const HasuraNotifications: React.FC< return ( <>