From 998cdf98abac5749ac281bfeb777246334d33f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Fri, 21 May 2021 08:56:22 +0200 Subject: [PATCH] feat: Add admin-invoice section (#299) * feat: Add admin-invoice section * fix: do not show invoice table if list is empty --- frontend/src/component/menu/routes.js | 10 +++ frontend/src/page/admin/invoice/index.js | 36 ++++++++ .../page/admin/invoice/invoice-container.js | 11 +++ .../src/page/admin/invoice/invoice-list.jsx | 88 +++++++++++++++++++ frontend/src/store/e-admin-invoice/actions.js | 20 +++++ frontend/src/store/e-admin-invoice/api.js | 14 +++ frontend/src/store/e-admin-invoice/index.js | 13 +++ frontend/src/store/index.js | 2 + .../ui-config-store.test.js.snap | 12 +-- frontend/src/store/ui-config/index.js | 4 +- 10 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 frontend/src/page/admin/invoice/index.js create mode 100644 frontend/src/page/admin/invoice/invoice-container.js create mode 100644 frontend/src/page/admin/invoice/invoice-list.jsx create mode 100644 frontend/src/store/e-admin-invoice/actions.js create mode 100644 frontend/src/store/e-admin-invoice/api.js create mode 100644 frontend/src/store/e-admin-invoice/index.js diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index 21bdc17e8d..7deaacd9c3 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -31,6 +31,7 @@ import AddonsEdit from '../../page/addons/edit'; import Admin from '../../page/admin'; import AdminApi from '../../page/admin/api'; import AdminUsers from '../../page/admin/users'; +import AdminInvoice from '../../page/admin/invoice'; import AdminAuth from '../../page/admin/auth'; import Reporting from '../../page/reporting'; import Login from '../user/Login'; @@ -327,6 +328,15 @@ export const routes = [ type: 'protected', layout: 'main', }, + { + path: '/admin-invoices', + title: 'Invoices', + icon: 'money', + component: AdminInvoice, + hidden: true, + type: 'protected', + layout: 'main', + }, { path: '/admin', title: 'Admin', diff --git a/frontend/src/page/admin/invoice/index.js b/frontend/src/page/admin/invoice/index.js new file mode 100644 index 0000000000..c6e79ad81d --- /dev/null +++ b/frontend/src/page/admin/invoice/index.js @@ -0,0 +1,36 @@ +import { useContext } from 'react'; +import PropTypes from 'prop-types'; +import InvoiceList from './invoice-container'; +import AccessContext from '../../../contexts/AccessContext'; +import { ADMIN } from '../../../component/AccessProvider/permissions'; +import ConditionallyRender from '../../../component/common/ConditionallyRender'; +import { Alert } from '@material-ui/lab'; + +const InvoiceAdminPage = ({ history }) => { + const { hasAccess } = useContext(AccessContext); + + return ( +
+ + } + elseShow={ + + You need to be instance admin to access this section. + + } + + /> + +
+ ); +}; + +InvoiceAdminPage.propTypes = { + match: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, +}; + +export default InvoiceAdminPage; diff --git a/frontend/src/page/admin/invoice/invoice-container.js b/frontend/src/page/admin/invoice/invoice-container.js new file mode 100644 index 0000000000..8064dadc48 --- /dev/null +++ b/frontend/src/page/admin/invoice/invoice-container.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux'; + +import Component from './invoice-list'; +import { fetchInvoices } from '../../../store/e-admin-invoice/actions'; +export default connect( + state => ({ + location: state.settings.toJS().location || {}, + invoices: state.invoiceAdmin.toJS(), + }), + { fetchInvoices } +)(Component); diff --git a/frontend/src/page/admin/invoice/invoice-list.jsx b/frontend/src/page/admin/invoice/invoice-list.jsx new file mode 100644 index 0000000000..c2eaf91a4b --- /dev/null +++ b/frontend/src/page/admin/invoice/invoice-list.jsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + Table, + TableHead, + TableBody, + TableRow, + TableCell, + Button, +} from '@material-ui/core'; +import OpenInNew from '@material-ui/icons/OpenInNew'; +import { formatDateWithLocale } from '../../../component/common/util'; +import PageContent from '../../../component/common/PageContent'; +import HeaderTitle from '../../../component/common/HeaderTitle'; +import ConditionallyRender from '../../../component/common/ConditionallyRender'; +import { formatApiPath } from '../../../utils/format-path'; + +const PORTAL_URL = formatApiPath('api/admin/invoices/portal'); + +function InvoiceList({ + location, + fetchInvoices, + invoices, +}) { + + const [isLoaded, setLoaded] = useState(false); + + useEffect(() => { + fetchInvoices().finally(() => setLoaded(true)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + 0} + show={ + }> + Billing portal + } />}> +
+ + + + + Amount + Status + Due date + PDF + Link + + + + {invoices.map(item => ( + + + {item.amountFomratted} + + + {item.status} + + + {formatDateWithLocale( + item.dueDate, + location.locale + )} + + + PDF + + + Payment link + + + ))} + +
+
+
} elseShow={
{isLoaded && "No invoices to show."}
} /> + ); +} + +InvoiceList.propTypes = { + location: PropTypes.object, + fetchInvoices: PropTypes.func.isRequired, + invoices: PropTypes.array.isRequired, +}; + +export default InvoiceList; diff --git a/frontend/src/store/e-admin-invoice/actions.js b/frontend/src/store/e-admin-invoice/actions.js new file mode 100644 index 0000000000..4a7a2cac8b --- /dev/null +++ b/frontend/src/store/e-admin-invoice/actions.js @@ -0,0 +1,20 @@ +import api from './api'; +import { dispatchError } from '../util'; +export const RECEIVE_INVOICES = 'RECEIVE_INVOICES'; +export const ERROR_FETCH_INVOICES = 'ERROR_FETCH_INVOICES'; + +const debug = require('debug')('unleash:e-admin-invoice-actions'); + +export function fetchInvoices() { + debug('Start fetching invoices for hosted customer'); + return dispatch => + api + .fetchAll() + .then(value => + dispatch({ + type: RECEIVE_INVOICES, + invoices: value.invoices, + }) + ) + .catch(dispatchError(dispatch, ERROR_FETCH_INVOICES)); +} \ No newline at end of file diff --git a/frontend/src/store/e-admin-invoice/api.js b/frontend/src/store/e-admin-invoice/api.js new file mode 100644 index 0000000000..0918ad1d68 --- /dev/null +++ b/frontend/src/store/e-admin-invoice/api.js @@ -0,0 +1,14 @@ +import { formatApiPath } from '../../utils/format-path'; +import { throwIfNotSuccess, headers } from '../api-helper'; + +const URI = formatApiPath('api/admin/invoices'); + +function fetchAll() { + return fetch(URI, { headers, credentials: 'include' }) + .then(throwIfNotSuccess) + .then(response => response.json()); +} + +export default { + fetchAll +}; diff --git a/frontend/src/store/e-admin-invoice/index.js b/frontend/src/store/e-admin-invoice/index.js new file mode 100644 index 0000000000..9def0dc310 --- /dev/null +++ b/frontend/src/store/e-admin-invoice/index.js @@ -0,0 +1,13 @@ +import { List } from 'immutable'; +import { RECEIVE_INVOICES } from './actions'; + +const store = (state = new List(), action) => { + switch (action.type) { + case RECEIVE_INVOICES: + return new List(action.invoices); + default: + return state; + } +}; + +export default store; diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index d3726c52a5..230cad2e40 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -19,6 +19,7 @@ import addons from './addons'; import apiAdmin from './e-api-admin'; import authAdmin from './e-admin-auth'; import apiCalls from './api-calls'; +import invoiceAdmin from './e-admin-invoice'; const unleashStore = combineReducers({ features, @@ -41,6 +42,7 @@ const unleashStore = combineReducers({ apiAdmin, authAdmin, apiCalls, + invoiceAdmin, }); export default unleashStore; diff --git a/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap b/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap index 99f14a0b22..ad585c9125 100644 --- a/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap +++ b/frontend/src/store/ui-config/__tests__/__snapshots__/ui-config-store.test.js.snap @@ -7,10 +7,10 @@ Object { "headerBackground": undefined, "links": Array [ Object { - "href": "https://unleash.github.io?source=oss", + "href": "https://docs.getunleash.io/docs?source=oss", "icon": "library_books", "title": "User documentation", - "value": "User documentation", + "value": "Documentation", }, Object { "href": "https://github.com/Unleash", @@ -32,10 +32,10 @@ Object { "headerBackground": "red", "links": Array [ Object { - "href": "https://unleash.github.io?source=oss", + "href": "https://docs.getunleash.io/docs?source=oss", "icon": "library_books", "title": "User documentation", - "value": "User documentation", + "value": "Documentation", }, Object { "href": "https://github.com/Unleash", @@ -57,10 +57,10 @@ Object { "headerBackground": "black", "links": Array [ Object { - "href": "https://unleash.github.io?source=oss", + "href": "https://docs.getunleash.io/docs?source=oss", "icon": "library_books", "title": "User documentation", - "value": "User documentation", + "value": "Documentation", }, Object { "href": "https://github.com/Unleash", diff --git a/frontend/src/store/ui-config/index.js b/frontend/src/store/ui-config/index.js index 1aa3f6e4ce..f5bd7ebf65 100644 --- a/frontend/src/store/ui-config/index.js +++ b/frontend/src/store/ui-config/index.js @@ -20,9 +20,9 @@ const DEFAULT = new $Map({ flags: {}, links: [ { - value: 'User documentation', + value: 'Documentation', icon: 'library_books', - href: 'https://unleash.github.io?source=oss', + href: 'https://docs.getunleash.io/docs?source=oss', title: 'User documentation', }, {