diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a02fe0..467789ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v4.0.0 (2025-04-29) + +* Remove Earthdata login button and user orders page. + # v3.1.0 (2025-04-15) * Updates to cloud collections must have Harmony services available to be diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 500dd52a..c4db7546 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -35,12 +35,6 @@ so accept the risk and continue if your web browser blocks the request. > get the API URL from a drupal-provided variable to support injection of the > current URL in non-NSIDC hosted DAT (when we move to earthdata landing pages) -> [!WARNING] -> The advice below does not work for the EDL login interaction on a local -> machine. Develop against a drupal VM for testing all interactions (see below) -You also need to run a proxy to Hermes: npx http-server -p 3000 -P https://nsidc.org/apps/orders/api -See the config in webpack.config.cjs. Would like to proxy directly to Hermes URL, but couldn't get that to work. - ## Developer VM (no Drupal) $ npm run build:dev # Build with source maps for development environment, and development diff --git a/README.md b/README.md index c810c919..a80dbdfd 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,4 @@ given data collection. ## Development notes -See [`DEVELOPMENT.md`](https://bitbucket.org/nsidc/everest-ui/src/master/DEVELOPMENT.md). +See [`DEVELOPMENT.md`](./DEVELOPMENT.md). diff --git a/package-lock.json b/package-lock.json index a6d82b79..317da346 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@nsidc/data-access-tools", - "version": "3.1.0", + "version": "4.0.0-alpha.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 36d86a46..8f5dd52e 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "@nsidc/data-access-tools", - "version": "3.1.0", - "description": "UI to order NASA data and track order history.", - "repository": "https://bitbucket.org/nsidc/everest-ui", + "version": "4.0.0-alpha.1", + "description": "UI to order NASA data.", + "repository": "https://github.com/nsidc/data-access-tool-ui", "files": [ "dist", "CHANGELOG", diff --git a/public/index.html b/public/index.html index 2e56cbaa..f5a78b8a 100644 --- a/public/index.html +++ b/public/index.html @@ -8,7 +8,6 @@ diff --git a/public/order-history.html b/public/order-history.html deleted file mode 100644 index dc904978..00000000 --- a/public/order-history.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Order History (template) - -
-
- - diff --git a/src/components/EDLButton.tsx b/src/components/EDLButton.tsx deleted file mode 100644 index c7660775..00000000 --- a/src/components/EDLButton.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import * as jQuery from "jquery"; -import * as React from "react"; - -import { isLoggedInUser, isLoggedOutUser } from "../types/User"; -import { IEnvironment } from "../utils/environment"; -import { UserContext } from "../utils/state"; -import { LoadingIcon } from "./LoadingIcon"; - -import "../styles/eui_buttons.less"; - -interface IEDLButtonProps { - environment: IEnvironment; -} - -export class EDLButton extends React.Component { - public static contextType = UserContext; - - public render() { - let button; - - if (isLoggedInUser(this.context.user)) { - button = ( - - ); - } else if (isLoggedOutUser(this.context.user)) { - // Change from form to fetch? Here we actually want to redirect the user. - const loginUrl = `${this.props.environment.urls.hermesApiUrl}/earthdata/auth/`; - button = ( -
-
- -
-
- ); - } else { - // Unknown login state - button = ( - - ); - } - - return ( -
- {button} -
- ); - } - - public LogoutButton = () => { - const logout = () => { - this.props.environment.hermesAPI.logoutUser() - .then((s: any) => { - if (s.status === 200) { - return this.context.updateUser(this); - } else { - throw(Error("Error communicating with hermes-api when attempting to logout")); - } - }); - }; - - const fullName = `${this.context.user.first_name} ${this.context.user.last_name}`; - - return ( -
- - - - - - - -
- ); - } - - public toggleDropdown(event: any) { - // Simply loading the earthdata UI javascript doesn't properly register - // all event listeners because the elements that need listeners - // registered wouldn't be mounted yet. This code is copied from: - // https://cdn.earthdata.nasa.gov/eui/1.1.7/js/eui.js - event.stopPropagation(); - jQuery(".button-group--dropdown").slideToggle("fast"); - } - - public componentDidMount() { - // Simply loading the earthdata UI javascript doesn't properly register - // all event listeners because the elements that need listeners - // registered wouldn't be mounted yet. This code is copied from: - // https://cdn.earthdata.nasa.gov/eui/1.1.7/js/eui.js - jQuery(() => { - jQuery(document).click((e: any) => { - const targetIds = [e.target.id, e.target.parentElement.id]; - const undesiredTargetId = "toggle-button-group"; - if (!targetIds.includes(undesiredTargetId)) { - jQuery(".button-group--dropdown").hide(); - } - }); - jQuery(document).keyup((e: any) => { - const escapeCode = 27; - if (e.keyCode === escapeCode) { - jQuery(".button-group--dropdown").hide(); - } - }); - }); - } -} diff --git a/src/components/EverestProfile.tsx b/src/components/EverestProfile.tsx deleted file mode 100644 index 800abae6..00000000 --- a/src/components/EverestProfile.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import * as moment from "moment"; -import * as React from "react"; - -import { EverestUser, EverestUserUnknownStatus, isLoggedInUser } from "../types/User"; -import { IEnvironment } from "../utils/environment"; -import { hasChanged } from "../utils/hasChanged"; -import { updateUser, UserContext } from "../utils/state"; -import { EDLButton } from "./EDLButton"; -import { OrderDetails } from "./OrderDetails"; -import { OrderList } from "./OrderList"; - -interface IEverestProps { - environment: IEnvironment; -} - -interface IEverestProfileState { - initialLoadComplete: boolean; - orderList: object[]; - selectedOrder?: string; - user: EverestUser; -} - -export class EverestProfile extends React.Component { - public constructor(props: any) { - super(props); - - this.state = { - initialLoadComplete: false, - orderList: [], - selectedOrder: undefined, - user: EverestUserUnknownStatus, - }; - } - - public shouldComponentUpdate(nextProps: IEverestProps, nextState: IEverestProfileState) { - const propsChanged = hasChanged(this.props, nextProps, ["environment"]); - const stateChanged = hasChanged(this.state, nextState, ["initialLoadComplete", - "orderList", - "selectedOrder", - "user"]); - - return propsChanged || stateChanged; - } - - public componentDidMount() { - updateUser(this); - } - - public componentDidUpdate(prevProps: IEverestProps, prevState: IEverestProfileState) { - if (isLoggedInUser(this.state.user) && !isLoggedInUser(prevState.user)) { - this.props.environment.hermesAPI.openNotificationConnection(this.state.user, this.handleNotification); - this.updateOrderList(); - } - } - - public render() { - const userHasNoOrders = this.state.orderList.length === 0; - - let ordersView; - - if (!isLoggedInUser(this.state.user)) { - ordersView = ( -
{"You must be logged in to view your orders."}
- ); - } else if (userHasNoOrders) { - ordersView = ( -
{"You have no orders."}
- ); - } else { - ordersView = ( -
- - -
- ); - } - - const className = this.props.environment.inDrupal ? "in-drupal" : "standalone"; - - return ( - updateUser(this)}} > -
- - {ordersView} -
-
- ); - } - - private handleNotification = (event: any) => { - this.updateOrderList(); - } - - private updateOrderList = () => { - if (this.state.user) { - this.props.environment.hermesAPI.getUserOrders(this.state.user) - .then((orders: any) => Object.values(orders).sort((a: any, b: any) => { - return moment(b.submitted_timestamp).diff(moment(a.submitted_timestamp)); - })) - .then((orderList: any) => { - this.setState({ orderList, initialLoadComplete: true }); - }); - } - } - - private handleOrderSelection = (orderId: string) => { - this.setState({ - selectedOrder: orderId, - }); - } -} diff --git a/src/components/EverestUI.tsx b/src/components/EverestUI.tsx index 2a4f6ce5..026a856e 100644 --- a/src/components/EverestUI.tsx +++ b/src/components/EverestUI.tsx @@ -8,17 +8,15 @@ import { CmrGranule } from "../types/CmrGranule"; import { IDrupalDataset } from "../types/DrupalDataset"; import { GranuleSorting, IOrderParameters, OrderParameters } from "../types/OrderParameters"; import { OrderSubmissionParameters } from "../types/OrderSubmissionParameters"; -import { EverestUser, EverestUserUnknownStatus } from "../types/User"; import { CMR_COUNT_HEADER, cmrBoxArrToSpatialSelection, cmrCollectionRequest, cmrGranuleRequest, cmrStatusRequest } from "../utils/CMR"; import { IEnvironment } from "../utils/environment"; import { hasChanged } from "../utils/hasChanged"; import { mergeOrderParameters } from "../utils/orderParameters"; -import { updateStateInitGranules, updateUser, UserContext } from "../utils/state"; +import { updateStateInitGranules } from "../utils/state"; import { CmrDownBanner } from "./CmrDownBanner"; import { CollectionDropdown } from "./CollectionDropdown"; -import { EDLButton } from "./EDLButton"; import { GranuleList } from "./GranuleList"; import { OrderButtons } from "./OrderButtons"; import { OrderParameterInputs } from "./OrderParameterInputs"; @@ -44,7 +42,6 @@ export interface IEverestState { orderSubmissionParameters?: OrderSubmissionParameters; stateCanBeFrozen: boolean; totalSize: number; - user: EverestUser; } export class EverestUI extends React.Component { @@ -75,7 +72,6 @@ export class EverestUI extends React.Component { orderSubmissionParameters: undefined, stateCanBeFrozen: false, totalSize: 0, - user: EverestUserUnknownStatus, }; // allow easy testing of CMR errors by creating functions that can be called @@ -105,8 +101,6 @@ export class EverestUI extends React.Component { if (this.props.environment.inDrupal && this.props.environment.drupalDataset) { this.initStateFromCollectionDefaults(this.props.environment.drupalDataset); } - - updateUser(this); } public shouldComponentUpdate(nextProps: IEverestProps, nextState: IEverestState) { @@ -122,7 +116,6 @@ export class EverestUI extends React.Component { "orderParameters", "orderSubmissionParameters", "totalSize", - "user", ]); return propsChanged || stateChanged; @@ -154,8 +147,6 @@ export class EverestUI extends React.Component {
{collectionDropdown}
-
columnContainer = n}> {
); - return ( - updateUser(this)}} > - {appJSX} - - ); + return appJSX; } private cmrStatusRequestUntilOK = () => { diff --git a/src/components/ModalContent/OrderSubmitResponse.tsx b/src/components/ModalContent/OrderSubmitResponse.tsx deleted file mode 100644 index 5114e940..00000000 --- a/src/components/ModalContent/OrderSubmitResponse.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react"; - -import { IEnvironment } from "../../utils/environment"; - -interface IOrderErrorContentProps { - error: any; - onOK: () => void; -} - -export const OrderErrorContent = (props: IOrderErrorContentProps) => { - console.error(props.error); - - const msg = "We're sorry, but there was an error processing your request. " + - "Please contact User Services (nsidc@nsidc.org) for further information and assistance. " + - "User Services operates from 9:00 a.m. to 5 p.m. (MT)."; - return ( -
-

Something went wrong!

-

{msg}

- -
- ); -}; - -interface IOrderSuccessContentProps { - environment: IEnvironment; - onOK: () => void; - response: any; -} - -export const OrderSuccessContent = (props: IOrderSuccessContentProps) => { - const orderId = props.response.order.order_id; - return ( -
-

Order Received

-

- Your download order has been submitted. You may view the status of your - order ({orderId}) on the Orders page. -

- -
- ); -}; - -(OrderErrorContent as React.SFC).displayName = "OrderErrorContent"; -(OrderSuccessContent as React.SFC).displayName = "OrderSuccessContent"; diff --git a/src/components/OrderButtons.tsx b/src/components/OrderButtons.tsx index 234aaa7b..e4a05743 100644 --- a/src/components/OrderButtons.tsx +++ b/src/components/OrderButtons.tsx @@ -6,7 +6,6 @@ import { OrderSubmissionParameters } from "../types/OrderSubmissionParameters"; import { boundingBoxMatch, combineGranuleFilters } from "../utils/CMR"; import { IEnvironment } from "../utils/environment"; import { hasChanged } from "../utils/hasChanged"; -import { UserContext } from "../utils/state"; import { EdscFlow } from "./EdscFlow"; import { EddFlow } from "./EddFlow"; import { ScriptButton } from "./ScriptButton"; @@ -27,7 +26,6 @@ interface IOrderButtonsState { } export class OrderButtons extends React.Component { - public static contextType = UserContext; public constructor(props: IOrderButtonsProps) { super(props); diff --git a/src/components/OrderDetails.tsx b/src/components/OrderDetails.tsx deleted file mode 100644 index 79330d05..00000000 --- a/src/components/OrderDetails.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import * as moment from "moment"; -import * as React from "react"; - -import { IEnvironment } from "../utils/environment"; -import { hasChanged } from "../utils/hasChanged"; -import { orderExpirationTimestamp } from "../utils/misc"; -import { UserContext } from "../utils/state"; -import { LoadingIcon } from "./LoadingIcon"; -import { getOrderStatus } from "./OrderListItem"; - -export interface IOrderDetailsProps { - environment: IEnvironment; - initialLoadComplete?: boolean; - orderId?: string; -} - -export interface IOrderDetailsState { - loading: boolean; - order?: any; -} - -export class OrderDetails extends React.Component { - public static contextType = UserContext; - private static timeFormat = "l LT"; - - public constructor(props: any) { - super(props); - - this.state = { - loading: false, - order: undefined, - }; - } - - public shouldComponentUpdate(nextProps: IOrderDetailsProps, nextState: IOrderDetailsState) { - const stateChanged = hasChanged(this.state, nextState, ["order", "loading"]); - const propsChanged = hasChanged(this.props, nextProps, ["initialLoadComplete", "orderId"]); - return stateChanged || propsChanged; - } - - public render() { - const loading = !this.props.initialLoadComplete || this.state.loading; - const noOrderSelected = !this.state.order; - - if (loading) { - return ( -
- ); - } else if (noOrderSelected) { - return ( -
{"Select an order from the list above to see details."}
- ); - } else { - const order: any = this.state.order; - - let links = null; - // Show a mock expiration timestamp based on completed timestamp - const expirationTimestamp = orderExpirationTimestamp(order); - if (expirationTimestamp && expirationTimestamp.isBefore(moment.now())) { - links = ( -
- Expired: {expirationTimestamp.format(OrderDetails.timeFormat)} -
- ); - } else { - const dataLinks = this.buildDataLinks(order); - const zipLinks = this.buildZipLinks(order); - const textFileLinks: any = this.buildTextFileLink(order); - const expirationJSX = expirationTimestamp ? - (
Expires: {expirationTimestamp.format(OrderDetails.timeFormat)}
) : - null; - links = ( -
- {expirationJSX} -
Zip links: (download may take a moment to start) -
    {zipLinks}
-
-
File list: -
    {textFileLinks}
-
-
File links: -
    {dataLinks}
-
-
- ); - } - - return ( -
-
Order ID: {order.order_id}
-
Status: {getOrderStatus(order.status)}
-
Submitted:  - {moment(order.submitted_timestamp).format(OrderDetails.timeFormat)} -
- {this.completedTimestampJSX(order)} - {links} -
- ); - } - } - - public componentDidMount() { - this.props.environment.hermesAPI.openNotificationConnection(this.context.user, - this.handleNotification); - } - - public componentDidUpdate() { - const orderSynced: boolean = this.state.order && (this.props.orderId === this.state.order.order_id); - if (this.props.orderId && !orderSynced && !this.state.loading) { - this.loadOrder(); - } - } - - private buildDataLinks(order: any): JSX.Element[] { - if (["inprogress", "expired"].includes(order.status)) { - return []; - } - - const html = order.file_urls.data.map((link: any, index: number) => { - return (
  • {link}
  • ); - }); - return html; - } - - private buildTextFileLink(order: any): JSX.Element { - const textLinks = order.file_urls.data.join("\n"); - if (textLinks.length > 0) { - const orderId = this.props.orderId ? this.props.orderId.substring(0, 8) : ""; - const fileName = "nsidc-download_" + orderId + ".txt"; - const file = new Blob([textLinks], { type: "text/plain" }); - const fileList = ( -
  • - {fileName} -
  • - ); - return fileList; - } - return ; - } - - private buildZipLinks(order: any): JSX.Element { - const zipLinks = this.findZipLinks(order); - if (!zipLinks) { - return ( Please wait... ); - } - - const html = zipLinks.map((link: any, index: number) => { - return (
  • {link}
  • ); - }); - return html; - } - - private findZipLinks(order: any): any { - return order.file_urls.archive; - } - - private completedTimestampJSX(order: any): JSX.Element | null { - if (order.finalized_timestamp) { - return ( -
    - Completed:  - {moment(order.finalized_timestamp).format(OrderDetails.timeFormat)} -
    - ); - } - return null; - } - - private loadOrder = () => { - if (this.props.orderId) { - this.setState({loading: true}, this.requestOrder); - } - } - - private requestOrder = () => { - this.props.environment.hermesAPI.getOrder(this.props.orderId!) - .then((order: object) => this.setState({order, loading: false})) - .catch(() => this.setState({loading: false})); - } - - private handleNotification = (event: any) => { - const notification: any = JSON.parse(event); - if (notification.order_id === this.props.orderId) { - this.loadOrder(); - } - } -} diff --git a/src/components/OrderList.tsx b/src/components/OrderList.tsx deleted file mode 100644 index 12e45709..00000000 --- a/src/components/OrderList.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import * as moment from "moment"; -import * as React from "react"; - -import { faSortDown, faSortUp } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { IEnvironment } from "../utils/environment"; -import { hasChanged } from "../utils/hasChanged"; -import { OrderListItem } from "./OrderListItem"; - -interface IOrderListProps { - environment: IEnvironment; - initialLoadComplete: boolean; - onSelectionChange: any; - orderList: object[]; - selectedOrder?: string; -} - -interface IOrderListState { - orderSorting: OrderSorting; -} - -export enum OrderSorting { - OrderTimeUp, - OrderTimeDown, - IDUp, - IDDown, - FilesUp, - FilesDown, - SizeUp, - SizeDown, - StatusUp, - StatusDown, - DeliveryUp, - DeliveryDown, - DeliverTimeUp, - DeliverTimeDown, -} - -export class OrderList extends React.Component { - public constructor(props: IOrderListProps) { - super(props); - - this.state = { - orderSorting: OrderSorting.OrderTimeDown, - }; - } - - public shouldComponentUpdate(nextProps: IOrderListProps, nextState: IOrderListState) { - const propsChanged = hasChanged(this.props, nextProps, ["initialLoadComplete", "orderList", "selectedOrder"]); - const stateChanged = hasChanged(this.state, nextState, ["orderSorting"]); - - return propsChanged || stateChanged; - } - - public render() { - let orderList: any[] = this.props.orderList; - - if (!this.props.initialLoadComplete) { return null; } - - orderList = orderList.sort((o1: any, o2: any) => { - let sign = 1; - // For columns other than Order Time or Order ID, if there is a tie, - // always return the tied orders in descending Order Time. - const order1isBeforeOrder2 = moment(o1.submitted_timestamp).isBefore(o2.submitted_timestamp); - switch (this.state.orderSorting) { - case OrderSorting.OrderTimeUp: sign = -1; - case OrderSorting.OrderTimeDown: - return order1isBeforeOrder2 ? sign : -sign; - break; - case OrderSorting.IDUp: sign = -1; - case OrderSorting.IDDown: - return (o1.order_id < o2.order_id) ? sign : -sign; - break; - case OrderSorting.FilesUp: sign = -1; - case OrderSorting.FilesDown: - if (o1.granule_count < o2.granule_count) { - return sign; - } else if (o1.granule_count > o2.granule_count) { - return -sign; - } - return order1isBeforeOrder2 ? 1 : -1; - break; - case OrderSorting.StatusUp: sign = -1; - case OrderSorting.StatusDown: - if (o1.status < o2.status) { - return sign; - } else if (o1.status > o2.status) { - return -sign; - } - return order1isBeforeOrder2 ? 1 : -1; - break; - case OrderSorting.DeliveryUp: sign = -1; - case OrderSorting.DeliveryDown: - if (o1.delivery < o2.delivery) { - return sign; - } else if (o1.delivery > o2.delivery) { - return -sign; - } - return order1isBeforeOrder2 ? 1 : -1; - break; - default: - break; - } - return 0; - }); - - orderList = orderList.map((order: any, index: number) => { - let selected: boolean = false; - if (order.order_id === this.props.selectedOrder) { - selected = true; - } - return ( - - ); - }); - - // {this.columnHeader("Size (MB)", "order-list-right", - // OrderSorting.SizeUp, OrderSorting.SizeDown)} - // {this.columnHeader("Deliver Time", "", - // OrderSorting.DeliverTimeUp, OrderSorting.DeliverTimeDown)} - return ( -
    - - - - {this.columnHeader("Order Time", "", - OrderSorting.OrderTimeUp, OrderSorting.OrderTimeDown)} - {this.columnHeader("Order ID", "", - OrderSorting.IDUp, OrderSorting.IDDown)} - {this.columnHeader("# Files", "order-list-right", - OrderSorting.FilesUp, OrderSorting.FilesDown)} - {this.columnHeader("Status", "", - OrderSorting.StatusUp, OrderSorting.StatusDown)} - {this.columnHeader("Delivery", "", - OrderSorting.DeliveryUp, OrderSorting.DeliveryDown)} - - - - {orderList} - -
    -
    - ); - } - - private updateOrderSorting = (orderSorting: OrderSorting) => { - this.setState({ - orderSorting, - }); - } - - private columnHeader = (header: JSX.Element | string, style: string, - columnSortUp: OrderSorting, columnSortDown: OrderSorting) => { - let newColumnSortOrder = columnSortUp; - let classSortUp = "fa-stack-1x sort-icon"; - let classSortDown = "fa-stack-1x sort-icon"; - if (this.state.orderSorting === columnSortUp) { - classSortUp += " sort-active"; - newColumnSortOrder = columnSortDown; - } else if (this.state.orderSorting === columnSortDown) { - classSortDown += " sort-active"; - } - const colStyle = "order-list-header" + (style ? " " + style : ""); - const iconUp = ; - const iconDown = ; - const sortOnClick = (e: any) => { - this.updateOrderSorting(newColumnSortOrder); - }; - return ( - -
    - {header} - - {iconUp}{iconDown} - -
    - - ); - } - -} diff --git a/src/components/OrderListItem.tsx b/src/components/OrderListItem.tsx deleted file mode 100644 index 9a8eed68..00000000 --- a/src/components/OrderListItem.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as moment from "moment"; -import * as React from "react"; - -import { faBan, faCheck, faClock, faEllipsisH, - faExclamationTriangle, faTimesCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { hasChanged } from "../utils/hasChanged"; -import { orderExpirationTimestamp } from "../utils/misc"; - -interface IOrderListItemProps { - order: any; - onOrderSelection: (orderId: string) => void; - selected: boolean; -} - -export class OrderListItem extends React.Component { - private static timeFormat = "l LT"; - - public shouldComponentUpdate(nextProps: IOrderListItemProps) { - return hasChanged(this.props, nextProps, ["order", "selected"]); - } - - public render() { - let style: string = "order-list-item"; - if (this.props.selected) { - style += " order-list-item-selected"; - } - let status = this.props.order.status; - - // Make mock "expired" status based on completion date +2 weeks - const expirationTimestamp = orderExpirationTimestamp(this.props.order); - if (expirationTimestamp && expirationTimestamp.isBefore(moment.now())) { - style += " order-list-item-expired"; - if (status === "complete" || status === "error" || status === "warning") { - status = "expired"; - } - } - - const delivery = (this.props.order.delivery === "ESI") ? - "Zip" : this.props.order.delivery; - const submitted = moment(this.props.order.submitted_timestamp).format(OrderListItem.timeFormat); - const statusIcon = getOrderStatus(status); - return ( - - {submitted} - {this.props.order.order_id} - {this.props.order.granule_count} - {statusIcon} - {delivery} - - ); - } - - private handleOrderSelection = () => { - this.props.onOrderSelection(this.props.order.order_id); - } -} - -export function getOrderStatus(orderStatus: string): any { - let status = null; - switch (orderStatus) { - case "cancelrequested": - case "cancelled": - status = ; - break; - case "complete": - status = ; - break; - case "error": - status = ; - break; - case "expired": - status = ; - break; - case "failed": - status = ; - break; - case "inprogress": - case "pending": - status = ; - break; - case "warning": - status = ; - break; - default: - break; - } - return {status} {orderStatus}; -} diff --git a/src/profile.ts b/src/profile.ts deleted file mode 100644 index f819fdeb..00000000 --- a/src/profile.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { shim } from "promise.prototype.finally"; - -import setupEnvironment from "./utils/environment"; -import { renderApp } from "./renderProfile"; - -declare var Drupal: any; - -shim(); // Get support for Promise.finally(). Can be replaced with Typescript 2.7+ and esnext -renderApp((setupEnvironment())); diff --git a/src/renderProfile.tsx b/src/renderProfile.tsx deleted file mode 100644 index c19b7f8e..00000000 --- a/src/renderProfile.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from "react"; -import * as ReactDOM from "react-dom"; - -import "./styles/index.less"; - -import { EverestProfile } from "./components/EverestProfile"; - -export const renderApp = (environment: any) => { - ReactDOM.render( - , - document.getElementById("order-history"), - ); -}; diff --git a/src/styles/eui_buttons.less b/src/styles/eui_buttons.less index 20c8b63d..d21640ea 100644 --- a/src/styles/eui_buttons.less +++ b/src/styles/eui_buttons.less @@ -1,5 +1,4 @@ -#order-data, -#order-history { +#order-data { /* Down-arrow icon for button group */ @font-face { font-family: eui-icon-library; diff --git a/src/styles/index.less b/src/styles/index.less index cf7f3385..e5ab45de 100644 --- a/src/styles/index.less +++ b/src/styles/index.less @@ -456,136 +456,6 @@ } } - &-profile { - #everest-profile-container { - align-content: baseline; - align-items: baseline; - display: flex; - flex-flow: column wrap; - position: relative; - - &.in-drupal { - bottom: 64px; - } - } - - .earthdata-login { - align-self: end; - order: 0; - margin-bottom: 22px; - } - - #order-list-wrapper { - order: 1; - width: 100%; - } - - #order-list { - max-height: 400px; - overflow-y: scroll; - margin-bottom: 10px; - border-bottom: 1px solid; - - #order-table { - overflow-y: scroll; - display: table; - border-collapse: collapse; - table-layout: auto; - padding: 0; - width: 100%; - - .order-list-item { - padding: 5px; - cursor: pointer; - - &:hover { - background-color: #f0f0f0; - } - - &:active { - background-color: silver; - } - } - - .order-list-item-selected { - background-color: silver; - - &:hover { - background-color: silver; - } - } - - .order-list-item-expired { - color: gray; - } - - .order-list-header { - text-align: left; - color: white; - cursor: pointer; - position: sticky; - position: -webkit-sticky; /* stylelint-disable-line value-no-vendor-prefix */ - top: 0; - padding: 0.5rem 0 0.5rem 0.5rem; - z-index: 9; - border-top: 2px; - background: #7ab5da; /* EUI Sky Blue */ - } - - .order-list-right { - text-align: right; - } - } - } - - .order-success { - color: green; - } - - .order-warning { - color: gold; - } - - .order-error { - color: red; - } - - #order-details { - overflow-wrap: break-word; - word-break: break-all; - min-height: 500px; - width: 100%; - } - } - - &, - &-profile { - .sortColumn { /* stylelint-disable-line selector-class-pattern */ - .sort-icon-stack { - font-size: 1.5em; - padding-top: -0.25em; - padding-bottom: -0.25em; - height: 1em; - width: 1em; - margin-left: 0.25em; - vertical-align: middle; - margin-bottom: 2px; - } - - .sort-icon { - opacity: 0.25; - } - - .sort-active { - opacity: 1; - } - - &:hover .sort-icon { - opacity: 1; - } - } - } - .cursor-crosshair { cursor: crosshair; } @@ -760,29 +630,6 @@ } /* stylelint-enable selector-class-pattern */ /* stylelint-enable property-no-vendor-prefix */ - - .earthdata-login { - float: right; - - button { - z-index: 1002; - } - - ul.button-group--dropdown { /* stylelint-disable-line selector-class-pattern */ - z-index: 1002; - } - - #logout-button { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } - - .in-drupal { - .earthdata-login { - right: 217px; - } - } } /* The `+` is the `next-sibling combinator. This applies `margin-left` when two diff --git a/src/types/User.ts b/src/types/User.ts deleted file mode 100644 index 9be73505..00000000 --- a/src/types/User.ts +++ /dev/null @@ -1,35 +0,0 @@ -// response from hermes-api's `GET /user/` has at least these keys when logged -// in, and this is also the interface the Everest app uses for the user state -export interface ILoggedInUser { - readonly first_name: string; - readonly last_name: string; - readonly type: "user"; - readonly uid: string; -} - -// response from hermes-api's `GET /user/` when not logged in -interface IHermesApiLoggedOutUser { - readonly type: "anonymous"; -} - -// hermes-api `GET /user/` returns JSON matching either an ILoggedInUser or -// `{ "type": "anonymous" }` -export type HermesAPIUserJSON = ILoggedInUser | IHermesApiLoggedOutUser; - -export const EverestUserUnknownStatus = null; -export const EverestUserLoggedOut = false; -export type EverestUser = ILoggedInUser | false | null; - -// https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards -export function isLoggedInUser(object: any): object is ILoggedInUser { - return Boolean(object) && - ("first_name" in object) && - ("last_name" in object) && - ("type" in object) && - ("uid" in object) && - (object.type === "user"); -} - -export function isLoggedOutUser(object: any): boolean { - return object === EverestUserLoggedOut; -} diff --git a/src/utils/Hermes.ts b/src/utils/Hermes.ts deleted file mode 100644 index 93bd8027..00000000 --- a/src/utils/Hermes.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as io from "socket.io-client"; - -import { ISelectionCriteria } from "../types/OrderSubmissionParameters"; -import { EverestUser, HermesAPIUserJSON, ILoggedInUser, isLoggedInUser } from "../types/User"; - -export interface IHermesAPI { - getOrder: (orderId: string) => any; - getUser: () => Promise; - getUserOrders: (user: ILoggedInUser) => any; - logoutUser: () => Promise; - openNotificationConnection: (user: EverestUser, callback: any) => void; - submitOrder: (user: EverestUser, - selectionCriteria: ISelectionCriteria) => Promise; -} - -// TODO: now that we don't depend on `inDrupal`, we could export a dict instead -// of this function -export function constructAPI(urls: any): IHermesAPI { - const getOrder = (orderId: string) => { - return fetch(`${urls.hermesApiUrl}/orders/${orderId}`, {credentials: "include"}) - .then((response) => response.json()); - }; - - const getUserOrders = (user: ILoggedInUser) => { - const url = `${urls.hermesApiUrl}/users/${user.uid}/orders/`; - return fetch(url, {credentials: "include"}) - .then((response) => response.json()) - .then((orders) => orders.filter((order: any) => ["Everest", "Everest-dev"].includes(order.source_client))); - }; - - const openNotificationConnection = (user: EverestUser, callback: any) => { - if (!isLoggedInUser(user)) { return; } - - const ws: any = io.connect(urls.orderNotificationHost, { - path: urls.orderNotificationPath, - transports: ["websocket", "polling"], - }); - ws.emit("join", { user_id: user.uid }); - ws.on("reconnect", (event: any) => { - // console.log("Order notification: reconnected and rejoining"); - ws.emit("join", { user_id: user.uid }); - }); - ws.on("notification", callback); - }; - - const submitOrder = ( - user: EverestUser, - selectionCriteria: ISelectionCriteria, - ) => { - if (!isLoggedInUser(user)) { return Promise.resolve(null); } - - const headers: any = { - "Content-Type": "application/json", - }; - let body: object = { - delivery: "esi", - fulfillment: "esi", - selection_criteria: { - include_granules: selectionCriteria.includeGranules, - }, - }; - - body = Object.assign(body, {uid: user.uid, user}); - - const url = `${urls.hermesApiUrl}/orders/`; - return fetch(url, { - body: JSON.stringify(body), - credentials: "include", - headers, - method: "POST", - }); - }; - - const logoutUser = () => { - return fetch( - `${urls.hermesApiUrl}/earthdata/deauth/`, - { - credentials: "include", - method: "GET", - }, - ); - }; - - const getUser = (): Promise => { - return fetch( - `${urls.hermesApiUrl}/user/`, - { - credentials: "include", - method: "GET", - }, - ).then((response: Response) => { - if (!response.ok) { return Promise.resolve(null); } - return response.json(); - }) - }; - - return { - getOrder, - getUser, - getUserOrders, - logoutUser, - openNotificationConnection, - submitOrder, - }; -} diff --git a/src/utils/environment.ts b/src/utils/environment.ts index b7edabe9..2081f87c 100644 --- a/src/utils/environment.ts +++ b/src/utils/environment.ts @@ -1,21 +1,13 @@ import {IDrupalDataset} from "../types/DrupalDataset"; -import {constructAPI, IHermesAPI} from "./Hermes"; interface IUrls { datBackendApiUrl: string; - hermesApiUrl: string; - orderNotificationHost: string; - orderNotificationPath: string; - profileUrl: string; } export interface IEnvironment { drupalDataset?: IDrupalDataset; exposeFunction: (name: string, callback: (...args: any[]) => any) => boolean; - hermesAPI: IHermesAPI; inDrupal: boolean; - // TODO: add dat-backend api url here. - // TODO: consider adding a module for the dat API similar to Hermes.ts. urls: IUrls; } @@ -28,24 +20,20 @@ export function getEnvironment(): string { } function getEnvironmentDependentURLs() { - // TODO: this function gives the same result in every case. Necessary? if (getEnvironment() === "dev") { return { + // TODO: this should be more easily configurable. Integration is nice to + // test against for the EDD interactions because it's easy to get changes + // there and the dev setup is a little simpler (behind Apache proxy and + // allow-listed by the EDD). But it is possible and often desirable to + // change this to reflect an individual dev's dev environment. datBackendApiUrl: "https://integration.nsidc.org/apps/data-access-tool/api", - // TODO: these will all be removed with the decom of hermes. - hermesApiUrl: "/apps/orders/api", - orderNotificationHost: `wss://${window.location.hostname}`, - orderNotificationPath: "/apps/orders/notification/", }; } else { return { // Note: the backend API url must be fully specified for its use in the // EDD deep link (it cannot be relative to the root) datBackendApiUrl: `${window.location.protocol}//${window.location.host}/apps/data-access-tool/api`, - // TODO: these will all be removed with the decom of hermes. - hermesApiUrl: "/apps/orders/api", - orderNotificationHost: `wss://${window.location.hostname}`, - orderNotificationPath: "/apps/orders/notification/", }; } } @@ -73,7 +61,6 @@ export default function setupEnvironment(): IEnvironment { let datasetFromDrupal: IDrupalDataset | undefined; let inDrupal: boolean = false; - let profileLocation: string = "/order-history.html"; const drupalSettings: {[key: string]: any} = (window as {[key: string]: any}).drupalSettings; if (typeof (drupalSettings) !== "undefined") { @@ -83,18 +70,15 @@ export default function setupEnvironment(): IEnvironment { version: drupalSettings.data_downloads?.dataset?.version, title: '', }; - profileLocation = "/order-history"; inDrupal = true; } const urls = { ...getEnvironmentDependentURLs(), - profileUrl: profileLocation, }; return { drupalDataset: datasetFromDrupal, exposeFunction, - hermesAPI: constructAPI(urls), inDrupal, urls, }; diff --git a/src/utils/misc.ts b/src/utils/misc.ts deleted file mode 100644 index 8cfdc99f..00000000 --- a/src/utils/misc.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as moment from "moment"; - -export const orderExpirationTimestamp = (order: any): moment.Moment | null => { - if (order.finalized_timestamp) { - return moment(order.finalized_timestamp).clone().add(14, "days"); - } else if (["complete", "error", "warning"].includes(order.status)) { - return moment(order.submitted_timestamp).clone().add(14, "days"); - } - - return null; -}; diff --git a/src/utils/state.ts b/src/utils/state.ts index aa3c9693..39523ee3 100644 --- a/src/utils/state.ts +++ b/src/utils/state.ts @@ -1,28 +1,8 @@ import { fromJS, List } from "immutable"; -import * as React from "react"; import { IEverestState } from "../components/EverestUI"; import { CmrGranule, ICmrGranule } from "../types/CmrGranule"; import { OrderSubmissionParameters } from "../types/OrderSubmissionParameters"; -import { EverestUser, EverestUserLoggedOut, EverestUserUnknownStatus, HermesAPIUserJSON, isLoggedInUser } from "../types/User"; - -interface IUserContextProps { - updateUser: (thisArg: any) => void; - user: EverestUser; -} - -export const UserContext = React.createContext({ - updateUser: () => { return; }, - user: EverestUserUnknownStatus, -}); - -export const updateUser = (thisArg: any): void => { - thisArg.props.environment.hermesAPI.getUser() - .then((hermesUser: HermesAPIUserJSON) => { - const user: EverestUser = isLoggedInUser(hermesUser) ? hermesUser : EverestUserLoggedOut; - return thisArg.setState({user}); - }); -}; const orderSubmissionParametersFromCmrGranules = (cmrGranules: List) => { const granules = cmrGranules.map((g) => g!.title) as List; diff --git a/tests/Profile.test.tsx b/tests/Profile.test.tsx deleted file mode 100644 index 5a91c791..00000000 --- a/tests/Profile.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { mount } from "enzyme"; -import * as React from "react"; - -import { IOrderDetailsProps, IOrderDetailsState, OrderDetails } from "../src/components/OrderDetails"; -import setupEnvironment from "../src/utils/environment"; - -const setup = () => { - const orders: any = { - "order1": { - "collection_info": [["A Collection", "https://a.collection.url"]], - "delivery": "esi", - "fulfillment": "esi", - "selection_criteria": { - "include_granules": ["1 granule", "2 granule"], - }, - "links": [], - "order_id": "order1", - "status": "complete", - "timestamp": "2018-08-10T01:22:21.457Z", - }, - "order2": { - "collection_info": [["B Collection", "https://b.collection.url"]], - "delivery": "esi", - "fulfillment": "esi", - "selection_criteria": { - "include_granules": ["3 granule", "4 granule"], - }, - "links": [], - "order_id": "order2", - "status": "complete", - "timestamp": "2018-08-10T01:22:21.457Z", - }, - }; - const environment = Object.assign(setupEnvironment(), { - hermesAPI: { - getOrder: jest.fn((orderId: string) => Promise.resolve(orders[orderId])), - getUserOrders: jest.fn(() => Promise.resolve(orders)), - openNotificationConnection: jest.fn(), - }, - }); - return { - environment, - }; -}; - -describe("The user profile", () => { - describe("OrderDetails", () => { - it("should update when new order selected", () => { - const environment = setup().environment; - const oldProps: IOrderDetailsProps = {environment, orderId: undefined, initialLoadComplete: true}; - const oldState: IOrderDetailsState = {loading: false, order: undefined}; - const wrapper = mount( - , - ); - const instance = wrapper.instance(); - instance.state = oldState; - - expect((instance.state as IOrderDetailsState).order).not.toBeDefined(); - expect((instance.props as IOrderDetailsProps).orderId).not.toBeDefined(); - - const nextProps: IOrderDetailsProps = {environment, orderId: "order1"}; - expect(instance.shouldComponentUpdate!(nextProps, {}, {})).toBe(true); - - // Test the component state changes and renders expected HTML - // TODO: Fix. Doesn't actually produce a state change or HTML change. - /* - console.log("Set props..."); - wrapper.setProps({orderId: "order1"}); - expect((instance.props as IOrderDetailsProps).orderId).toEqual("order1"); - expect((instance.state as IOrderDetailsState).order).toBeDefined(); - */ - }); - }); -}); diff --git a/tests/state.test.ts b/tests/state.test.ts index 51d8d661..72fe7029 100644 --- a/tests/state.test.ts +++ b/tests/state.test.ts @@ -4,7 +4,6 @@ import * as moment from "moment"; import { CmrGranule } from "../src/types/CmrGranule"; import { OrderParameters } from "../src/types/OrderParameters"; import { OrderSubmissionParameters } from "../src/types/OrderSubmissionParameters"; -import { EverestUserUnknownStatus } from "../src/types/User"; import { updateStateAddGranules, updateStateInitGranules } from "../src/utils/state"; const granuleInput = { @@ -30,7 +29,6 @@ const initialState = { orderSubmissionParameters: undefined, stateCanBeFrozen: false, totalSize: 0, - user: EverestUserUnknownStatus, }; const expected = { diff --git a/tests/types/User.test.ts b/tests/types/User.test.ts deleted file mode 100644 index e4ac9bca..00000000 --- a/tests/types/User.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { EverestUserLoggedOut, - EverestUserUnknownStatus, - isLoggedInUser, - isLoggedOutUser } from "../../src/types/User"; - -describe("isLoggedInUser", () => { - test("returns true with a user matching the interface ILoggedInUser", () => { - const user = { - first_name: 'Obi-Wan', - last_name: 'Kenobi', - type: 'user', - uid: 'okenobi', - }; - const actual = isLoggedInUser(user); - expect(actual).toBe(true); - }); - - test("returns false with a logged out user", () => { - const user = EverestUserLoggedOut; - const actual = isLoggedInUser(user); - expect(actual).toBe(false); - }); - - test("returns false with a user whose status is not yet known", () => { - const user = EverestUserUnknownStatus; - const actual = isLoggedInUser(user); - expect(actual).toBe(false); - }); -}); - -describe("isLoggedOutUser", () => { - test("returns false with a user matching the interface ILoggedInUser", () => { - const user = { - first_name: 'Obi-Wan', - last_name: 'Kenobi', - type: 'user', - uid: 'okenobi', - }; - const actual = isLoggedOutUser(user); - expect(actual).toBe(false); - }); - - test("returns true with a logged out user", () => { - const user = EverestUserLoggedOut; - const actual = isLoggedOutUser(user); - expect(actual).toBe(true); - }); - - test("returns false with a user whose status is not yet known", () => { - const user = EverestUserUnknownStatus; - const actual = isLoggedOutUser(user); - expect(actual).toBe(false); - }); -}); diff --git a/webpack.config.cjs b/webpack.config.cjs index 99bd9cf6..0178db22 100644 --- a/webpack.config.cjs +++ b/webpack.config.cjs @@ -30,13 +30,6 @@ const devConfig = { devServer : { server: "https", - proxy: { - "/apps/orders/api": { - target: "https://localhost:5000", - pathRewrite: { '^/apps/orders/api': '' }, - secure: false, - } - } }, }; @@ -51,7 +44,6 @@ const prodConfig = { const config = { entry: { "order-data": ['./src/index.ts'], - "order-history": ['./src/profile.ts'] }, output: { filename: '[name].bundle.js', @@ -121,16 +113,6 @@ const config = { links, scripts }), - new HtmlWebpackPlugin({ - title: 'Order History Interface', - chunks: ['order-history'], - inject: 'body', - template: './public/order-history.html', - filename: 'order-history.html', - appMountId: 'order-history', - links, - scripts - }), // Copy Cesium Assets, Widgets, and Workers to a static directory new CopyWebpackPlugin({ patterns: [