From 9689906d70cfe3305cff9dbdd9dbe876e5b27a01 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Fri, 20 Oct 2023 14:52:19 +0000 Subject: [PATCH 1/6] fix manual workflow [CE-314] --- .github/workflows/manual.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml index 18cbb09b..b4e06952 100644 --- a/.github/workflows/manual.yml +++ b/.github/workflows/manual.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: jobs: test: - uses: ./.github/workflows/reusable_test-crats.yml + uses: ./.github/workflows/reusable_test.yml build-push: needs: test uses: ./.github/workflows/reusable_build-push.yml From 384a8a9623be1498b0b39b8d3459f0ea66748d81 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Fri, 20 Oct 2023 14:58:18 +0000 Subject: [PATCH 2/6] a little bit of cleanup [CE-314] --- .gitignore | 1 + .../Notifications/Notifications.css | 174 ----------- .../Notifications/Notifications.tsx | 128 --------- .../Notifications/NotificationsMain.tsx | 270 ------------------ react-app-todo/Notifications/readme.md | 32 --- react-app-todo/SystemAlertBanner/data.tsx | 160 ----------- react-app-todo/SystemAlertBanner/style.css | 60 ---- react-app-todo/SystemAlertBanner/view.tsx | 230 --------------- react-app-todo/SystemAlertToggle/data.js | 201 ------------- react-app-todo/SystemAlertToggle/style.css | 12 - react-app-todo/SystemAlertToggle/view.js | 116 -------- react-app-todo/services/feeds.ts | 165 ----------- react-app-todo/session.ts | 210 -------------- 13 files changed, 1 insertion(+), 1758 deletions(-) delete mode 100644 react-app-todo/Notifications/Notifications.css delete mode 100644 react-app-todo/Notifications/Notifications.tsx delete mode 100644 react-app-todo/Notifications/NotificationsMain.tsx delete mode 100644 react-app-todo/Notifications/readme.md delete mode 100644 react-app-todo/SystemAlertBanner/data.tsx delete mode 100644 react-app-todo/SystemAlertBanner/style.css delete mode 100644 react-app-todo/SystemAlertBanner/view.tsx delete mode 100644 react-app-todo/SystemAlertToggle/data.js delete mode 100644 react-app-todo/SystemAlertToggle/style.css delete mode 100644 react-app-todo/SystemAlertToggle/view.js delete mode 100644 react-app-todo/services/feeds.ts delete mode 100644 react-app-todo/session.ts diff --git a/.gitignore b/.gitignore index 20bcc427..e52b8570 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ yarn.lock # /react-app/public/data # !/react-app/public/data/README.md _working/ +_todo/ # General purpose temp directory /temp/* diff --git a/react-app-todo/Notifications/Notifications.css b/react-app-todo/Notifications/Notifications.css deleted file mode 100644 index a87f994d..00000000 --- a/react-app-todo/Notifications/Notifications.css +++ /dev/null @@ -1,174 +0,0 @@ -.Notifications { - position: relative; - display: inline-block; - height: 100%; - vertical-align: top; - border: 1px silver solid; - font-size: 90%; - width: 80px -} - -.Notifications .-summary { - cursor: pointer; - font-size: 10px; - padding: 0; - margin: 0; -} - -.Notifications .-summary .-item { - padding: 0; - margin: 0; -} - -.Notifications .-summary .-item>.-count { - display: inline-block; - width: 30%; - text-align: right; - padding-right: 3px; - line-height: 1 -} - -.Notifications .-summary .-item>.-label { - display: inline-block; - width: 60%; -} - -.Notifications .-container { - background-color: white; - position: absolute; - top: 0; - left: 0; -} - -.Notifications .-notification-set { - padding: 3px; - position: relative; - background-color: silver; - - position: absolute; - top: 0px; - right: 20px; - z-index: 100; - width: 200px; - text-align: center -} - -.Notifications .-notification-set .-triangle { - position: absolute; - top: -3px; - left: 196px; - font-size: 25px; - color: silver; -} - -.Notifications .-notification-set .-pointer { - position: absolute; - top: 0px; - left: 200px; - height: 25px; - width: 25px; -} - -.Notifications .-notification-set .-notifications-container { - max-height: 50vh; - overflow-y: auto; - overflow-x: hidden; - text-align: left; -} - -.Notifications .-notification { - padding: 4px; - color: black; - margin: 2px; - position: relative; -} - -.Notifications .-notification .-message { - padding-right: 1em; -} - -.Notifications a.-button { - padding: 0 4px; - background-color: rgba(255, 255, 255, 0.5); - color: black; - user-select: none; -} - -.Notifications .-notification a.-close-button .fa::before { - color: rgb(143, 46, 46); -} - -.Notifications .-notification a.-close-button { - position: absolute; - right: 2px; - top: 2px; -} - -.Notifications a.-button { - text-decoration: none; - color: black; -} - -.Notifications a.-button:hover, -.Notifications a.-button:hover { - color: black; -} - -.Notifications a.-button:hover, -.Notifications a.-button:active { - background-color: rgba(255, 255, 255, 1); - color: black; -} - - -/* new */ - -.Notifications .-notification.-success { - /* bootstrap is dff0d8 */ - background-color: #dff0d8; -} - -.Notifications .-notification.-success:hover { - /* bootstrap is #c1e2b3 */ - background-color: #c1e2b3; -} - -.Notifications .-item.-success.-has-items { - background-color: #c1e2b3; -} - -.Notifications .-notification.-info { - background-color: #d9edf7; -} - -.Notifications .-notification.-info:hover { - background-color: #afd9ee; -} - -.Notifications .-item.-info.-has-items { - background-color: #afd9ee; -} - -.Notifications .-notification.-warning { - background-color: #fcf8e3; -} - -.Notifications .-notification.-warning:hover { - background-color: #f7ecb5; -} - -.Notifications .-item.-warning.-has-items { - background-color: #f7ecb5; -} - -.Notifications .-notification.-error { - background-color: #f2dede; -} - -.Notifications .-notification.-error:hover { - background-color: #e4b9b9; -} - -.Notifications .-item.-error.-has-items { - background-color: #e4b9b9; -} \ No newline at end of file diff --git a/react-app-todo/Notifications/Notifications.tsx b/react-app-todo/Notifications/Notifications.tsx deleted file mode 100644 index 373539ce..00000000 --- a/react-app-todo/Notifications/Notifications.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Component } from 'react'; -import './Notifications.css'; -// define([ -// 'preact', -// 'htm', - -// 'css!./Notifications.css' -// ], ( -// preact, -// htm -// ) => { - -export interface NotificationsProps { - closeNotifications: () => void; -} - -interface NotificationsState { - -} - - -export default class Notifications extends Component { - renderSummaryItem(name: string) { - const summary = this.props.summary[name]; - const activeClass = summary ? ' -has-items' : ''; - return
-
- ${summary} -
-
- ${name} -
-
- } - - renderSummary() { - return
- {this.renderSummaryItem('success')} - {this.renderSummaryItem('info')} - {this.renderSummaryItem('warning')} - {this.renderSummaryItem('error')} -
- } - - doCloseNotifications() { - this.props.closeNotifications(); - } - - doClearNotification(notification) { - this.props.clearNotification(notification); - } - - doToggleNotifications() { - this.props.toggleNotifications(); - } - - renderNotifications() { - return this.props.notifications.map((notification) => { - const typeClass = (() => { - switch (notification.type) { - case 'success': return '-success'; - case 'info': return '-info'; - case 'warning': return '-warning'; - case 'error': return '-error'; - } - })(); - return
- -
- ${notification.message} -
-
- }); - } - - renderNotificationDisplay() { - const activeStyle = (this.props.show ? ' -active' : ''); - return
-
-
- - - - -
- -
- ${this.renderNotifications()} -
-
-
- `; - } - - render() { - if (!this.props.notifications || this.props.notifications.length === 0) { - return; - } - return html` -
- ${this.renderSummary()} - ${this.renderNotificationDisplay()} -
- `; - } -} \ No newline at end of file diff --git a/react-app-todo/Notifications/NotificationsMain.tsx b/react-app-todo/Notifications/NotificationsMain.tsx deleted file mode 100644 index cf9851cd..00000000 --- a/react-app-todo/Notifications/NotificationsMain.tsx +++ /dev/null @@ -1,270 +0,0 @@ -import { Component } from 'react'; -import uuid from 'uuid'; - -// define([ -// 'preact', -// 'htm', -// 'uuid', -// './Notifications' -// ], ( -// preact, -// htm, -// {v4: uuidv4}, -// Notifications -// ) => { - -// const {h, Component} = preact; -// const html = htm.bind(h); - -const AUTODISMISSER_INTERVAL = 1000; - -export class Notification { - id: String; - constructor({ notification, parent }) { - const newNotification = notification; - this.id = newNotification.id || uuid.v4(); - this.message = newNotification.message; - this.description = newNotification.description; - this.autodismiss = newNotification.autodismiss; - this.icon = newNotification.icon; - this.autodismissStartedAt = newNotification.autodismiss - ? new Date().getTime() - : null; - this.type = newNotification.type; - this.parent = parent; - - // this.type.subscribeChanged((newVal, oldVal) => { - // this.parent.summary[oldVal].count(this.parent.summary[oldVal].count() - 1); - // this.parent.summary[newVal].count(this.parent.summary[newVal].count() + 1); - // }); - - // this.autodismiss.subscribe((newVal) => { - // if (newVal) { - // // TODO: this - // this.autodismissStartedAt = new Date().getTime(); - // this.parent.startAutoDismisser(); - // } - // }); - } -} - -class AutoDismisser { - constructor({ runner }) { - this.runner = runner; - } - - run() { - if (this.runner()) { - this.loop(); - } - } - - loop() { - this.autoDismisser = window.setTimeout(() => { - this.run(); - }, AUTODISMISSER_INTERVAL); - } - - startLoop(force) { - if (this.autoDismisser && !force) { - return; - } - this.run(); - } -} - -export default class NotificationsMain extends Component { - constructor(props) { - super(props); - - this.runtime = this.props.runtime; - this.sendingChannel = uuidv4(); - this.autoDismisser = new AutoDismisser({ - runner: this.autodismissRunner.bind(this), - }); - - this.state = { - notifications: [], - summary: { - info: 0, - success: 0, - warning: 0, - error: 0, - }, - show: false, - }; - } - - closeNotifications() { - this.setState({ - show: false, - }); - } - - showNotifications() { - this.setState({ - show: true, - }); - } - - toggleNotifications() { - this.setState({ - show: !this.state.show, - }); - } - - autodismissRunner() { - const toRemove = []; - const now = new Date().getTime(); - let autodismissLeft = 0; - this.state.notifications.forEach((item) => { - if (item.autodismiss) { - const elapsed = now - item.autodismissStartedAt; - if (item.autodismiss < elapsed) { - toRemove.push(item); - } else { - autodismissLeft += 1; - } - } - }); - - toRemove.forEach((item) => { - this.removeNotification(item); - }); - - // Return true to keep on truckin' - return autodismissLeft > 0; - } - - componentDidMount() { - this.runtime.send('notification', 'ready', { - channel: this.sendingChannel, - }); - - this.runtime.receive(this.sendingChannel, 'new', (message) => { - this.processMessage(message); - }); - } - - // TODO: without observables, we need to force the - // component to update when the notification is modified. - updateNotification(newMessage) { - const { summary, notifications } = this.state; - - const notification = notifications.filter((notification) => { - return notification.id === newMessage.id; - })[0]; - - if (!notification) { - // console.error('Cannot update message, not found: ' + newMessage.id, newMessage); - // return; - return false; - } - - if (notification.type !== newMessage.type) { - summary[notification.type] -= 1; - summary[newMessage.type] += 1; - } - - notification.type = newMessage.type; - notification.message = newMessage.message; - notification.autodismiss = newMessage.autodismiss; - if (newMessage.autodismiss) { - notification.autodismissStartedAt = new Date().getTime(); - } - - this.setState( - { - notifications, - summary, - }, - () => { - this.autoDismisser.run(); - } - ); - - return true; - } - - addNotification(newMessage) { - const notification = new Notification({ - notification: newMessage, - parent: this, - }); - const { summary, notifications } = this.state; - // const summaryItem = summary[notification.type]; - // if (summaryItem) { - // summaryItem.count += 1; - // } - - summary[notification.type] += 1; - - notifications.unshift(notification); - // this.notificationMap[notification.id] = notification; - this.setState({ - notifications, - summary, - }); - } - - removeNotification(notificationToRemove) { - const { summary, notifications: currentNotifications } = this.state; - const notifications = currentNotifications.filter((notification) => { - return notification.id !== notificationToRemove.id; - }); - // const summaryItem = summary[notificationToRemove.type]; - // if (summaryItem) { - // summaryItem.count -= 1; - // } - - summary[notificationToRemove.type] -= 1; - - this.setState({ - notifications, - summary, - }); - } - - clearNotification(notification) { - this.removeNotification(notification); - } - - processMessage(message) { - if (!message.type) { - console.error('Message not processed - no type', message); - return; - } - if ( - ['success', 'info', 'warning', 'error'].indexOf(message.type) === -1 - ) { - console.error('Message not processed - invalid type', message); - return; - } - - if (!this.updateNotification(message)) { - this.addNotification(message); - } - this.setState({ - show: true, - }); - } - - render() { - const props = { - notifications: this.state.notifications, - summary: this.state.summary, - show: this.state.show, - closeNotifications: this.closeNotifications.bind(this), - toggleNotifications: this.toggleNotifications.bind(this), - clearNotification: this.clearNotification.bind(this), - }; - return html` -
- <${Notifications} ...${props} /> -
- `; - } -} diff --git a/react-app-todo/Notifications/readme.md b/react-app-todo/Notifications/readme.md deleted file mode 100644 index 9e72ccc2..00000000 --- a/react-app-todo/Notifications/readme.md +++ /dev/null @@ -1,32 +0,0 @@ -Allows ui components to provide a notification to the user. - -Notifications are classified as info, warn, and error. - -Notifications are displayed as soon as the are received, and may be removed, or the container closed. - -Notification counts are displayed in the title bar until the notification is dismissed. - -Notifications are displayed in a container per type, closing the container does not remove the notification. - -Some notifications will auto-remove themselves after a period of time, because it is annoying to have to dismiss informational alerts. - -Notifications do though have a history which will reveal recently dismissed notifiations. - -Receives notification messages at ('notification', 'add', ) - -Notifications have ids. A notification received with the same id will update the notification and force it to be displayed. - -A notification looks like this: - -id: string -type: string (info, warn, error) -icon: string (a font awesome icon name -- the part of the class name following the "fa-") -title: string -description: string -dismissable: boolean -autodismiss: integer (time until auto dismiss) - -Implementation - -Notifications are implemented via a knockout viewmodel. -Subscriptions are via our mini-pub-sub system since it is included in the runtime available to all widgets via the runtime parameter (or in the global env sometimes) diff --git a/react-app-todo/SystemAlertBanner/data.tsx b/react-app-todo/SystemAlertBanner/data.tsx deleted file mode 100644 index be071091..00000000 --- a/react-app-todo/SystemAlertBanner/data.tsx +++ /dev/null @@ -1,160 +0,0 @@ -// define([ -// 'preact', -// 'htm', -// 'kb_lib/poller', -// './view', -// 'css!./style.css' -// ], ( -// preact, -// htm, -// poller, -// SystemAlertBanner -// ) => { -import { Component } from 'react'; -import Poller, { Job, Task } from '../../lib/poller'; -import SystemAlertBanner from './view'; - -// const {h, Component } = preact; -// const html = htm.bind(h); - -const POLL_INTERVAL = 10000; - -export default class LoadNotificationsJob extends Job { - callback: () => void; - constructor({ callback }: { callback: () => void }) { - super({ - description: 'Load notifications', - }); - this.callback = callback; - // this.state = { - // alerts: null, - // showAlertBanner: false, - // error: null - // }; - } - run() { - return this.callback(); - } -} - -export interface Alert {} - -export interface SystemAlertsDataProps {} - -interface SystemAlertsDataState { - showAlertBanner: boolean; - alerts: Array; - error: string | null; - hideAlerts: boolean; -} - -export class SystemAlertData extends Component< - SystemAlertsDataProps, - SystemAlertsDataState -> { - newAlertsPoller: Poller; - constructor(props: SystemAlertsDataProps) { - super(props); - this.newAlertsPoller = this.setupPolling(); - this.state = { - showAlertBanner: false, - hideAlerts: true, - alerts: [], - error: null, - }; - } - - setupPolling() { - const job = new LoadNotificationsJob({ - callback: () => { - this.loadNotifications(); - }, - }); - - const task = new Task({ - interval: POLL_INTERVAL, - runInitially: true, - }); - task.addJob(job); - - return new Poller({ task }); - } - - componentDidMount() { - this.newAlertsPoller.start(); - - // TODO: the system alert should be a ui service, probably is, - // hook it up here... - - // this.props.runtime.receive('system-alert', 'toggle-banner', () => { - // this.setState({ - // showAlertBanner: !this.state.showAlertBanner, - // }); - // }); - // this.props.runtime.receive('system-alert', 'close-banner', () => { - // this.setState({ - // showAlertBanner: true, - // }); - // }); - // this.props.runtime.receive('system-alert', 'open-banner', () => { - // this.setState({ - // showAlertBanner: false, - // }); - // }); - } - - getActiveAlerts() { - return Promise.resolve([ - { - type: 'maintenance', - title: 'Maintenance sample 1', - startAt: new Date(), - endAt: new Date(Date.now() + 1000 * 60 * 60), - read: false, - }, - { - type: 'maintenance', - title: 'Maintenance sample 2', - startAt: new Date(Date.now() + 1000 * 60 * 60), - endAt: new Date(Date.now() + 1000 * 60 * 60 * 2), - read: false, - }, - ]); - // const client = this.props.runtime.service('rpc').newClient({ - // module: 'UIService' - // }); - - // return client.callFunc('get_active_alerts', []) - // .then(([result]) => { - // return result; - // }); - } - - loadNotifications() { - return this.getActiveAlerts() - .then((data) => { - this.setState({ - alerts: data, - error: null, - }); - }) - .catch((err) => { - console.error('ERROR', err); - this.setState({ - alerts: [], - error: err.message, - }); - }); - } - - render() { - if (this.state.showAlertBanner) { - const props = { - // runtime: this.props.runtime, - alerts: this.state.alerts, - hideAlerts: this.state.hideAlerts, - }; - return ; - } - } -} diff --git a/react-app-todo/SystemAlertBanner/style.css b/react-app-todo/SystemAlertBanner/style.css deleted file mode 100644 index 281710b6..00000000 --- a/react-app-todo/SystemAlertBanner/style.css +++ /dev/null @@ -1,60 +0,0 @@ -.SystemAlert {} - -.SystemAlert .wrapper { - margin: 4px 10px 10px 10px; - box-shadow: 4px 4px 4px silver; -} - -.SystemAlert .itemsWrapper { - border: 2px #f2dede solid; -} - -.SystemAlert .title1 { - background-color: rgba(169, 68, 68, 1); - color: white; -} - -.SystemAlert .title2 { - background-color: white; - color: rgba(169, 68, 68, 1); -} - -.SystemAlert .-header { - padding: 10px 15px; - position: relative; - display: flex; - flex-direction: row -} - -.SystemAlert .-alert { - border-radius: 0; - margin-bottom: 0; - border-top: 2px solid #f2dede; -} - -.SystemAlert .-alert>.-row { - display: flex; - flex-direction: row; -} - -.SystemAlert .-alert>.-row>.-col1 { - flex: 1 1 0px; - padding: 4px; - display: flex; - flex-direction: column; - justify-content: center; -} - -.SystemAlert .-alert>.-row>.-col2 { - flex: 1 1 0px; - padding: 4px; - display: flex; - flex-direction: row; - align-items: center; -} - -.SystemAlert .-alert .-icon { - width: 1.5em; - font-size: 120%; - text-align: center; -} \ No newline at end of file diff --git a/react-app-todo/SystemAlertBanner/view.tsx b/react-app-todo/SystemAlertBanner/view.tsx deleted file mode 100644 index 21804365..00000000 --- a/react-app-todo/SystemAlertBanner/view.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import UserProfileClient from '@kbase/ui-lib/lib/comm/coreServices/UserProfile'; -import { Component } from 'react'; - -import CountdownClock from '../CountdownClock'; - -// define([ -// 'preact', -// 'htm', -// '../CountdownClock', -// 'css!./style.css' -// ], ( -// preact, -// htm, -// CountdownClock -// ) => { - -// const {h, Component } = preact; -// const html = htm.bind(h); - -function plural(amount: number, singular: string, plural: string) { - if (amount === 1) { - return singular; - } - return plural; -} - -export interface AlertInfo { - startAt: number; - endAt: number; - title: string; - message: string; - hash: string; - now: number; -} - -export class Alert { - startAt: Date; - endAt: Date | null; - title: string; - message: string; - hash: string; - now: number; - read: boolean; - showMessage: boolean; - constructor({ startAt, endAt, title, message, hash, now }: AlertInfo) { - this.startAt = new Date(startAt); - if (endAt === null || endAt === undefined) { - this.endAt = null; - } else { - this.endAt = new Date(endAt); - } - this.title = title; - this.message = message; - this.hash = hash; - this.now = now; - - this.read = false; - this.showMessage = false; - - this.countdownToStart = (() => { - if (!this.now()) { - return null; - } - return this.startAt - this.now(); - })(); - - this.countdownToEnd = (() => { - if (!this.now()) { - return null; - } - if (this.endAt === null) { - return Infinity; - } - return this.endAt - this.now(); - })(); - } - - toggleMessage() { - this.showMessage(!this.showMessage()); - } -} - -export default class SystemAlertBanner extends Component { - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() {} - - doShowHidden() { - // this.props.alerts.forEach((alert) => { - // alert.read(false); - // }); - } - - doHide() {} - - renderHiddenCount() { - if (!this.props.hiddenAlertCount) { - return; - } - return ( - - ${this.props.hiddenAlertCount})$ hidden - - - ); - } - - renderHeader() { - return ( -
-
- - System${' '} - {plural(this.props.alerts.length, 'Alert', 'Alerts')} - -
-
- {this.props.alerts.length}{' '} - {plural(this.props.alerts.length, 'alert', 'alerts')}{' '} - {this.renderHiddenCount()} - -
-
- ); - } - - renderAlertIcon(alert) { - const now = Date.now(); - const startsIn = alert.startAt - now; - const endsIn = alert.endsAt === null ? Infinity : alert.endsAt - now; - const iconClass = (() => { - if (startsIn > 0) { - return 'fa-clock-o'; - } else if (startsIn <= 0 && endsIn > 0) { - return 'fa-exclamation-triangle'; - } - })(); - const iconColor = (() => { - if (startsIn > 0) { - return 'fa-color-warning'; - } else if (startsIn <= 0 && endsIn > 0) { - return 'fa-color-danger'; - } - })(); - return ( - - - - ); - } - - renderAlert(alert) { - return ( -
-
-
-
- {alert.title || '** untitled **'} -
-
-
- {this.renderAlertIcon(alert)} -
- -
-
-
-
- ); - } - - renderAlerts() { - if ( - !this.props.alerts.some((alert) => { - return !alert.read; - }) - ) { - return; - } - const alerts = this.props.alerts.map((alert) => { - return this.renderAlert(alert); - }); - return
{alerts}
; - } - - renderAlertsPanel() { - if (this.props.alerts && this.props.alerts.length > 0) { - return ( -
- {this.renderHeader()} - {this.renderAlerts()} -
- ); - } - } - - render() { - return ( -
- {this.renderAlertsPanel()} -
- ); - } -} diff --git a/react-app-todo/SystemAlertToggle/data.js b/react-app-todo/SystemAlertToggle/data.js deleted file mode 100644 index ac3c8b6b..00000000 --- a/react-app-todo/SystemAlertToggle/data.js +++ /dev/null @@ -1,201 +0,0 @@ -define([ - 'bluebird', - 'knockout', - 'uuid', - '../../lib/searchApi', - 'yaml!../../data/stopWords.yml' -], function ( - Promise, - ko, - Uuid, - SearchAPI, - stopWordsDb -) { - 'use strict'; - - function isStopWord(word) { - if (stopWordsDb.warn.indexOf(word) >= 0) { - return true; - } - if (stopWordsDb.ignore.indexOf(word) >= 0) { - return true; - } - return false; - } - - // TODO: configure this somewhere - function isBlacklistedHighlightField(fieldName) { - return ['tags'].includes(fieldName); - } - - function encodeHTML(possibleHTML) { - const node = document.createElement('div'); - node.innerHTML = possibleHTML; - return node.innerText; - } - - - // For now, this fakes the search... - function factory(params) { - const maxSearchResults = params.maxSearchItems; - - const types = params.types; - - const searchConfig = { - // max number of search result items to hold in the buffer - // before we start removing those out of view - maxBufferSize: params.maxBufferSize || 100, - // number of search items to fetch at one time. - fetchSize: params.pageSize || 20 - }; - - function objectToViewModel(obj) { - const type = types.getTypeForObject(obj); - if (!type) { - console.error('ERROR cannot type object', obj); - throw new Error('Cannot type this object'); - } - - const icon = type.getIcon(type); - - const ref = type.getRef(); - const detail = type.detail(); - const detailMap = detail.reduce(function (m, field) { - m[field.id] = field; - return m; - }, {}); - - const matches = Object.keys(obj.highlight).reduce((matches, field) => { - if (isBlacklistedHighlightField(field)) { - console.warn('highlight field ' + field + ' ignored'); - return matches; - } - - let label = type.getSearchFieldLabel(field); - if (!label) { - label = field; - console.warn('highlight field ' + field + ' not found in type spec', obj); - } - - const emStart = new Uuid(4).format(); - const emFinish = new Uuid(4).format(); - - matches - .push({ - id: field, - label: label, - highlights: obj.highlight[field] - .map((highlight) => { - const safe1 = highlight.replace('', emStart).replace('', emFinish); - const safe2 = encodeHTML(safe1); - const safe3 = safe2.replace(emStart, '').replace(emFinish, ''); - return { - highlight: safe3 - }; - }) - }); - return matches; - }, []); - - // Uncomment to re-enable highlights merging into details - // detail.forEach(function (field) { - // if (matchMap[field.id]) { - // field.highlights = matchMap[field.id].highlights; - // } - // }); - - const vm = { - type: { - id: obj.type, - label: type.getLabel(), - icon: icon - }, - // TODO: I don't remember why I named this "matchClass", but it confuses me now. - matchClass: { - id: type.getUIClass(), - copyable: type.isCopyable(), - viewable: type.isViewable(), - ref - }, - - // Detail, type-specific - detail: detail, - - url: window.location.origin + '#dataview/' + ref.workspaceId + '/' + ref.objectId + '/' + ref.version, - - // should be different per object type? E.g. narrative - nice name, others object name?? - // Generic fields - name: obj.object_name, - date: new Date(obj.modified_at), - scientificName: detailMap.scientificName ? detailMap.scientificName.value || '' : '', - - matches: matches, - selected: ko.observable(), - showMatches: ko.observable(false), - showDetails: ko.observable(false), - active: ko.observable(false) - }; - return vm; - } - - function search(query) { - const searchApi = SearchAPI.make({ - runtime: params.runtime - }); - return Promise.all([ - searchApi.referenceObjectSearch({ - query: query.terms.join(' '), - page: query.start || 0, - pageSize: searchConfig.fetchSize - }), - searchApi.typeSearch({ - query: query.terms.join(' '), - withPrivateData: 0, - withPublicData: 1, - dataSource: 'referenceData' - }) - ]) - .then(([objectResults, typeResults]) => { - const objects = objectResults.objects.map((object) => { - return objectToViewModel(object); - }); - const totalByType = Object.keys(typeResults.type_to_count).map(function (typeName) { - return { - id: typeName.toLowerCase(), - count: typeResults.type_to_count[typeName] - }; - }); - let totalSearchHits; - if (objectResults.total > maxSearchResults) { - totalSearchHits = maxSearchResults; - } else { - totalSearchHits = objectResults.total; - } - return { - items: objects, - first: query.start, - isTruncated: true, - summary: { - totalByType: totalByType, - totalSearchHits: totalSearchHits, - totalSearchSpace: objectResults.total, - isTruncated: totalSearchHits < objectResults.total - }, - stats: { - objectSearch: objectResults.search_time, - typeSearch: typeResults.search_time - } - }; - }); - } - - return { - search, - isStopWord - }; - } - - return { - make: factory - }; -}); diff --git a/react-app-todo/SystemAlertToggle/style.css b/react-app-todo/SystemAlertToggle/style.css deleted file mode 100644 index 27ff300a..00000000 --- a/react-app-todo/SystemAlertToggle/style.css +++ /dev/null @@ -1,12 +0,0 @@ -.SystemAlertToggle {} - -.SystemAlertToggle .-button { - border: 1px silver solid; - padding: 3px; - margin: 2px; - cursor: pointer -} - -.SystemAlertToggle .-button:hover { - background-color: rgba(200, 200, 200, 0.5); -} \ No newline at end of file diff --git a/react-app-todo/SystemAlertToggle/view.js b/react-app-todo/SystemAlertToggle/view.js deleted file mode 100644 index 7a1ee0c1..00000000 --- a/react-app-todo/SystemAlertToggle/view.js +++ /dev/null @@ -1,116 +0,0 @@ -define([ - 'preact', - 'htm' -], ( - preact, - htm -) => { - - const {h, Component } = preact; - const html = htm.bind(h); - - function plural(amount, singular, plural) { - if (amount === 1) { - return singular; - } - return plural; - } - - class SystemAlertToggle extends Component { - constructor(props) { - super(props); - } - - componentDidMount() { - } - - doToggle() { - this.props.runtime.send('system-alert', 'toggle-banner'); - } - - renderAlertCount() { - if (this.props.alerts && this.props.alerts.length > 0) { - return html` - ${this.props.alerts.length} - ${' '}${plural(this.props.alerts.length, 'alert', 'alerts')} - `; - } else { - return 'no alerts'; - } - } - - renderPresentAlerts() { - if (this.props.summary.present === 0) { - return; - } - return html` -
- -
- `; - } - - renderFutureAlerts() { - if (this.props.summary.future === 0) { - return; - } - return html` -
- -
- `; - } - - doClose() { - this.props.runtime.send('system-alert', 'close-banner'); - } - - renderSummary() { - if (!this.props.alerts || this.props.alerts.length === 0) { - return html` -
- -
- `; - } - - if (!this.props.summary) { - return; - } - return html` - - ${this.renderPresentAlerts()} - ${this.renderFutureAlerts()} - `; - } - - renderButton() { - // - return html` -
-
- ${this.renderAlertCount()} - ${this.renderSummary()} -
-
- `; - } - - render() { - // // We only show the toggle when - // if (!this.props.hideAlerts) { - // return; - // } - return html` -
- ${this.renderButton()} -
- `; - } - } - - return SystemAlertToggle; -}); \ No newline at end of file diff --git a/react-app-todo/services/feeds.ts b/react-app-todo/services/feeds.ts deleted file mode 100644 index 3025dd96..00000000 --- a/react-app-todo/services/feeds.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { DataState } from '../lib/DataState'; -import { Feeds } from '../lib/kb_lib/comm/coreServices/Feeds'; -import { tryPromise } from '../lib/kb_lib/Utils'; -import { Runtime } from '../lib/runtime'; -import { Config } from '../types/config'; - -const MONITORING_INTERVAL = 10000; - -interface FeedsServiceParams { - // params: { - // runtime: Runtime; - // }; - runtime: Runtime; - config: Config; -} - -export interface FeedsNotification { - unseenNotificationsCount?: number; - error?: string; -} - -export default class FeedsService { - runtime: Runtime; - monitoringInterval: number; - monitorRunning: boolean; - monitoringRunCount: number; - monitoringErrorCount: number; - disabled: boolean; - monitoringTimer: number | null; - state: DataState; - - constructor({ config, runtime }: FeedsServiceParams) { - this.runtime = runtime; - - // TODO: move to service config. - this.monitoringInterval = MONITORING_INTERVAL; - - this.monitorRunning = false; - this.monitoringRunCount = 0; - this.monitoringErrorCount = 0; - this.monitoringTimer = null; - - this.disabled = config.ui.coreServices.disabled.includes('Feeds'); - - this.state = new DataState({}); - } - - start() { - return tryPromise(() => { - this.runtime.db().set('feeds', { - notifications: null, - error: null, - }); - - if (this.disabled) { - console.warn( - 'Feeds service disabled; skipping monitoring hooks' - ); - return; - } - - // listen for login and out events... - this.runtime.on('session', 'loggedin', () => { - this.startFeedsMonitoring(); - }); - - this.runtime.on('session', 'loggedout', () => { - this.stopFeedsMonitoring(); - }); - - // if logged in, populate and start monitoring for feeds notifications - if (this.runtime.service('session').getAuthToken()) { - return this.startFeedsMonitoring(); - } - }); - } - - stop() { - return tryPromise(() => { - this.stopFeedsMonitoring(); - }); - } - - startFeedsMonitoring() { - if (this.monitorRunning) { - return; - } - this.monitorRunning = true; - this.monitoringLoop(); - } - - monitoringLoop() { - if (this.monitoringTimer) { - return; - } - - const monitoringJob = () => { - const feedsClient = new Feeds({ - url: this.runtime.config('services.Feeds.url', null), - token: this.runtime.service('session').getAuthToken(), - }); - return feedsClient - .getUnseenNotificationCount() - .then(({ unseen: { global, user } }) => { - const currentUnseen = global + user; - - // are notifications different than the last time? - const unseenNotificationsCount = this.runtime - .db() - .get('feeds.unseenNotificationsCount', 0); - - if (unseenNotificationsCount === currentUnseen) { - return; - } - - this.runtime.db().set('feeds', { - unseenNotificationsCount: currentUnseen, - error: null, - }); - }) - .catch((err) => { - console.error('ERROR', err.message); - this.runtime.db().set('feeds', { - error: err.message, - }); - }); - }; - - const loop = () => { - this.monitoringTimer = window.setTimeout(() => { - monitoringJob() - .then(() => { - this.monitoringRunCount += 1; - if (this.monitorRunning) { - loop(); - } - }) - .catch((err) => { - this.monitoringErrorCount += 1; - console.error('ERROR', err); - }); - }, this.monitoringInterval); - }; - - monitoringJob() - .then(() => { - loop(); - }) - .catch((err) => { - console.error('Error', err); - }); - } - - stopFeedsMonitoring() { - this.monitorRunning = false; - if (this.monitoringTimer !== null) { - window.clearTimeout(this.monitoringTimer); - } - this.monitoringTimer = null; - } - - // pluginHandler() {} -} - -export const ServiceClass = FeedsService; diff --git a/react-app-todo/session.ts b/react-app-todo/session.ts deleted file mode 100644 index 2c9a9e11..00000000 --- a/react-app-todo/session.ts +++ /dev/null @@ -1,210 +0,0 @@ -// define([ -// 'lib/kb_lib/Auth2Session', -// 'kb_lib/observed', -// 'kb_lib/html' -// ], ( -// { Auth2Session }, -// Observed, -// html -// ) => { -// const t = html.tag, -// div = t('div'), -// p = t('p'), -// a = t('a'); - -import { Auth2Session, CookieConfig } from '../lib/kb_lib/Auth2Session'; -import { Observed } from '../lib/kb_lib/observed'; -import { Runtime } from '../lib/runtime'; -import { Config } from '../types/config'; - -export interface SessionServiceState { - loggedIn: boolean; -} - -export class SessionService { - runtime: Runtime; - extraCookies: Array; - auth2Session: Auth2Session; - state: Observed; - constructor(runtime: Runtime, config: Config) { - this.runtime = runtime; - this.extraCookies = []; - if (config.ui.services.session.cookie.backup.enabled) { - this.extraCookies.push({ - name: config.ui.services.session.cookie.backup.name, - domain: config.ui.services.session.cookie.backup.domain, - }); - } - this.auth2Session = new Auth2Session({ - cookieName: config.ui.services.session.cookie.name, - extraCookies: this.extraCookies, - baseUrl: config.services.Auth2.url, - }); - - this.state = new Observed(null); - } - - getAuthToken() { - return this.auth2Session.getToken(); - } - - getUsername() { - return this.auth2Session.getUsername(); - } - - getEmail() { - return this.auth2Session.getEmail(); - } - - getRealname() { - return this.auth2Session.getRealname(); - } - - getRoles() { - return this.auth2Session.getRoles() || []; - } - - getCustomRoles() { - return this.auth2Session.getCustomRoles() || []; - } - - getTokenInfo() { - return this.auth2Session.getTokenInfo(); - } - - getMe() { - return this.auth2Session.getMe(); - } - - isLoggedIn() { - return this.auth2Session.isLoggedIn(); - } - - isAuthorized() { - return this.auth2Session.isAuthorized(); - } - - isAuthenticated() { - return this.auth2Session.isAuthorized(); - } - - getKbaseSession() { - return this.auth2Session.getKbaseSession(); - } - - getLastProvider() { - return this.auth2Session.getLastProvider(); - } - - getProviders() { - return this.auth2Session.getClient().getProviders(); - } - - // Session state change - loginStart(arg) { - // starts an auth login / signup redirect loop - // it _could_ be done inside an iframe ... - this.auth2Session.loginStart(arg); - } - - logout() { - return this.auth2Session.logout().then((result) => { - return result; - }); - } - - notifyError(message) { - this.runtime.send('ui', 'alert', { - type: 'warning', - message: message.message, - description: message.description, - icon: 'exclamation-triangle', - name: 'auth-connection', - }); - } - - notifyOk(message) { - this.runtime.send('ui', 'alert', { - type: 'success', - message: message.message, - description: message.description, - icon: 'check', - name: 'auth-connection', - timeout: 10000, - }); - } - - start() { - return this.auth2Session.start().then(() => { - if (this.auth2Session.isAuthorized()) { - this.state.setItem('loggedin', true); - this.runtime.send('session', 'loggedin'); - } else { - this.state.setItem('loggedin', false); - this.runtime.send('session', 'loggedout'); - } - this.auth2Session.onChange((change) => { - this.runtime.send('session', 'change', { - state: change, - }); - switch (change) { - case 'interrupted': - var description = div([ - p( - 'Your session cannot be verified because the authorization service is currently inaccessible' - ), - p([ - "You may patiently await it's recovery or ", - a( - { - href: '/#signout', - }, - 'signout' - ), - ' and try again later', - ]), - ]); - this.notifyError({ - message: 'Session cannot be verified', - description: description, - }); - return; - case 'restored': - this.notifyOk({ - message: - 'Communication restored -- session has been verified', - description: '', - }); - } - if (this.auth2Session.isAuthorized()) { - if (change === 'newuser') { - // TODO: do something special... - } - this.state.setItem('loggedin', true); - this.runtime.send('session', 'loggedin'); - } else { - this.state.setItem('loggedin', false); - this.runtime.send('session', 'loggedout'); - } - }); - }); - } - - stop() { - return this.auth2Session.stop().then(() => { - // session = null; - }); - } - - onChange(fun) { - this.state.listen('loggedin', { - onSet: (value) => { - fun(value); - }, - }); - } - - getClient() { - return this.auth2Session; - } -} From 09afc23306fe4f354029e3f4d9d2535a96385bfa Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Fri, 20 Oct 2023 21:00:29 +0000 Subject: [PATCH 3/6] a few updates to work better w/integration tests [CE-314] --- vite-app/src/components/Body.tsx | 2 +- vite-app/src/components/Signin/Signin.tsx | 4 ++-- vite-app/src/components/Title.module.css | 1 + vite-app/src/components/Title.tsx | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/vite-app/src/components/Body.tsx b/vite-app/src/components/Body.tsx index 8f013947..7a9f449c 100644 --- a/vite-app/src/components/Body.tsx +++ b/vite-app/src/components/Body.tsx @@ -349,7 +349,7 @@ export default class Body extends Component { // Samples new Route( 'samples/view/:sampleId/:sampleVersion?', - { authenticationRequired: true }, + { authenticationRequired: false }, (props: RouteProps) => { return ( { renderAvatar(userProfile: UserProfile) { const avatarURL = this.renderAvatarUrl(userProfile); - return ( {`Avatar @@ -80,7 +80,7 @@ export default class Signin extends Component { renderLoggedIn(authState: AuthenticationStateAuthenticated) { return ( - + {this.renderAvatar(authState.userProfile)} diff --git a/vite-app/src/components/Title.module.css b/vite-app/src/components/Title.module.css index ca6575eb..c6b8828f 100644 --- a/vite-app/src/components/Title.module.css +++ b/vite-app/src/components/Title.module.css @@ -4,4 +4,5 @@ overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; + margin: 0; } \ No newline at end of file diff --git a/vite-app/src/components/Title.tsx b/vite-app/src/components/Title.tsx index d605f061..d01651cd 100644 --- a/vite-app/src/components/Title.tsx +++ b/vite-app/src/components/Title.tsx @@ -5,7 +5,7 @@ export interface TitleProps { title: string; } -interface TitleState {} +interface TitleState { } export default class Title extends Component { constructor(props: TitleProps) { @@ -19,9 +19,9 @@ export default class Title extends Component { // Note that this allows html to be set in the title. This allows plugins to set // html. return ( -
+

{this.props.title} -

+ ); } } From 8fce761d80f1e1adc3f0b4f855181e20ea65c0a9 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Tue, 24 Oct 2023 14:10:33 -0700 Subject: [PATCH 4/6] support a base path [CE-315] wip - not finished yet, but much in place. --- .devcontainer/docker-compose.yml | 6 +- Dockerfile | 2 +- Taskfile | 2 +- config/README.md | 11 - deployment/templates/config.json.tmpl | 4 +- tools/deno/scripts/hello.ts | 29 -- tools/deno/scripts/install-plugin.ts | 8 - tools/deno/scripts/install-plugins.ts | 8 - tools/dockerize/docker-compose.yml | 1 + tools/dockerize/scripts/render-templates.sh | 1 + tools/proxy/contents/conf/nginx.conf.tmpl | 31 +- vite-app/index.html | 14 +- vite-app/package-lock.json | 239 +++++---- vite-app/package.json | 12 +- .../about/AboutKBaseUI/AboutKBaseUI.tsx | 4 +- .../NarrativeDetails/NarrativeHeader.tsx | 2 +- .../NarrativeDetails/ToolMenu/LinkOrgItem.tsx | 2 +- .../NarrativeDetails/cells/AppCell.tsx | 2 +- .../NarrativeDetails/cells/DataObjectCell.tsx | 6 +- .../components/NarrativeList/CategoryMenu.tsx | 4 +- .../NarrativeList/NarrativeList.tsx | 2 +- .../src/apps/ORCIDLink/continue/Error.tsx | 2 +- .../ORCIDLink/home/LinkPermissions/View.tsx | 2 +- vite-app/src/apps/ORCIDLink/home/View.tsx | 4 +- .../ORCIDLink/manage/linkingSessions/view.tsx | 6 +- .../apps/ORCIDLink/manage/queryLinks/view.tsx | 4 +- .../apps/ORCIDLink/manage/viewLink/view.tsx | 4 +- .../UserProfile/Profile/ProfileEditor.tsx | 2 +- .../apps/UserProfile/Profile/controller.tsx | 2 +- .../src/apps/UserProfile/SearchUsers/view.tsx | 2 +- vite-app/src/apps/gallery/index.tsx | 6 +- vite-app/src/components/HelpLinks.tsx | 8 +- .../NotFoundChecked/NotFoundChecked.tsx | 8 +- .../src/components/Signin/SigninButton.tsx | 2 +- vite-app/src/gtagSupport.ts | 467 ++++++++++++++++++ vite-app/src/lib/kb_lib/Auth2.ts | 1 + .../src/lib/kb_lib/comm/coreServices/Auth.ts | 1 + vite-app/vite.config-og.ts | 89 ++++ vite-app/vite.config.ts | 240 ++++++--- 39 files changed, 917 insertions(+), 323 deletions(-) delete mode 100644 tools/deno/scripts/hello.ts create mode 100644 vite-app/src/gtagSupport.ts create mode 100644 vite-app/vite.config-og.ts diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 62326960..13583fdb 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -44,7 +44,7 @@ services: environment: # - PORT=80 - DEPLOY_ENV=${DEPLOY_ENV:-ci} - # - BASE_PATH= + - BASE_PATH=${BASE_PATH} # Required for a devcontainer -- keeps the container running. # Don't worry, our main interaction with the container is through the # VSC terminal, which for a devcontainer opens a shell within the @@ -95,10 +95,10 @@ services: - 1.1.1.1 - 208.67.222.222 environment: - - BASE_PATH=/ + - BASE_PATH=${BASE_PATH} - # note that this is really a docker env file and is relative to the docker-compose file + # note that this is really aq docker env file and is relative to the docker-compose file # TODO: I think we may need to use an entrypoint which selects the deploy environment # config (env) file based on the DEPLOY_ENV. diff --git a/Dockerfile b/Dockerfile index 3ab6c674..41d2e210 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,4 +50,4 @@ ENTRYPOINT [ "dockerize" ] CMD [ \ "-template", "/kb/deployment/templates/nginx.conf.tmpl:/etc/nginx/nginx.conf", \ "-template", "/kb/deployment/templates/config.json.tmpl:/kb/deployment/app/deploy/config.json", \ - "bash", "/kb/deployment/scripts/start-server.bash" ] + "bash", "-x", "/kb/deployment/scripts/start-server.bash" ] diff --git a/Taskfile b/Taskfile index 94e3f31e..519b6ba8 100755 --- a/Taskfile +++ b/Taskfile @@ -108,7 +108,7 @@ function format { } function render-templates { - env DIR="${PWD}" ENV="${ENV:-ci}" DEFAULT_PATH="${DEFAULT_PATH}" bash tools/dockerize/scripts/render-templates.sh + env DIR="${PWD}" ENV="${ENV:-ci}" DEFAULT_PATH="${DEFAULT_PATH}" BASE_PATH="${BASE_PATH}" bash tools/dockerize/scripts/render-templates.sh } # Optional dev tooling: diff --git a/config/README.md b/config/README.md index 681b1747..0008911e 100644 --- a/config/README.md +++ b/config/README.md @@ -1,14 +1,3 @@ # Configuration This directory contains files necessary to configure kbase ui builds: - -```services.yml`` is a YAML file containing definitions of all service endpoints which need to be known to the ui. Most service endpoints are defined as template strings in which the base url is placeholder which is populated at runtime by the deployment configuration (more about that later.) - - -```npmInstall.yml``` is a YAML file containing declarative instructions for installing each and every package brought into the project via bower. - - -- ```builds``` - one file per named configuration set. -- ```deploy``` - classic KBase deploy configuration: service urls, target dirs, per KBase deploy environment. -- ```ui``` - ui build configurations -- ```bowerInstall``` - master file for importing bower packages into kbase module space. diff --git a/deployment/templates/config.json.tmpl b/deployment/templates/config.json.tmpl index aeeff4e5..ea647346 100644 --- a/deployment/templates/config.json.tmpl +++ b/deployment/templates/config.json.tmpl @@ -2,7 +2,7 @@ "deploy": { "id": "kbase-ui", "target": "/kb/deployment/app", - "basePath": "{{ default "" .Env.deploy_basepath }}", + "basePath": "{{ default "" .Env.BASE_PATH }}", "environment": "{{ .Env.deploy_environment }}", "displayEnvironment": {{ eq .Env.deploy_environment "prod" | ternary true false }}, "hostname": "{{ .Env.deploy_hostname }}", @@ -174,7 +174,7 @@ "defaults": { "path": { "type": "path", - "value": "{{ default "/narratives" .Env.DEFAULT_PATH }}" + "value": "{{ default "#about" .Env.DEFAULT_PATH }}" }, "hideHeader": {{ default "false" .Env.HIDE_HEADER }}, "hideNavigation": {{ default "false" .Env.HIDE_NAVIGATION }}, diff --git a/tools/deno/scripts/hello.ts b/tools/deno/scripts/hello.ts deleted file mode 100644 index 15a0dc97..00000000 --- a/tools/deno/scripts/hello.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Runner } from "./common.ts"; - - -async function main() { - // if (Deno.args.length !== 2) { - // log("Usage: git-info.ts "); - // Deno.exit(1); - // } - // const targetDir = Deno.args[0]; - // const destinationFile = Deno.args[1]; - - // log(`Getting git info from ${targetDir}`, "git-info.ts:main()"); - // log(`Saving git info info to ${destinationFile}`, "git-info.ts:main()"); - - // const info = await getGitInfo(targetDir); - // log(JSON.stringify(info, null, 4), "git-info.ts:main()"); - - // await Deno.writeFile( - // destinationFile, - // new TextEncoder().encode(JSON.stringify(info, null, 4)), - // ); - const runner = new Runner('/app'); - const result = await runner.run('cat', ['Taskfile']); - console.log('RESULT', result); -} - -if (import.meta.main) { - main(); -} diff --git a/tools/deno/scripts/install-plugin.ts b/tools/deno/scripts/install-plugin.ts index 088d59e2..cf9b0ff1 100644 --- a/tools/deno/scripts/install-plugin.ts +++ b/tools/deno/scripts/install-plugin.ts @@ -286,14 +286,6 @@ async function updatePluginManifest( log('done!', 'updatePluginManifest'); } -// async function savePluginManifest(path) { -// const root = state.environment.path; -// const configDest = root.concat(['build', 'client', 'modules', 'config']); -// const manifestPath = configDest.concat(['plugins-manifest.json']); -// await mutant.saveJson(manifestPath, state.pluginsManifest); -// return state; -// } - async function main() { if (Deno.args.length < 4) { log(`Incorrect number of args ${Deno.args.length}`) diff --git a/tools/deno/scripts/install-plugins.ts b/tools/deno/scripts/install-plugins.ts index 6242102b..677bde4d 100644 --- a/tools/deno/scripts/install-plugins.ts +++ b/tools/deno/scripts/install-plugins.ts @@ -307,14 +307,6 @@ async function generatePluginsManifest(uiConfig: string, source: string, dest: s log('done!', 'generatePluginsManifest'); } -// async function savePluginManifest(path) { -// const root = state.environment.path; -// const configDest = root.concat(['build', 'client', 'modules', 'config']); -// const manifestPath = configDest.concat(['plugins-manifest.json']); -// await mutant.saveJson(manifestPath, state.pluginsManifest); -// return state; -// } - async function main() { if (Deno.args.length < 3) { log(`Incorrect number of args ${Deno.args.length}`) diff --git a/tools/dockerize/docker-compose.yml b/tools/dockerize/docker-compose.yml index 2f064c0b..783f710e 100644 --- a/tools/dockerize/docker-compose.yml +++ b/tools/dockerize/docker-compose.yml @@ -6,6 +6,7 @@ services: container_name: dockerize dns: 8.8.8.8 environment: + - BASE_PATH - DEFAULT_PATH - HIDE_HEADER - HIDE_NAVIGATION diff --git a/tools/dockerize/scripts/render-templates.sh b/tools/dockerize/scripts/render-templates.sh index eba413e5..192cef29 100644 --- a/tools/dockerize/scripts/render-templates.sh +++ b/tools/dockerize/scripts/render-templates.sh @@ -3,6 +3,7 @@ echo "Rendering kbase-ui config file from template for environment '${ENV}' usin echo "Parameters (Environment Variables):" echo " ENV=${ENV}" echo " DIR=${DIR}" +echo " BASE_PATH=${BASE_PATH}" echo " DEFAULT_PATH=${DEFAULT_PATH}" echo " HIDE_HEADER=${HIDE_HEADER}" echo " HIDE_NAVIGATION=${HIDE_NAVIGATION}" diff --git a/tools/proxy/contents/conf/nginx.conf.tmpl b/tools/proxy/contents/conf/nginx.conf.tmpl index 30d18dfd..1bfe9664 100644 --- a/tools/proxy/contents/conf/nginx.conf.tmpl +++ b/tools/proxy/contents/conf/nginx.conf.tmpl @@ -153,12 +153,12 @@ http { return 413 "Request entity is too large (> 5GB)"; } - location /__ping__ { + location {{ .Env.BASE_PATH }}/__ping__ { default_type "text/html"; return 204; } - location /data/__perf__ { + location {{ .Env.BASE_PATH }}/data/__perf__ { gzip off; gunzip on; add_header Cache-Control 'no-cache, no-transform'; @@ -226,7 +226,7 @@ http { # proxy_pass https://kbase_navigator/narratives/; # {{ end }} - {{ if .Env.deploy_ui_hostname }} + {{ if .Env.deploy_ui_hostname }} proxy_pass https://{{ .Env.deploy_ui_hostname }}/narratives/; {{ else }} proxy_pass https://{{ .Env.deploy_hostname }}/narratives/; @@ -252,6 +252,20 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } + + # This one to handle legacy plugin /modules/plugins paths in dev + location / { + proxy_pass http://kbase_ui; + #proxy_set_header Cache-Control 'no-store'; + #proxy_set_header Cache-Control 'no-cache'; + #expires 0; + # proxy_set_header Connection ""; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } } {{ end }} @@ -415,7 +429,7 @@ http { # host name as services. {{ if not .Env.deploy_ui_hostname }} - location /__ping__ { + location {{ .Env.BASE_PATH }}/__ping__ { default_type "text/html"; return 204; } @@ -492,6 +506,15 @@ http { proxy_set_header Connection $connection_upgrade; } + # legacy plugins + location / { + proxy_pass http://kbase_ui; + proxy_set_header Connection ""; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + {{ end }} } } \ No newline at end of file diff --git a/vite-app/index.html b/vite-app/index.html index d06ae8fe..b8abf8c5 100644 --- a/vite-app/index.html +++ b/vite-app/index.html @@ -2,18 +2,16 @@ + + - - - - - - + + KBase @@ -25,14 +23,14 @@
Loading KBase UI ...
- KBase animated logo + KBase animated logo
- + \ No newline at end of file diff --git a/vite-app/package-lock.json b/vite-app/package-lock.json index 9781b3ac..c16f769a 100644 --- a/vite-app/package-lock.json +++ b/vite-app/package-lock.json @@ -9,14 +9,14 @@ "version": "0.0.0", "dependencies": { "@ant-design/icons": "5.2.6", - "antd": "5.10.1", + "antd": "5.10.2", "bootstrap": "5.3.2", "dompurify": "3.0.6", "es-cookie": "1.4.0", "font-awesome": "4.7.0", "marked": "9.1.2", "react": "18.2.0", - "react-bootstrap": "2.9.0", + "react-bootstrap": "2.9.1", "react-bootstrap-icons": "1.10.3", "react-dom": "18.2.0", "react-router-dom": "6.17.0", @@ -30,15 +30,15 @@ "@testing-library/user-event": "14.5.1", "@types/dompurify": "3.0.4", "@types/jest": "29.5.6", - "@types/react": "18.2.30", + "@types/react": "18.2.31", "@types/react-dom": "18.2.14", "@types/react-router-dom": "5.3.3", "@types/uuid": "9.0.6", - "@typescript-eslint/eslint-plugin": "6.8.0", - "@typescript-eslint/parser": "6.8.0", + "@typescript-eslint/eslint-plugin": "6.9.0", + "@typescript-eslint/parser": "6.9.0", "@vitejs/plugin-react": "4.1.0", "@vitest/coverage-v8": "0.34.6", - "eslint": "8.51.0", + "eslint": "8.52.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.3", "jsdom": "22.1.0", @@ -594,11 +594,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -1178,9 +1178,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1200,12 +1200,12 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -1227,9 +1227,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@istanbuljs/schema": { @@ -1487,17 +1487,16 @@ } }, "node_modules/@rc-component/trigger": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-1.17.0.tgz", - "integrity": "sha512-KN+lKHCi7L4kjuA9DU2PnwZxtIyes6R1wsexp0/Rnjr/ITELsPuC9kpzDK1+7AZMarDXUAHUdDGS2zUNEx2P0g==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-1.17.2.tgz", + "integrity": "sha512-Jp3dXk/IzwHKM2Tn3ezdvQSwkPeH4v1s7QjIo7f5NFLIZVpJQ8a34FduZw8E6fT1PVgLXYd/JBIyd+YpgyQddA==", "dependencies": { - "@babel/runtime": "^7.18.3", + "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", - "rc-align": "^4.0.0", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", - "rc-util": "^5.33.0" + "rc-util": "^5.38.0" }, "engines": { "node": ">=8.x" @@ -1853,9 +1852,9 @@ "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, "node_modules/@types/node": { @@ -1875,9 +1874,9 @@ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { - "version": "18.2.30", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.30.tgz", - "integrity": "sha512-OfqdJnDsSo4UNw0bqAjFCuBpLYQM7wvZidz0hVxHRjrEkzRlvZL1pJVyOSY55HMiKvRNEo9DUBRuEl7FNlJ/Vg==", + "version": "18.2.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", + "integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1928,9 +1927,9 @@ "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "node_modules/@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", "dev": true }, "node_modules/@types/stack-utils": { @@ -1972,16 +1971,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", - "integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.0.tgz", + "integrity": "sha512-lgX7F0azQwRPB7t7WAyeHWVfW1YJ9NIgd9mvGhfQpRY56X6AVf8mwM8Wol+0z4liE7XX3QOt8MN1rUKCfSjRIA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.8.0", - "@typescript-eslint/type-utils": "6.8.0", - "@typescript-eslint/utils": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/type-utils": "6.9.0", + "@typescript-eslint/utils": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2007,15 +2006,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz", - "integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.0.tgz", + "integrity": "sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.8.0", - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/typescript-estree": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", "debug": "^4.3.4" }, "engines": { @@ -2035,13 +2034,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz", - "integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz", + "integrity": "sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0" + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2052,13 +2051,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz", - "integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.0.tgz", + "integrity": "sha512-XXeahmfbpuhVbhSOROIzJ+b13krFmgtc4GlEuu1WBT+RpyGPIA4Y/eGnXzjbDj5gZLzpAXO/sj+IF/x2GtTMjQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.8.0", - "@typescript-eslint/utils": "6.8.0", + "@typescript-eslint/typescript-estree": "6.9.0", + "@typescript-eslint/utils": "6.9.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -2079,9 +2078,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz", - "integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.0.tgz", + "integrity": "sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2092,13 +2091,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz", - "integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz", + "integrity": "sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/visitor-keys": "6.9.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2119,17 +2118,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz", - "integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.0.tgz", + "integrity": "sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.8.0", - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/scope-manager": "6.9.0", + "@typescript-eslint/types": "6.9.0", + "@typescript-eslint/typescript-estree": "6.9.0", "semver": "^7.5.4" }, "engines": { @@ -2144,12 +2143,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz", - "integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz", + "integrity": "sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/types": "6.9.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2160,6 +2159,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@vitejs/plugin-react": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.1.0.tgz", @@ -2452,9 +2457,9 @@ } }, "node_modules/antd": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.10.1.tgz", - "integrity": "sha512-alcBmeH4oAdmEdBs6EORH3onRFRjGYRkWtVjPyJxlTIfLILb/+S5Y+ZqisV3AobC8mlj6T3RV8aKG9ic6PgtzQ==", + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.10.2.tgz", + "integrity": "sha512-0kV6PmlJi7vhPmYH9GCAlU62ZhiuLF+gE3REJ/9MZTo++/3i5q6SALNoRgHLMsa+rX50U3RO3wJVY+fPib594Q==", "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/cssinjs": "^1.17.2", @@ -2465,7 +2470,7 @@ "@rc-component/color-picker": "~1.4.1", "@rc-component/mutate-observer": "^1.1.0", "@rc-component/tour": "~1.10.0", - "@rc-component/trigger": "^1.17.0", + "@rc-component/trigger": "^1.17.2", "classnames": "^2.2.6", "copy-to-clipboard": "^3.2.0", "dayjs": "^1.11.1", @@ -2473,7 +2478,7 @@ "rc-cascader": "~3.18.1", "rc-checkbox": "~3.1.0", "rc-collapse": "~3.7.1", - "rc-dialog": "~9.3.3", + "rc-dialog": "~9.3.4", "rc-drawer": "~6.5.2", "rc-dropdown": "~4.1.0", "rc-field-form": "~1.39.0", @@ -2488,10 +2493,10 @@ "rc-picker": "~3.14.5", "rc-progress": "~3.5.1", "rc-rate": "~2.12.0", - "rc-resize-observer": "^1.3.1", + "rc-resize-observer": "^1.4.0", "rc-segmented": "~2.2.2", - "rc-select": "~14.9.1", - "rc-slider": "~10.3.0", + "rc-select": "~14.9.2", + "rc-slider": "~10.3.1", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", "rc-table": "~7.34.4", @@ -3136,11 +3141,6 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true }, - "node_modules/dom-align": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz", - "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==" - }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -3282,18 +3282,19 @@ } }, "node_modules/eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -5317,22 +5318,6 @@ } ] }, - "node_modules/rc-align": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-4.0.15.tgz", - "integrity": "sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "dom-align": "^1.7.0", - "rc-util": "^5.26.0", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/rc-cascader": { "version": "3.18.1", "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.18.1.tgz", @@ -5380,9 +5365,9 @@ } }, "node_modules/rc-dialog": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.3.3.tgz", - "integrity": "sha512-OpgzE0wq55ebN8TL/ZPc+MLY6qXswEuZg2/3uX3+lqjxUnVaH78PyntpJwqY+3BJdQkDj28XeXYRVY6gXQ8fNg==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.3.4.tgz", + "integrity": "sha512-975X3018GhR+EjZFbxA2Z57SX5rnu0G0/OxFgMMvZK4/hQWEm3MHaNvP4wXpxYDoJsp+xUvVW+GB9CMMCm81jA==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", @@ -5654,13 +5639,13 @@ } }, "node_modules/rc-resize-observer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.3.1.tgz", - "integrity": "sha512-iFUdt3NNhflbY3mwySv5CA1TC06zdJ+pfo0oc27xpf4PIOvfZwZGtD9Kz41wGYqC4SLio93RVAirSSpYlV/uYg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz", + "integrity": "sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q==", "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", - "rc-util": "^5.27.0", + "rc-util": "^5.38.0", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { @@ -5705,9 +5690,9 @@ } }, "node_modules/rc-slider": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.3.0.tgz", - "integrity": "sha512-kt8ehfvPLoZFYJS3Caaf3l+9OF8JUyexC84qy878vf9rOy9IulO/PEdha8E90i8mnj4mtJMSxojk1fJGkYWjpQ==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.3.1.tgz", + "integrity": "sha512-XszsZLkbjcG9ogQy/zUC0n2kndoKUAnY/Vnk1Go5Gx+JJQBz0Tl15d5IfSiglwBUZPS9vsUJZkfCmkIZSqWbcA==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -5920,9 +5905,9 @@ } }, "node_modules/react-bootstrap": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.9.0.tgz", - "integrity": "sha512-dGh6fGjqR9MBzPOp2KbXJznt1Zy6SWepXYUdxMT18Zu/wJ73HCU8JNZe9dfzjmVssZYsJH9N3HHE4wAtQvNz7g==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.9.1.tgz", + "integrity": "sha512-ezgmh/ARCYp18LbZEqPp0ppvy+ytCmycDORqc8vXSKYV3cer4VH7OReV8uMOoKXmYzivJTxgzGHalGrHamryHA==", "dependencies": { "@babel/runtime": "^7.22.5", "@restart/hooks": "^0.4.9", @@ -6081,9 +6066,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", diff --git a/vite-app/package.json b/vite-app/package.json index f3eceb8b..0aed2193 100644 --- a/vite-app/package.json +++ b/vite-app/package.json @@ -12,14 +12,14 @@ }, "dependencies": { "@ant-design/icons": "5.2.6", - "antd": "5.10.1", + "antd": "5.10.2", "bootstrap": "5.3.2", "dompurify": "3.0.6", "es-cookie": "1.4.0", "font-awesome": "4.7.0", "marked": "9.1.2", "react": "18.2.0", - "react-bootstrap": "2.9.0", + "react-bootstrap": "2.9.1", "react-bootstrap-icons": "1.10.3", "react-dom": "18.2.0", "react-router-dom": "6.17.0", @@ -33,15 +33,15 @@ "@testing-library/user-event": "14.5.1", "@types/dompurify": "3.0.4", "@types/jest": "29.5.6", - "@types/react": "18.2.30", + "@types/react": "18.2.31", "@types/react-dom": "18.2.14", "@types/react-router-dom": "5.3.3", "@types/uuid": "9.0.6", - "@typescript-eslint/eslint-plugin": "6.8.0", - "@typescript-eslint/parser": "6.8.0", + "@typescript-eslint/eslint-plugin": "6.9.0", + "@typescript-eslint/parser": "6.9.0", "@vitejs/plugin-react": "4.1.0", "@vitest/coverage-v8": "0.34.6", - "eslint": "8.51.0", + "eslint": "8.52.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.3", "jsdom": "22.1.0", diff --git a/vite-app/src/applets/about/AboutKBaseUI/AboutKBaseUI.tsx b/vite-app/src/applets/about/AboutKBaseUI/AboutKBaseUI.tsx index 85a6143d..0ac4d8bb 100644 --- a/vite-app/src/applets/about/AboutKBaseUI/AboutKBaseUI.tsx +++ b/vite-app/src/applets/about/AboutKBaseUI/AboutKBaseUI.tsx @@ -38,7 +38,7 @@ export default class AboutKBaseUI extends Component<
  • The{' '} @@ -179,7 +179,7 @@ export default class AboutKBaseUI extends Component< 'http://kbaseincubator.github.io/kbase-ui-docs'; const documentationURL = this.props.config.ui.urls.documentation.url; - const [githubContent, githubUrl,relNotesUrl] = this.renderGitInfo(); + const [githubContent, githubUrl, relNotesUrl] = this.renderGitInfo(); const kbaseGithubOrgURL = 'https://github.com/kbase'; diff --git a/vite-app/src/apps/Navigator/components/NarrativeDetails/NarrativeHeader.tsx b/vite-app/src/apps/Navigator/components/NarrativeDetails/NarrativeHeader.tsx index 90b26651..b6de9dab 100644 --- a/vite-app/src/apps/Navigator/components/NarrativeDetails/NarrativeHeader.tsx +++ b/vite-app/src/apps/Navigator/components/NarrativeDetails/NarrativeHeader.tsx @@ -56,7 +56,7 @@ function countCellTypes(narrative: NarrativeSearchDoc): CellTypesInfo { } const profileLink = (username: string, realname: string) => ( - + {realname} ); diff --git a/vite-app/src/apps/Navigator/components/NarrativeDetails/ToolMenu/LinkOrgItem.tsx b/vite-app/src/apps/Navigator/components/NarrativeDetails/ToolMenu/LinkOrgItem.tsx index ad97cff6..aeada1bb 100644 --- a/vite-app/src/apps/Navigator/components/NarrativeDetails/ToolMenu/LinkOrgItem.tsx +++ b/vite-app/src/apps/Navigator/components/NarrativeDetails/ToolMenu/LinkOrgItem.tsx @@ -326,7 +326,7 @@ const LinkedOrg = (props: LinkedOrgProps) => {
    diff --git a/vite-app/src/apps/Navigator/components/NarrativeDetails/cells/AppCell.tsx b/vite-app/src/apps/Navigator/components/NarrativeDetails/cells/AppCell.tsx index c3fe97dc..cd36ad36 100644 --- a/vite-app/src/apps/Navigator/components/NarrativeDetails/cells/AppCell.tsx +++ b/vite-app/src/apps/Navigator/components/NarrativeDetails/cells/AppCell.tsx @@ -53,7 +53,7 @@ export default class AppCellView extends Component { <>
    diff --git a/vite-app/src/apps/Navigator/components/NarrativeDetails/cells/DataObjectCell.tsx b/vite-app/src/apps/Navigator/components/NarrativeDetails/cells/DataObjectCell.tsx index 2edd55ea..1cab9e71 100644 --- a/vite-app/src/apps/Navigator/components/NarrativeDetails/cells/DataObjectCell.tsx +++ b/vite-app/src/apps/Navigator/components/NarrativeDetails/cells/DataObjectCell.tsx @@ -23,7 +23,7 @@ export default class DataObjectCellView extends Component { return (
    diff --git a/vite-app/src/apps/ORCIDLink/manage/linkingSessions/view.tsx b/vite-app/src/apps/ORCIDLink/manage/linkingSessions/view.tsx index 738737fc..de550ed9 100644 --- a/vite-app/src/apps/ORCIDLink/manage/linkingSessions/view.tsx +++ b/vite-app/src/apps/ORCIDLink/manage/linkingSessions/view.tsx @@ -65,7 +65,7 @@ export default class QueryLinkingSessionsView extends Component { return - {username} + {username} {session_id} @@ -114,7 +114,7 @@ export default class QueryLinkingSessionsView extends Component { return - {username} + {username} {session_id} @@ -172,7 +172,7 @@ export default class QueryLinkingSessionsView extends Component { return - {username} + {username} {session_id} diff --git a/vite-app/src/apps/ORCIDLink/manage/queryLinks/view.tsx b/vite-app/src/apps/ORCIDLink/manage/queryLinks/view.tsx index 3eebf740..f5a67e43 100644 --- a/vite-app/src/apps/ORCIDLink/manage/queryLinks/view.tsx +++ b/vite-app/src/apps/ORCIDLink/manage/queryLinks/view.tsx @@ -21,7 +21,7 @@ interface ORCIDLinkManageState { export default class ORCIDLinkManageView extends Component { viewLink(username: string) { - const url = `/#orcidlink/manage/link/${username}`; + const url = `#orcidlink/manage/link/${username}`; window.open(url, '_blank'); } @@ -52,7 +52,7 @@ export default class ORCIDLinkManageView extends Component { - return {linkRecord.username} + return {linkRecord.username} } }, { diff --git a/vite-app/src/apps/ORCIDLink/manage/viewLink/view.tsx b/vite-app/src/apps/ORCIDLink/manage/viewLink/view.tsx index c1807124..68e1f2e4 100644 --- a/vite-app/src/apps/ORCIDLink/manage/viewLink/view.tsx +++ b/vite-app/src/apps/ORCIDLink/manage/viewLink/view.tsx @@ -85,7 +85,7 @@ export default class ORCIDLinkManageView extends ComponentTools - + @@ -97,7 +97,7 @@ export default class ORCIDLinkManageView extends Component -
  • - Narrative + Narrative
  • - Dashboard + Narratives Navigator
  • @@ -64,7 +64,7 @@ export default class NotFound extends Component - + } } diff --git a/vite-app/src/components/NotFoundChecked/NotFoundChecked.tsx b/vite-app/src/components/NotFoundChecked/NotFoundChecked.tsx index 01a72b58..c4b21195 100644 --- a/vite-app/src/components/NotFoundChecked/NotFoundChecked.tsx +++ b/vite-app/src/components/NotFoundChecked/NotFoundChecked.tsx @@ -145,7 +145,7 @@ export default class NotFound extends Component< 'redirect-to-www' ) ) { - this.redirect('/#login'); + this.redirect('#login'); return; } // this.props.setTitle('Redirecting to h...'); @@ -225,7 +225,7 @@ export default class NotFound extends Component< messages: [ ...this.state.messages.slice(0, -1), this.state.messages[ - this.state.messages.length - 1 + this.state.messages.length - 1 ] + 'nope', ], }, @@ -474,10 +474,10 @@ export default class NotFound extends Component<
  • - Narrative + Narrative
  • - Dashboard + Narratives Navigator
  • diff --git a/vite-app/src/components/Signin/SigninButton.tsx b/vite-app/src/components/Signin/SigninButton.tsx index fac8b5fd..7f68b28f 100644 --- a/vite-app/src/components/Signin/SigninButton.tsx +++ b/vite-app/src/components/Signin/SigninButton.tsx @@ -35,7 +35,7 @@ export default class Signin extends Component { render() { const url = new URL(window.location.href); - url.pathname = ''; + // url.pathname = ''; url.hash = '#login'; const nextRequest = this.props.nextRequest; // || this.makeNextRequstFromHere(); diff --git a/vite-app/src/gtagSupport.ts b/vite-app/src/gtagSupport.ts new file mode 100644 index 00000000..1476713a --- /dev/null +++ b/vite-app/src/gtagSupport.ts @@ -0,0 +1,467 @@ +/** + * Google Analytics (GA) app implementation + * + * The GA mechanism for sending data to the GA collection service + * consists of a very simple, small web app. + * + * Upon loading the primary kbase-ui SPA index.html page, an embedded + * script call to Google loads a GA app which loads necessary support + * files and begins to monitor a global variable "window.dataLayer". + * + * (Why Goggle didn't pick something namespaced to their app like + * GoogleDataLayer, or GoogleAnalyticsDataQueue, I don't know.) + * + * Then, when data is to be sent to GA for a page load (all that we use it for), + * this script below pushes (in the sense of "array push") the appropriate + * data in the appropriate format into the global data queue "dataLayer". + * + * On the next polling call, the GA app pulls the data off the queue and sends it + * to the GA collection service. + * + * Quite straightforward. + * + * Now for our implementation. + * + * First, there is no good reason for the code to be in a standalone file, afaik, + * and it is a bit complicated by the fact that it has to replicate functionality + * in kbase-ui, but here it is. + * + * The script has the general task of sending, for the initial page load and every + * subsequent navigation, the: + * - current url + * - current page path + * - current page title + * - username if there is a valid kbase auth token + * + * There a few complications in this otherwise relatively straightforward task. + * + * Since this script is independent of kbase-ui, it must be configured manually, + * including the auth token name, urls, Google GA ids, and other. + * + * Fetching the username requires a call to the auth service. Since this + * script is independent of kbase-ui, it must perform this logic separately. + * To prevent calling the auth service with every navigation, the token is + * cached for 10 minutes. + * + * The page title is not available until the view code associated with the page load + * or navigation has run and determined and set the title. In other words, it is + * asynchronous, and may take even up to a few seconds. To accommodate this fact, + * the script will monitor the page title and only set the GA data when the title + * has been stable for some period of time (TITLE_STABILIZATION_PERIOD). If navigation + * occurs before the title has stabilized, the GA data is sent anyway + * + * References: + * https://developers.google.com/tag-platform/gtagjs/reference + * https://developers.google.com/analytics/devguides/collection/gtagjs/single-page-applications#measure_virtual_pageviews + */ + +import { Account } from "lib/kb_lib/comm/coreServices/Auth"; + +type GTagStatus = 'WAITING' | 'PENDING' | 'SEND_NOW' | 'SENDING'; + +interface WindowWithGTag extends Window { + dataLayer: Array; +} + +const gtagWindow = window as unknown as WindowWithGTag; + +interface PageView { + page_location: string, + page_path: string, + page_title: string | null, + user_id?: string +}; + +class GTagSupport { + // Configuration + // These should be in a separate configuration, but, to be honest, some of these values have never changed, and others, + // like the Google tags, only every few years. + SESSION_COOKIE_NAME = 'kbase_session'; + PROD_UI_ORIGIN = 'https://narrative.kbase.us'; + PROD_SERVICE_ORIGIN = 'https://kbase.us'; + GOOGLE_GTAG = 'G-KXZCE6YQFZ'; + GOOGLE_AD_TAG = 'AW-753507180'; + + // The sending of the page hit is delayed until the page title stops change for the duration + // set here. + TITLE_STABILIZATION_PERIOD = 3000; + + // Keep the auth token cached for up to 10 minutes. + KBASE_AUTH_TOKEN_TTL = 600000; + KBASE_AUTH_INFO_EXPIRES_AT: number | null = null; + KBASE_AUTH_INFO: Account | null = null; + KBASE_AUTH_TOKEN: string | null = null; + + + + // Stores the current page title, which is really the last one recorded + // in the async loop below. + LAST_PAGE_TITLE: string | null = null; + + /** + * GTag processing states. + * + * Due to the asynchronous nature of loading page titles, and generally the fact that this code + * runs independently of the kbase-ui, which is of course asynchronous in nature, we use a + * little state model to manage this. + * + */ + + GTAG_STATE: GTagStatus = 'WAITING'; + + + + /** + * Sets a new status value, which should be one of WAITING, PENDING, SEND_NOW, or SENDING. + * + * @param {string} newState The new value for the gtag support status + */ + setState(newState: any) { + this.GTAG_STATE = newState; + } + + /** + * Simply compares the provided value to the current status value, returning whether they are the same. + * + * @param {string} state + * @returns {boolean} Whether the provided value is the same as the current status + */ + isState(state: string) { + return this.GTAG_STATE === state; + } + + /** + * Returns a promise that does not resolve until the given status is active. + * + * + * @param {*} status A status value, as defined above: WAITING, PENDING, SEND_NOW, SENDING + * @returns {Promise} A Promise with no important value + */ + // TODO there should be a timeout + async waitUntilStatus(status: GTagStatus, timeout: number) { + const started = Date.now(); + const timeoutAfter = started + timeout; + return new Promise((resolve) => { + const loop = () => { + if (Date.now() >= timeoutAfter) { + throw new Error(`Timed out after ${Date.now() - started}`); + } + setTimeout(() => { + if (this.isState(status)) { + loop(); + } + resolve(); + }, 100); + }; + loop(); + }); + } + + /** + * Returns the KBase service origin. + * + * In all but production, the service origin is the same as the document origin + * for the ui. In production, the ui origin is https://narrative.kbase.us but the + * service origin is https://kbase.us - this adjustment is made. + * + * @returns {string} The KBase service "origin" (protocol and hostname, e.g. https://ci.kbase.us) + */ + getServiceOrigin() { + if (document.location.origin === this.PROD_UI_ORIGIN) { + return this.PROD_SERVICE_ORIGIN; + } + return document.location.origin; + } + + /** + * The canonical function name for queuing messages for the Google gtag app. + * + * Note that the function relies up on the usage of the magic "arguments" variable + * provided to all functions. + * + * @returns {void} - Nothing + */ + pushGTag(_tag: string, _value: any, _options?: any) { + //developers.google.com/tag-platform/tag-manager/datalayer + gtagWindow.dataLayer.push(arguments); + } + + /** + * Get and return the current hash path as a path with a leading + * forward slash (/). + * + * Note that this code is could be a one-liner but for the fact that + * some usages of kbase-ui hash-paths place the search query string + * on the hash itself. + * + * @returns {string} - The current navigation path in kbase-ui + */ + getHashPath() { + const hash = document.location.hash; + let path; + if (hash) { + const m = hash.match(/#[/]?([^?]*)/); + if (m) { + path = m[1]; + } else { + path = ''; + } + } else { + path = ''; + } + return `/${path}`; + } + + /** + * Find KBase auth token from document cookie. + * + * If a token is not found in the cookie, null is returned. + * + * @returns {string|null} A KBase auth token, if any, stored in the browser. + */ + getToken() { + if (!document.cookie) { + return null; + } + const cookies = document.cookie.split(';').map((item) => { + return item.trim(); + }); + + for (const cookie of cookies) { + const [name, value] = cookie.split('='); + if (name === this.SESSION_COOKIE_NAME) { + return value; + } + } + return null; + } + + /** + * Returns the username associated with the given KBase auth token. + * + * If there is no token, or an error is encountered interacting with + * the auth service, null is returned. + * + * @param {string} token - KBase auth token + * @returns {string|null} - A kbase username + */ + async fetchAuth(token: string) { + if ( + this.KBASE_AUTH_INFO !== null && + this.KBASE_AUTH_INFO_EXPIRES_AT !== null && + this.KBASE_AUTH_INFO_EXPIRES_AT > Date.now() && + token === this.KBASE_AUTH_TOKEN + ) { + return this.KBASE_AUTH_INFO; + } + this.KBASE_AUTH_TOKEN = token; + const url = `${this.getServiceOrigin()}/services/auth/api/V2/me`; + try { + const response = await fetch(url, { + method: 'GET', + mode: 'cors', + headers: { + 'Content-Type': 'application/json', + Authorization: token, + }, + }); + if (response.status !== 200) { + console.warn('[gtagSupport] bad auth response, user not available', response); + return null; + } + this.KBASE_AUTH_INFO = await response.json(); + this.KBASE_AUTH_INFO_EXPIRES_AT = Date.now() + this.KBASE_AUTH_TOKEN_TTL; + return this.KBASE_AUTH_INFO; + } catch (error) { + console.error('[gtagSupport] error occurred in call to auth service', error); + return null; + } + } + + /** + * Get the username associated with the current KBase auth token. + * + * If there is no valid KBase auth token available, or an error is + * encountered with the auth service call, null is returned instead. + * + * + * @returns {string|null} username + */ + async getAuth() { + const token = this.getToken(); + if (token) { + try { + const auth = this.fetchAuth(token); + if (auth) { + return auth; + } + } catch (ex) { + console.error('[gtagSupport] Error fetching username', ex); + } + } + this.KBASE_AUTH_INFO = null; + this.KBASE_AUTH_TOKEN = null; + return null; + } + + /** + * + * + * @returns {void} - Nothing + */ + async sendGTag() { + this.setState('SENDING'); + + // The "path" for a view in the ui is determined by + // the url hash. This will change one day. + const path = this.getHashPath(); + + // Simply ensures that the data layer exists. The data layer + // is just an array placed into a "dataLayer" property on the global + // window. Nothing special about the name "dataLayer", other than that + // is where the google scripts expect to see + gtagWindow.dataLayer = gtagWindow.dataLayer || []; + + // See google docs for the api and field definitions. + // E.g. https://developers.google.com/analytics/devguides/collection/gtagjs/pages + // Queue up date for GA + const pageView: PageView = { + page_location: document.location.href, + page_path: path, + page_title: this.LAST_PAGE_TITLE, + }; + + const auth = await this.getAuth(); + if (auth) { + pageView.user_id = auth.anonid; + } + + this.pushGTag('event', 'page_view', pageView); + + this.setState('WAITING'); + } + + /** + * The ever-popular "sleep" function, which returns a promise that resolves with the given period of time elapsed. + * + * @param {number} until The amount of time, in milliseconds, after which the promise should resolve. + * @returns {Promise} + */ + async sleep(until: number) { + return new Promise((resolve) => { + window.setTimeout(() => { + resolve(); + }, until); + }); + } + + /** + * Ensures that if the next page hit is being sent, if the current one has not yet + * been sent, that it send immediately. + * + * Remember, that upon initiating a gtag request, we first wait for the page title + * to stabilize, and then the gtag event is registered. So if a new navigation event + * occurs during the waiting period, this function will be called in order to just + * send the gtag event even if the page title is not yet stabilized. + * + * @returns {Promise} + */ + async sendPendingGTag() { + if (this.isState('PENDING')) { + // Here we set the status to SEND_NOW, which causes the loop monitoring the + // page title to terminate, which returns a promise, and causes the gtag + // event to be pushed. + this.setState('SEND_NOW'); + + // And here we wait until the message is sent and we are back in the WAITING state. + // This should be sent very quickly, so a 2 second timeout should be plenty. + return await this.waitUntilStatus('WAITING', 2000); + } + } + + /** + * When called, will return a Promise that will only resolve when the page title + * has not changed for some amount of time. + * + * This amount of time is set at top of the file as TITLE_STABILIZATION_PERIOD), but + * should be on the order of 2-3 seconds. + * + * @returns {Promise} + */ + + async sendGTagAfterTitleSettles() { + this.setState('PENDING'); + + let lastChanged = Date.now(); + this.LAST_PAGE_TITLE = document.title; + + // This is where we "wait" for the page title to stabilize, + // i.e. stop changing. + const loop = async () => { + if (this.isState('SEND_NOW')) { + return; + } + const now = Date.now(); + if (now >= lastChanged + this.TITLE_STABILIZATION_PERIOD) { + return; + } + if (document.title !== this.LAST_PAGE_TITLE) { + lastChanged = Date.now(); + this.LAST_PAGE_TITLE = document.title; + } + await this.sleep(100); + await loop(); + }; + await loop(); + + // And ... finally we trigger the pushing up data for gtag. + this.sendGTag(); + + // This signals that the status of this little gtag engine + // is waiting for a navigation event. + this.setState('WAITING'); + } + + + + /** + * Sets up gtag support to a known state, and sets up some gtag data that + * will not change over this session. + */ + initialize() { + // Simply ensures that the data layer exists. + gtagWindow.dataLayer = gtagWindow.dataLayer || []; + this.setState('WAITING'); + this.pushGTag('js', new Date()); + this.pushGTag('config', this.GOOGLE_GTAG, { + send_page_view: false, + }); + this.pushGTag('config', this.GOOGLE_AD_TAG, { + send_page_view: false, + }); + + /** + * Send a ping to GA whenever the history state changes, i.e., url navigation + */ + window.onpopstate = async () => { + await this.sendPendingGTag(); + this.sendGTagAfterTitleSettles(); + }; + + this.sendGTagAfterTitleSettles() + } + + +} + +/** + * Initializes the state, and schedules the first page hit to be sent to GA. + * + * After this, navigation triggers the page hit. + */ +window.addEventListener('load', () => { + const gtagSupport = new GTagSupport(); + gtagSupport.initialize(); + // initialize(); + // sendGTagAfterTitleSettles(); +}); diff --git a/vite-app/src/lib/kb_lib/Auth2.ts b/vite-app/src/lib/kb_lib/Auth2.ts index 69b932c9..beb64123 100644 --- a/vite-app/src/lib/kb_lib/Auth2.ts +++ b/vite-app/src/lib/kb_lib/Auth2.ts @@ -132,6 +132,7 @@ export interface Account { local: boolean; roles: Array; user: string; + anonid: string; } export interface LoginCreateResponse { diff --git a/vite-app/src/lib/kb_lib/comm/coreServices/Auth.ts b/vite-app/src/lib/kb_lib/comm/coreServices/Auth.ts index a5af49d1..92ad895a 100644 --- a/vite-app/src/lib/kb_lib/comm/coreServices/Auth.ts +++ b/vite-app/src/lib/kb_lib/comm/coreServices/Auth.ts @@ -85,6 +85,7 @@ export interface Account { local: boolean; roles: Array; user: string; + anonid: string; } export default class AuthClient { url: string; diff --git a/vite-app/vite.config-og.ts b/vite-app/vite.config-og.ts new file mode 100644 index 00000000..a3c5357e --- /dev/null +++ b/vite-app/vite.config-og.ts @@ -0,0 +1,89 @@ +import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vitest/config'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), tsconfigPaths()], + base: './', + build: { + commonjsOptions: { + include: ['node_modules/**'], + }, + rollupOptions: { + output: { + experimentalMinChunkSize: 500_000, + manualChunks(id) { + if (id.includes('node_modules')) { + return 'vendor'; + } + } + } + } + }, + optimizeDeps: { + disabled: 'build' + }, + server: { + port: 3000, + host: '0.0.0.0', + proxy: { + '/services': { + target: 'https://ci.kbase.us', + changeOrigin: true, + secure: false, + }, + '/dynserv': { + target: 'https://ci.kbase.us', + changeOrigin: true, + secure: false + }, + '/modules/plugins': { + target: 'http://kbase-ui-deploy:80/plugins', + changeOrigin: true, + rewrite: (path: string) => { + return path.replace(/^\/modules\/plugins/, ''); + }, + configure: (proxy, options) => { + proxy.on('error', (error, request, response) => { + console.log('PROXY ERROR', error); + }); + proxy.on('proxyReq', (proxyRequest, request, response) => { + console.log('PROXY Request', request.method, request.url); + }); + proxy.on('proxyRes', (proxyResponse, request, response) => { + console.log('PROXY Response', proxyResponse.statusCode, request.url); + }); + } + }, + '/deploy/plugins': { + target: 'http://kbase-ui-deploy:80/plugins/', + changeOrigin: true, + rewrite: (path: string) => { + return path.replace(/^\/deploy\/plugins/, ''); + } + }, + '/plugins': { + target: 'http://kbase-ui-deploy:80', + changeOrigin: true, + }, + '/deploy': { + target: 'http://kbase-ui-deploy:80/', + changeOrigin: true, + }, + '/build': { + target: 'http://kbase-ui-deploy:80', + changeOrigin: true, + } + } + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './tests/setup.js', + coverage: { + provider: 'v8', + all: true + } + }, +}) diff --git a/vite-app/vite.config.ts b/vite-app/vite.config.ts index a3c5357e..4c885bbb 100644 --- a/vite-app/vite.config.ts +++ b/vite-app/vite.config.ts @@ -1,89 +1,173 @@ import react from '@vitejs/plugin-react'; +import { loadEnv } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; // https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react(), tsconfigPaths()], - base: './', - build: { - commonjsOptions: { - include: ['node_modules/**'], +export default defineConfig(({ command, mode }) => { + + const env = loadEnv(mode, process.cwd(), ''); + + // console.log('ENV', basePath); + + // const basePath = env.BASE_PATH;; + // const basePath = '/foo'; + const basePath = "./"; + + const basetPathForProxy = basePath === './' ? '' : basePath; + + const proxy = { + '/services': { + target: 'https://ci.kbase.us', + changeOrigin: true, + secure: false, }, - rollupOptions: { - output: { - experimentalMinChunkSize: 500_000, - manualChunks(id) { - if (id.includes('node_modules')) { - return 'vendor'; - } - } + '/dynserv': { + target: 'https://ci.kbase.us', + changeOrigin: true, + secure: false + }, + '/modules/plugins': { + target: 'http://kbase-ui-deploy:80/plugins', + changeOrigin: true, + rewrite: (path: string) => { + return path.replace(/^\/modules\/plugins/, ''); + }, + configure: (proxy, options) => { + proxy.on('error', (error, request, response) => { + console.log('PROXY ERROR', error); + }); + proxy.on('proxyReq', (proxyRequest, request, response) => { + console.log('PROXY Request', request.method, request.url); + }); + proxy.on('proxyRes', (proxyResponse, request, response) => { + console.log('PROXY Response', proxyResponse.statusCode, request.url); + }); } + }, + '/plugins': { + target: 'http://kbase-ui-deploy:80', + changeOrigin: true, + }, + }; + + // This is for plugins; kbase-ui uses just /plugins + proxy[`^${basePath}/modules/plugins/.*`] = { + target: 'http://kbase-ui-deploy:80/plugins', + changeOrigin: true, + rewrite: (path: string) => { + return path.replace(new RegExp(`^${basePath}/modules/plugins/`), ''); } - }, - optimizeDeps: { - disabled: 'build' - }, - server: { - port: 3000, - host: '0.0.0.0', - proxy: { - '/services': { - target: 'https://ci.kbase.us', - changeOrigin: true, - secure: false, - }, - '/dynserv': { - target: 'https://ci.kbase.us', - changeOrigin: true, - secure: false - }, - '/modules/plugins': { - target: 'http://kbase-ui-deploy:80/plugins', - changeOrigin: true, - rewrite: (path: string) => { - return path.replace(/^\/modules\/plugins/, ''); - }, - configure: (proxy, options) => { - proxy.on('error', (error, request, response) => { - console.log('PROXY ERROR', error); - }); - proxy.on('proxyReq', (proxyRequest, request, response) => { - console.log('PROXY Request', request.method, request.url); - }); - proxy.on('proxyRes', (proxyResponse, request, response) => { - console.log('PROXY Response', proxyResponse.statusCode, request.url); - }); - } + }; + + // This is a stopgap - some (most?) plugins use the hard-coded path + // /modules/plugins. + // proxy[`/modules/plugins/.*`] = { + // target: 'http://kbase-ui-deploy:80/plugins', + // changeOrigin: true, + // // rewrite: (path: string) => { + // // return path.replace(/^\/modules\/plugins/, ''); + // // }, + + // rewrite: (path: string) => { + // return path.replace(new RegExp(`/modules/plugins/`), ''); + // }, + // configure: (proxy, options) => { + // proxy.on('error', (error, request, response) => { + // console.log('PROXY ERROR', error); + // }); + // proxy.on('proxyReq', (proxyRequest, request, response) => { + // console.log('PROXY Request', request.method, request.url); + // }); + // proxy.on('proxyRes', (proxyResponse, request, response) => { + // console.log('PROXY Response', proxyResponse.statusCode, request.url); + // }); + // } + // }; + + + // proxy[`^${basePath}/plugins/.*`] = { + // target: 'http://kbase-ui-deploy:80/plugins/', + // changeOrigin: true, + // // rewrite: (path: string) => { + // // return path.replace(/^\/deploy\/plugins/, ''); + // // } + // rewrite: (path: string) => { + // return path.replace(new RegExp(`^${basePath}`), ''); + // }, + // }; + + proxy[`^${basetPathForProxy}/plugins/.*`] = { + target: 'http://kbase-ui-deploy:80', + changeOrigin: true, + rewrite: (path: string) => { + return path.replace(new RegExp(`^${basePath}`), ''); + }, + }; + + proxy[`^${basetPathForProxy}/deploy/.*`] = { + target: 'http://kbase-ui-deploy:80', + changeOrigin: true, + rewrite: (path: string) => { + return path.replace(new RegExp(`^${basePath}`), ''); + }, + }; + + + proxy[`^${basetPathForProxy}/build/.*`] = { + target: 'http://kbase-ui-deploy:80', + changeOrigin: true, + rewrite: (path: string) => { + return path.replace(new RegExp(`^${basePath}`), ''); + }, + }; + + // What uses this? + proxy[`^${basetPathForProxy}/deploy/plugins/.`] = { + target: 'http://kbase-ui-deploy:80', + changeOrigin: true, + rewrite: (path: string) => { + return path.replace(/^\/deploy\/plugins/, ''); + } + }; + + console.log('PROXY', proxy); + + return { + plugins: [react(), tsconfigPaths()], + // base: '/foo', + base: basePath, + build: { + commonjsOptions: { + include: ['node_modules/**'], }, - '/deploy/plugins': { - target: 'http://kbase-ui-deploy:80/plugins/', - changeOrigin: true, - rewrite: (path: string) => { - return path.replace(/^\/deploy\/plugins/, ''); + rollupOptions: { + output: { + experimentalMinChunkSize: 500_000, + manualChunks(id) { + if (id.includes('node_modules')) { + return 'vendor'; + } + } } - }, - '/plugins': { - target: 'http://kbase-ui-deploy:80', - changeOrigin: true, - }, - '/deploy': { - target: 'http://kbase-ui-deploy:80/', - changeOrigin: true, - }, - '/build': { - target: 'http://kbase-ui-deploy:80', - changeOrigin: true, } - } - }, - test: { - globals: true, - environment: 'jsdom', - setupFiles: './tests/setup.js', - coverage: { - provider: 'v8', - all: true - } - }, -}) + }, + optimizeDeps: { + disabled: 'build' + }, + server: { + port: 3000, + host: '0.0.0.0', + proxy + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './tests/setup.js', + coverage: { + provider: 'v8', + all: true + } + }, + } +}); From 0400fe4f6fe1eb8fe66d4511576d004dbae32474 Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 25 Oct 2023 19:30:00 +0000 Subject: [PATCH 5/6] minor dep update [CE-315] --- vite-app/package-lock.json | 8 ++++---- vite-app/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vite-app/package-lock.json b/vite-app/package-lock.json index c16f769a..182ba30d 100644 --- a/vite-app/package-lock.json +++ b/vite-app/package-lock.json @@ -30,7 +30,7 @@ "@testing-library/user-event": "14.5.1", "@types/dompurify": "3.0.4", "@types/jest": "29.5.6", - "@types/react": "18.2.31", + "@types/react": "18.2.32", "@types/react-dom": "18.2.14", "@types/react-router-dom": "5.3.3", "@types/uuid": "9.0.6", @@ -1874,9 +1874,9 @@ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { - "version": "18.2.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", - "integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", + "version": "18.2.32", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.32.tgz", + "integrity": "sha512-F0FVIZQ1x5Gxy/VYJb7XcWvCcHR28Sjwt1dXLspdIatfPq1MVACfnBDwKe6ANLxQ64riIJooXClpUR6oxTiepg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", diff --git a/vite-app/package.json b/vite-app/package.json index 0aed2193..62783427 100644 --- a/vite-app/package.json +++ b/vite-app/package.json @@ -33,7 +33,7 @@ "@testing-library/user-event": "14.5.1", "@types/dompurify": "3.0.4", "@types/jest": "29.5.6", - "@types/react": "18.2.31", + "@types/react": "18.2.32", "@types/react-dom": "18.2.14", "@types/react-router-dom": "5.3.3", "@types/uuid": "9.0.6", From eceeddfb3825bc303aed5f82f45a7ce056ca05cc Mon Sep 17 00:00:00 2001 From: Erik Pearson Date: Wed, 25 Oct 2023 19:33:31 +0000 Subject: [PATCH 6/6] local image, disable base path changes for local proxy [CE-315] --- Taskfile | 8 + scripts/host/build-image.sh | 1 + scripts/host/run-image.sh | 5 +- .../conf/nginx.conf-base-path-changes.tmpl | 497 ++++++++++++++++++ tools/proxy/contents/conf/nginx.conf.tmpl | 35 +- tools/proxy/docker-compose.yml | 2 + 6 files changed, 518 insertions(+), 30 deletions(-) mode change 100644 => 100755 scripts/host/build-image.sh mode change 100644 => 100755 scripts/host/run-image.sh create mode 100644 tools/proxy/contents/conf/nginx.conf-base-path-changes.tmpl diff --git a/Taskfile b/Taskfile index 519b6ba8..18767a63 100755 --- a/Taskfile +++ b/Taskfile @@ -125,6 +125,14 @@ function start-local-server { docker compose up } +function build-image { + ./scripts/host/build-image.sh +} + +function run-image { + ./scripts/host/run-image.sh +} + # # TO PORT # diff --git a/scripts/host/build-image.sh b/scripts/host/build-image.sh old mode 100644 new mode 100755 index 96596c06..7df5df53 --- a/scripts/host/build-image.sh +++ b/scripts/host/build-image.sh @@ -1 +1,2 @@ +#!/usr/bin/env bash docker build . -t kbase/kbase-ui:dev \ No newline at end of file diff --git a/scripts/host/run-image.sh b/scripts/host/run-image.sh old mode 100644 new mode 100755 index 61abfb49..fc18df9a --- a/scripts/host/run-image.sh +++ b/scripts/host/run-image.sh @@ -1,9 +1,12 @@ +#!/usr/bin/env bash + docker network create kbase-dev -# -v `pwd`/build/app/modules/plugins/auth2-client:/kb/deployment/services/kbase-ui/dist/modules/plugins/auth2-client \ + export ENV=ci echo echo "Running kbase-ui production image in development, deploy env is '${ENV}'" echo + docker run \ -v `pwd`/dev/gitlab-config:/kb/deployment/config \ --network kbase-dev \ diff --git a/tools/proxy/contents/conf/nginx.conf-base-path-changes.tmpl b/tools/proxy/contents/conf/nginx.conf-base-path-changes.tmpl new file mode 100644 index 00000000..41782c03 --- /dev/null +++ b/tools/proxy/contents/conf/nginx.conf-base-path-changes.tmpl @@ -0,0 +1,497 @@ +# +# A minimal proxying configuration for running kbase-ui through a secure proxy +# against a KBase deploy environment. +# +# Note that lower case environment variables are taken from conf/*.env, where * is the +# deploy environment, taken from DEPLOY_ENV. + +daemon off; +error_log /dev/stdout info; +worker_processes auto; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + types_hash_max_size 2048; + proxy_headers_hash_bucket_size 256; + proxy_ssl_server_name on; + proxy_request_buffering off; + proxy_buffering off; + proxy_http_version 1.1; + + # In addition to the desired file size limit (in bytes), + # we ad smidgen more, as the multipart/form mime structure + # includes the destPath as one part, the file as another, + # and each has a small header section. + # + # In practice, this appears to be about 300 bytes plus + # the value for destPath and the file name. + # And the mime boundary size will vary as well. + # + # So let's call it 1K and then call it a day. + # So, the max body size should be MAX_FILE_SIZE + 1000. + + # For testing, use a 1K file size, 1K extra. + # Or set as you wish to push boundaries. + # client_max_body_size 2000; + + # For production, 5G max file size plus 1KB extra. + # client_max_body_size 5000001000; + + keepalive_requests 0; + # keepalive_timeout 65; + keepalive_timeout 0; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + + # Define upstream servers, for convenience. + upstream kbase_ui { + # Use 80 for image; 3000 for devcontainer + # server {{ .Env.kbase_ui_host }}:80; + server {{ .Env.kbase_ui_host }}:3000; + # allows the proxy to start up even though the + # dev server may not be running yet + server {{ .Env.kbase_ui_host }}:80 backup; + keepalive 16; + } + + # upstream services { + # server {{ .Env.deploy_hostname }}; + # keepalive 16; + # } + + # upstream dynamic_services { + # server {{ .Env.deploy_hostname }}; + # keepalive 16; + # } + + upstream narrative { + {{ if .Env.local_narrative }} + server narrative:8888; + {{ else }} + {{ if .Env.deploy_ui_hostname }} + server {{ .Env.deploy_ui_hostname }}; + {{ else }} + server {{ .Env.deploy_hostname }}; + {{ end }} + {{ end }} + } + + upstream kbase_navigator { + {{ if .Env.local_navigator }} + server navigator:5000; + {{ else }} + {{ if .Env.deploy_ui_hostname }} + server {{ .Env.deploy_ui_hostname }}; + {{ else }} + server {{ .Env.deploy_hostname }}; + {{ end }} + {{ end }} + } + + log_format upstream_log '[$time_local] $remote_addr - $remote_user - $server_name to: $upstream_addr: $request upstream_response_time $upstream_response_time proxy_host $proxy_host upstream_status $upstream_status upstream_response_length $upstream_response_length upstream_http_location $upstream_http_location msec $msec request_time $request_time'; + + + # Logging Settings + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log debug; + + # Route insecure requests to secure. + server { + listen 80 default_server; + listen [::]:80 default_server; + server_name {{ default .Env.deploy_hostname .Env.deploy_ui_hostname }}; + return 301 https://{{ default .Env.deploy_hostname .Env.deploy_ui_hostname }}$request_uri; + } + + + # If there is a separate ui hostname provided in the configuration, + # we need to listen for this separately. + # The only real usage of this is for production, in which kbase-ui and + # the narrative operate at narrative.kbase.us, but services at kbase.us. + {{ if .Env.deploy_ui_hostname }} + + # server { + # server_name {{ .Env.deploy_ui_hostname }}; + # listen 3000 ssl; + # ssl_certificate /kb/deployment/ssl/test.crt; + # ssl_certificate_key /kb/deployment/ssl/test.key; + # ssl_protocols TLSv1.2 TLSv1.1 TLSv1; + # location /ws/ { + # client_max_body_size 300M; + # proxy_connect_timeout 10s; + # proxy_pass http://kbase_ui/ws/; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "Upgrade"; + # proxy_set_header Host {{ .Env.deploy_ui_hostname }}; + # } + # } + + server { + listen 443 ssl; + server_name {{ .Env.deploy_ui_hostname }}; + ssl_certificate /kb/deployment/ssl/test.crt; + ssl_certificate_key /kb/deployment/ssl/test.key; + ssl_protocols TLSv1.2 TLSv1.1 TLSv1; + + location @request_entity_too_large { + default_type text/plain; + return 413 "Request entity is too large (> 5GB)"; + } + + location /__ping__ { + default_type "text/html"; + return 204; + } + + location /data/__perf__ { + gzip off; + gunzip on; + add_header Cache-Control 'no-cache, no-transform'; + proxy_pass http://kbase_ui; + } + + # Needed for running narratives + location /narrative/ { + access_log /var/log/nginx/narrative.log upstream_log; + + include /etc/nginx/cors.conf; + {{ if .Env.local_narrative }} + proxy_pass http://narrative/narrative/; + {{ else }} + {{ if .Env.deploy_ui_hostname }} + proxy_pass https://{{ .Env.deploy_ui_hostname }}/narrative/; + {{ else }} + proxy_pass https://{{ .Env.deploy_hostname }}/narrative/; + {{ end }} + # proxy_pass https://narrative/narrative/; + {{ end }} + + proxy_connect_timeout 10s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Origin https://{{ .Env.deploy_ui_hostname }}; + proxy_set_header Host {{ .Env.deploy_ui_hostname }}; + } + + location /ui-assets/ { + proxy_pass https://{{ .Env.deploy_ui_hostname }}; + proxy_set_header Connection ""; + } + + location /sn/ { + proxy_pass https://{{ .Env.deploy_ui_hostname }}; + proxy_set_header Connection ""; + } + + location /narratives/ { + # {{ if .Env.local_navigator }} + # proxy_pass http://navigator/narratives/; + # {{ else }} + # proxy_pass https://kbase_navigator/narratives/; + # {{ end }} + + {{ if .Env.deploy_ui_hostname }} + proxy_pass https://{{ .Env.deploy_ui_hostname }}/narratives/; + {{ else }} + proxy_pass https://{{ .Env.deploy_hostname }}/narratives/; + {{ end }} + + access_log /var/log/nginx/navigator.log upstream_log; + + proxy_set_header Connection ""; + proxy_read_timeout 10m; + proxy_set_header Origin https://{{ .Env.deploy_ui_hostname }}; + proxy_set_header Host {{ .Env.deploy_ui_hostname }}; + } + + location /navigator/ { + # {{ if .Env.local_navigator }} + # proxy_pass http://navigator/narratives/; + # {{ else }} + # proxy_pass https://kbase_navigator/narratives/; + # {{ end }} + + {{ if .Env.deploy_ui_hostname }} + proxy_pass https://{{ .Env.deploy_ui_hostname }}/narratives/; + {{ else }} + proxy_pass https://{{ .Env.deploy_hostname }}/narratives/; + {{ end }} + + access_log /var/log/nginx/navigator.log upstream_log; + + proxy_set_header Connection ""; + proxy_read_timeout 10m; + proxy_set_header Origin https://{{ .Env.deploy_ui_hostname }}; + proxy_set_header Host {{ .Env.deploy_ui_hostname }}; + } + + location {{ .Env.base_path }} { + proxy_pass http://kbase_ui; + #proxy_set_header Cache-Control 'no-store'; + #proxy_set_header Cache-Control 'no-cache'; + #expires 0; + # proxy_set_header Connection ""; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + } + + {{ end }} + + # server { + # server_name {{ .Env.deploy_hostname }}; + # listen 3000 ssl; + # ssl_certificate /kb/deployment/ssl/test.crt; + # ssl_certificate_key /kb/deployment/ssl/test.key; + # ssl_protocols TLSv1.2 TLSv1.1 TLSv1; + # proxy_http_version 1.1; + # location /ws/ { + # proxy_connect_timeout 10s; + # proxy_pass http://kbase_ui/ws/; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "Upgrade"; + # proxy_set_header Host {{ .Env.deploy_hostname }}; + # } + # } + + server { + server_name {{ .Env.deploy_hostname }}; + listen 443 ssl; + ssl_certificate /kb/deployment/ssl/test.crt; + ssl_certificate_key /kb/deployment/ssl/test.key; + ssl_protocols TLSv1.2 TLSv1.1 TLSv1; + + location @request_entity_too_large { + default_type application/json; + # return 413 "{\"message\": \"Request entity is too large\", \"responseCode\": 413, \"maxBodySize\": \"5GB\", \"contentLength\": ${content_length}}"; + return 413 "{\"message\": \"Request entity is too large\", \"responseCode\": 413, \"maxBodySize\": \"2KB\", \"contentLength\": ${content_length}}"; + } + + keepalive_requests 0; + keepalive_timeout 0; + proxy_socket_keepalive off; + + # Service proxying + # specified as a list of service modules + # note should put the service module name in /etc/hosts and map to localhost + {{ if .Env.service_proxies }} + {{ range splitList " " .Env.service_proxies }} + + {{ if eq . "searchapi2" }} + location /services/searchapi2/legacy { + {{ if $.Env.deploy_ui_hostname }} + include /etc/nginx/cors.conf; + {{ end }} + proxy_pass http://searchapi2:5000/legacy; + proxy_set_header Connection ""; + } + location /services/searchapi2/rpc { + {{ if $.Env.deploy_ui_hostname }} + include /etc/nginx/cors.conf; + {{ end }} + proxy_pass http://searchapi2:5000/rpc; + proxy_set_header Connection ""; + } + {{ else if eq . "staging_service" }} + location /services/{{ . }}/ { + + # For testing, use a 1K file size, 1K extra. + # Or set as you wish to push boundaries. + client_max_body_size 2000; + + # For production, 5G max file size plus 1KB extra. + # client_max_body_size 5000001000; + + error_page 413 @request_entity_too_large; + {{ if $.Env.deploy_ui_hostname }} + include /etc/nginx/cors.conf; + {{ end }} + proxy_pass http://{{ . }}:3000/; + proxy_set_header Connection ""; + proxy_read_timeout 10m; + } + {{ else }} + location /services/{{ . }}/ { + {{ if $.Env.deploy_ui_hostname }} + include /etc/nginx/cors.conf; + {{ end }} + proxy_pass http://{{ . }}:5000/; + proxy_set_header Connection ""; + proxy_read_timeout 10m; + } + location /services/{{ . }} { + {{ if $.Env.deploy_ui_hostname }} + include /etc/nginx/cors.conf; + {{ end }} + proxy_pass http://{{ . }}:5000; + proxy_set_header Connection ""; + proxy_read_timeout 10m; + } + {{ end }} + + {{ end }} + {{ end }} + + + # Proxy all service calls, including auth2, to the real CI + location /services { + access_log /var/log/nginx/services_proxy.log upstream_log; + + # The cookie path rewriting is just for auth2 + {{ if .Env.deploy_ui_hostname }} + include /etc/nginx/cors.conf; + {{ end }} + proxy_cookie_path /login /services/auth/login; + proxy_cookie_path /link /services/auth/link; + proxy_pass https://{{ .Env.deploy_hostname }}/services; + proxy_set_header Connection ""; + proxy_read_timeout 10m; + } + + # Dynamic Service proxying + # If dynamic services are provided in the configuration, we will proxy requests + # to them to local instances. + # This works by trapping calls to /dynserv/XXX.Module + # where /dynserv/ is always the path prefix for dynamic service call urls provided by + # the service wizard + # where XXX is a random-appearing string component provided by the service wizard + # and Module is the dynamic service module name. + + {{ if .Env.dynamic_service_proxies }} + {{ range splitList " " .Env.dynamic_service_proxies }} + + # note that the elements of the list must match the service path used in the ui call, + # and also the hostname assigned to the docker container. + # SO this means that probably service entries which are more complicated than simple strings + # without punctuation will work, but if they contain a / or something, maybe not. + + location ~ ^/dynserv/[^.]+[.]{{ . }}.*$ { + {{ if $.Env.deploy_ui_hostname }} + include /etc/nginx/cors.conf; + {{ end }} + + # This handles plain dynamic service calls, which don't have any path following the module name + rewrite ^/dynserv/[^.]+[.]{{ . }}$ / break; + + # This handles calls into the dynamic service with the path is propagated. + rewrite ^/dynserv/[^.]+[.]{{ . }}(.*)$ $1 break; + proxy_pass http://{{ . }}:5000; + proxy_read_timeout 10m; + } + + {{ end }} + {{ end }} + + # Un-trapped dynamic service calls are routed to the real dynamic service + # endpoints. + location /dynserv { + {{ if .Env.deploy_ui_hostname }} + include /etc/nginx/cors.conf; + {{ end }} + proxy_pass https://{{ .Env.deploy_hostname }}/dynserv; + proxy_set_header Connection ""; + proxy_read_timeout 10m; + } + + # Repeat the ui proxying for the case in which they reside on the same + # host name as services. + {{ if not .Env.deploy_ui_hostname }} + + location /__ping__ { + default_type "text/html"; + return 204; + } + + location /narrative/ { + {{ if .Env.local_narrative }} + proxy_pass http://narrative/narrative/; + {{ else }} + {{ if .Env.deploy_ui_hostname }} + proxy_pass https://{{ .Env.deploy_ui_hostname }}/narrative/; + {{ else }} + proxy_pass https://{{ .Env.deploy_hostname }}/narrative/; + {{ end }} + # proxy_pass https://narrative/narrative/; + {{ end }} + + access_log /var/log/nginx/narrative.log upstream_log; + + proxy_connect_timeout 10s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Origin https://{{ .Env.deploy_hostname }}; + proxy_set_header Host {{ .Env.deploy_hostname }}; + } + + location /ui-assets/ { + proxy_pass https://{{ .Env.deploy_hostname }}; + proxy_set_header Connection ""; + } + + location /sn/ { + proxy_pass https://{{ .Env.deploy_hostname }}; + proxy_set_header Connection ""; + } + + location /n/ { + proxy_pass https://{{ .Env.deploy_hostname }}; + proxy_set_header Connection ""; + } + + location /narratives/ { + # {{ if .Env.local_navigator }} + # proxy_pass http://navigator/narratives/; + # {{ else }} + # proxy_pass https://kbase_navigator/narratives/; + # {{ end }} + {{ if .Env.deploy_ui_hostname }} + proxy_pass https://{{ .Env.deploy_ui_hostname }}/narratives/; + {{ else }} + proxy_pass https://{{ .Env.deploy_hostname }}/narratives/; + {{ end }} + + access_log /var/log/nginx/navigator.log upstream_log; + + proxy_set_header Connection ""; + proxy_read_timeout 10m; + proxy_set_header Origin https://{{ .Env.deploy_hostname }}; + proxy_set_header Host {{ .Env.deploy_hostname }}; + } + + # location /vite_hms { + # proxy_pass http://{{ .Env.deploy_hostname }}:3001/vite_hms; + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "Upgrade"; + # proxy_set_header Host {{ .Env.deploy_hostname }}; + # } + + location {{ .Env.base_path }} { + proxy_pass http://kbase_ui; + proxy_set_header Connection ""; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + {{ end }} + } +} \ No newline at end of file diff --git a/tools/proxy/contents/conf/nginx.conf.tmpl b/tools/proxy/contents/conf/nginx.conf.tmpl index 1bfe9664..41782c03 100644 --- a/tools/proxy/contents/conf/nginx.conf.tmpl +++ b/tools/proxy/contents/conf/nginx.conf.tmpl @@ -153,12 +153,12 @@ http { return 413 "Request entity is too large (> 5GB)"; } - location {{ .Env.BASE_PATH }}/__ping__ { + location /__ping__ { default_type "text/html"; return 204; } - location {{ .Env.BASE_PATH }}/data/__perf__ { + location /data/__perf__ { gzip off; gunzip on; add_header Cache-Control 'no-cache, no-transform'; @@ -226,7 +226,7 @@ http { # proxy_pass https://kbase_navigator/narratives/; # {{ end }} - {{ if .Env.deploy_ui_hostname }} + {{ if .Env.deploy_ui_hostname }} proxy_pass https://{{ .Env.deploy_ui_hostname }}/narratives/; {{ else }} proxy_pass https://{{ .Env.deploy_hostname }}/narratives/; @@ -240,21 +240,7 @@ http { proxy_set_header Host {{ .Env.deploy_ui_hostname }}; } - location {{ .Env.BASE_PATH }} { - proxy_pass http://kbase_ui; - #proxy_set_header Cache-Control 'no-store'; - #proxy_set_header Cache-Control 'no-cache'; - #expires 0; - # proxy_set_header Connection ""; - # proxy_set_header Upgrade $http_upgrade; - # proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } - - # This one to handle legacy plugin /modules/plugins paths in dev - location / { + location {{ .Env.base_path }} { proxy_pass http://kbase_ui; #proxy_set_header Cache-Control 'no-store'; #proxy_set_header Cache-Control 'no-cache'; @@ -429,7 +415,7 @@ http { # host name as services. {{ if not .Env.deploy_ui_hostname }} - location {{ .Env.BASE_PATH }}/__ping__ { + location /__ping__ { default_type "text/html"; return 204; } @@ -498,16 +484,7 @@ http { # proxy_set_header Host {{ .Env.deploy_hostname }}; # } - location {{ .Env.BASE_PATH }} { - proxy_pass http://kbase_ui; - proxy_set_header Connection ""; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - } - - # legacy plugins - location / { + location {{ .Env.base_path }} { proxy_pass http://kbase_ui; proxy_set_header Connection ""; proxy_http_version 1.1; diff --git a/tools/proxy/docker-compose.yml b/tools/proxy/docker-compose.yml index 0edea586..b302c517 100644 --- a/tools/proxy/docker-compose.yml +++ b/tools/proxy/docker-compose.yml @@ -15,5 +15,7 @@ services: - '80:80' - '443:443' dns: 8.8.8.8 + environment: + - BASE_PATH=${BASE_PATH} # note that this is really a docker env file and is relative to the docker-compose file env_file: ./conf/${DEPLOY_ENV}.env