UNPKG

django-bananas

Version:
668 lines (648 loc) 25.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _material = require("@mui/material"); var _CssBaseline = _interopRequireDefault(require("@mui/material/CssBaseline")); var _styles = require("@mui/material/styles"); var _styles2 = require("@mui/styles"); var _classnames = _interopRequireDefault(require("classnames")); var _jsLogger = _interopRequireDefault(require("js-logger")); var _propTypes = _interopRequireDefault(require("prop-types")); var _react = _interopRequireDefault(require("react")); var _AdminContext = _interopRequireDefault(require("./AdminContext")); var _Alert = require("./Alert"); var _api = _interopRequireDefault(require("./api")); var _errors = require("./errors"); var _LoadingScreen = _interopRequireDefault(require("./LoadingScreen")); var _Messages = require("./Messages"); var _NavBar = _interopRequireDefault(require("./NavBar")); var _Page = require("./Page"); var _pages = require("./pages"); var _router = _interopRequireDefault(require("./router")); var _settings = _interopRequireDefault(require("./settings")); var _themes = _interopRequireWildcard(require("./themes")); var _utils = require("./utils"); var _excluded = ["url"]; function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } // eslint thinks this is a hook. // eslint-disable-next-line _jsLogger.default.useDefaults(); var logger = _jsLogger.default.get("bananas"); var styles = theme => { return { root: { display: "flex", position: "fixed", top: 0, left: 0, width: "100%", height: "100%", backgroundColor: theme.palette.primary.dark }, horizontalRoot: { width: "100%", flexDirection: "row", [theme.breakpoints.down("sm")]: { flexDirection: "column" } }, verticalRoot: { flexDirection: "column" }, admin: { backgroundColor: theme.palette.background.default } }; }; class Admin extends _react.default.Component { constructor(props) { super(props); // Default GUI settings from props _defineProperty(this, "Page", null); var propSettings = { editable: props.editableSettings, horizontal: props.layout === "horizontal", icons: Boolean(props.nav) && !Array.isArray(props.nav), collapsable: !(props.permanent || false), collapsed: props.collapsed || false, dense: props.dense || false, container: props.container || undefined }; // Initialize GUI settings this.settings = new _settings.default(propSettings, this.settingsDidUpdate.bind(this)); // Initialize component controllers proxy references this.controllers = new _utils.ComponentProxy(); this.controllers.add(_Page.PageLoadController, "PageLoadController"); this.controllers.add(_Alert.AlertController, "AlertController"); this.controllers.add(_Messages.MessagesController, "MessagesController"); this.admin = this.controllers.proxy; // Shortcut this.state = { context: this.makeContext(), settings: this.settings.settings, pageProps: undefined, booting: true, booted: false }; logger.setLevel(this.getLogLevel("bananas", "WARN")); window.bananas = this; } getLogLevel(namespace, logLevel) { var level = (typeof this.props.logLevel === "string" ? this.props.logLevel : this.props.logLevel[namespace]) || logLevel || "WARN"; return _jsLogger.default[level]; } getLogger(namespace) { var log = _jsLogger.default.get(namespace); var level = this.getLogLevel(namespace); log.setLevel(level); return log; } makeContext(context) { var locals = ["setTitle", "login", "logout"].reduce((result, shortcut) => _objectSpread(_objectSpread({}, result), {}, { [shortcut]: this[shortcut].bind(this) }), { settings: this.settings }); return _objectSpread({ admin: _objectSpread(_objectSpread({}, this.admin), locals), router: undefined, api: undefined, user: undefined }, context); } setContext(newContext, callback) { this.setState(prev => ({ context: _objectSpread(_objectSpread({}, prev.context), typeof newContext === "function" ? newContext(prev.context) : newContext) }), () => { logger.debug("Patched AdminContext with:", newContext); callback === null || callback === void 0 || callback(); }); } resetContext(callback) { this.setContext(this.makeContext(), callback); } componentDidMount() { this.boot(); } componentWillUnmount() { // Unlisten to router/history events if (this.unlistenRouter) { this.unlistenRouter(); } // Undefine instances this.router = undefined; this.swagger = undefined; this.api = undefined; // Unbind app refernece on window window.bananas = undefined; } boot() { var _this = this; return _asyncToGenerator(function* () { logger.info("Booting..."); _this.setTitle(); // Initialize API client var apiProp = typeof _this.props.api === "string" ? { url: _this.props.api } : _this.props.api; var { url: apiBase } = apiProp, rest = _objectWithoutProperties(apiProp, _excluded); var apiUrl = "".concat(apiBase, "/v1.0/schema.json"); var swagger = undefined; try { swagger = yield new _api.default(_objectSpread({ url: apiUrl, errorHandler: _this.onAPIClientError.bind(_this), progressHandler: _this.onAPIClientProgress.bind(_this) }, rest)); } catch (error) { var _error$response$statu, _error$response; logger.error("Critical Error: Failed to initialize API client!", error); var cause = (_error$response$statu = error === null || error === void 0 || (_error$response = error.response) === null || _error$response === void 0 ? void 0 : _error$response.statusText) !== null && _error$response$statu !== void 0 ? _error$response$statu : "Unreachable"; _this.admin.error("Failed to boot: API ".concat(cause)); _this.setState({ booting: false }); return; } logger.info("Initialized ".concat(swagger.isAuthenticated ? "Authenticated" : "Un-authenticated", " Swagger Client:"), swagger); _this.swagger = swagger; _this.api = swagger.operations; // Load translations var i18n = yield _this.api["bananas.i18n:list"](); window.i18n = i18n.obj.catalog; // Initialize Router if (!_this.router) { _this.router = new _router.default({ prefix: _this.props.prefix }); _this.unlistenRouter = _this.router.on("routeDidUpdate", _this.routeDidUpdate.bind(_this)); } _this.router.initialize(swagger); // Update AdminContext _this.setContext({ api: _this.api, router: _this.router }); // Route to current window location if API is authenticatd if (swagger.isAuthenticated) { if (!_this.state.context.user) { yield _this.authorize(); } _this.router.reroute(); } // Allow adding extra things to the AdminContext. if (_this.props.customizeContext) { _this.setContext(_this.props.customizeContext); } // Finalize boot _this.setState({ booting: false, booted: true }, () => { logger.info("Booted!"); }); })(); } reboot(user) { var _this2 = this; return _asyncToGenerator(function* () { yield _this2.shutdown(); if (user) { _this2.setContext({ user }); } _this2.boot(); })(); } shutdown() { var _this3 = this; // eslint-disable-next-line return new Promise( /*#__PURE__*/function () { var _ref = _asyncToGenerator(function* (resolve) { yield _this3.unmountPage(); _this3.swagger = undefined; _this3.api = undefined; _this3.setState({ booted: false }, () => { _this3.resetContext(resolve); }); }); return function (_x) { return _ref.apply(this, arguments); }; }()); } authorize() { return new Promise((resolve, reject) => { var anonymous = new _errors.AnonymousUserError(); var endpoint = this.api["bananas.me:list"]; if (!endpoint) { reject(anonymous); return; } logger.debug("Authorizing..."); endpoint().then(response => { var user = (this.props.customizeUser || (usr => usr))((0, _utils.makeUser)(response.obj)); var current = this.state.context.user; if (JSON.stringify(user) !== JSON.stringify(current)) { logger.info("Authorized User:", user); this.setContext({ user }); } resolve(user); }, error => { if (error.message === "Forbidden") { reject(anonymous); } else { reject(error); } }); }); } onAPIClientError(error) { this.admin.progress(false); this.admin.error(error); } onAPIClientProgress(_ref2) { var { done } = _ref2; this.admin.progress(!done); } settingsDidUpdate(settings) { this.setState({ settings }); } routeDidUpdate(location, action) { var _this4 = this; return _asyncToGenerator(function* () { logger.info("App.routeDidUpdate()", action, location); // Get route from location state var route = location.state ? location.state.route : null; if (action === "POP" && !route) { _this4.router.reroute(); return; } // Un-mount current page, if location changed var currentPage = _this4.state.pageProps; if (currentPage && (!currentPage.route || currentPage.route.path !== location.pathname)) { yield _this4.unmountPage(); _this4.admin.loading(); } // Authorize, load and mount page try { var { PageComponent, pageProps } = yield _this4.loadPage(location, route); _this4.mountPage(PageComponent, pageProps); } catch (error) { _this4.admin.loading(false); if (error instanceof _errors.PageError) { _this4.mountErrorPage((0, _utils.t)(error.message), error.code); } else if (error.response && [401, 403].includes(error.response.status)) { try { yield _this4.authorize(); _this4.mountErrorPage((0, _utils.t)("Permission denied."), error.response.status); } catch (authorizeError) { if (authorizeError instanceof _errors.AnonymousUserError) { _this4.reboot(); } else { throw error; } } } else { throw error; } } })(); } loadPage(location, route) { var _this5 = this; return _asyncToGenerator(function* () { if (!route) { throw new _errors.PageNotFoundError(); } var { id, operationId, params, app, path, query, hash, template } = route; var referer = _this5.state.pageProps ? _this5.state.pageProps.route || null : null; var { PageComponent } = _this5; // Load or re-use page component if (_this5.PageComponent && referer && referer.id === id) { logger.debug("Re-using page component..."); } else { logger.debug("Loading page component...", path, route, referer); if (template === "Component") { // Route has predefined page component, and no data needed PageComponent = _this5.router.getOperationTemplate(operationId); } else { // Load page component PageComponent = yield _this5.loadPageComponent(template); } } // Initialize page component props var pageProps = { key: "".concat(id, ":").concat(location.search), route: { id, params, path, query, hash, location }, title: route.title, data: undefined, logger: _this5.getLogger(app), referer }; var reuseData = referer && location.hash && referer.location.pathname === location.pathname && referer.location.search === location.search; // Load page data, but only if path or query changed if (reuseData) { logger.debug("Re-using page data..."); pageProps.data = _this5.state.pageProps.data; } else if ((route.action || "").match(/\.?(list|read)$/)) { pageProps.data = yield _this5.loadPageData(id, params, query); } return { PageComponent, pageProps }; })(); } loadPageComponent(template) { var _this6 = this; return _asyncToGenerator(function* () { var { pages } = _this6.props; var exports = yield pages(template).catch(() => { throw new _errors.PageNotImplementedError(); }); return exports.default; })(); } loadPageData(operationId, params, filter) { var _this7 = this; return _asyncToGenerator(function* () { if (_this7.api[operationId]) { logger.debug("Loading page data...", operationId, params, filter); try { _this7.admin.loading(); var data = yield _this7.api[operationId](_objectSpread(_objectSpread({}, params), filter)); data.schema = _this7.api[operationId].response; data.getTitle = path => { if (data.schema == null) { throw new TypeError("Cannot get title because .schema is missing."); } return (0, _utils.getFromSchema)(data.schema, "".concat(path, ".title")); }; _this7.admin.loading(false); return data; } catch (error) { _this7.admin.loading(false); throw error; } } logger.debug("Omitting page data...operationId is undefined, no data endpoint found"); return null; })(); } getCustomTitle(pageProps) { var { nav } = this.props; if (nav && pageProps && pageProps.route && pageProps.route.id) { var routeConfig = nav[pageProps.route.id]; if (routeConfig) { return routeConfig.title || pageProps.title; } } return pageProps.title; } mountPage(PageComponent, pageProps) { logger.info("Mount Page:", pageProps); this.admin.loading(false); this.PageComponent = PageComponent; var title = this.getCustomTitle(pageProps); this.setState({ pageProps }, () => { this.setTitle(title); }); } mountErrorPage(title, statusCode) { var _title = title || (0, _utils.t)("Server error"); var _statusCode = statusCode || 500; logger.warn(_title, _statusCode); this.mountPage(_pages.ErrorPage, { key: _statusCode, title: _title, data: { statusCode: _statusCode } }); } unmountPage() { return new Promise(resolve => { if (this.state.pageProps) { logger.info("Un-mounting page...", this.state.pageProps); this.PageComponent = null; this.admin.dismissMessages(); this.setState({ pageProps: null }, resolve); } else { resolve(); } }); } setTitle(title) { if (title) { document.title = "".concat(title, " | ").concat(this.props.title); } else { document.title = this.props.title; } } login(username, password) { return new Promise((resolve, reject) => { this.api["bananas.login:create"]({ data: { username, password } }).then(response => { logger.info("Successful login...reboot"); var user = (0, _utils.makeUser)(response.obj); resolve(user); this.admin.dismissMessages(); this.reboot(user); this.admin.success("".concat((0, _utils.t)("Welcome,"), " ").concat(user.full_name)); }, error => { reject(error); }); }); } logout() { this.api["bananas.logout:create"]().then(() => { this.reboot(); }); } render() { var { PageComponent } = this; var { classes, pageTheme, loginForm, container: Container } = this.props; var { booting, booted, context, settings, pageProps } = this.state; var { user } = context; var isHorizontalLayout = settings.horizontal; var isVerticalLayout = !settings.horizontal; return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("div", { className: (0, _classnames.default)(classes.root, { [classes.admin]: booted && user, [classes.horizontalRoot]: isHorizontalLayout, [classes.verticalRoot]: isVerticalLayout }) }, booted ? /*#__PURE__*/_react.default.createElement(_AdminContext.default.Provider, { value: context }, /*#__PURE__*/_react.default.createElement(Container, null, user ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_NavBar.default, { variant: isHorizontalLayout ? "drawer" : "appbar", dense: settings.dense, permanent: !settings.collapsable, collapsed: settings.collapsed, nav: settings.icons ? this.props.nav : Array.isArray(this.props.nav) ? this.props.nav : this.props.nav ? Object.keys(this.props.nav) : null, logo: this.props.logo, title: this.props.title, branding: this.props.branding, version: this.props.version }), /*#__PURE__*/_react.default.createElement(_Page.Page, _extends({ theme: pageTheme, component: PageComponent, controller: this.controllers.PageLoadController }, pageProps))) : /*#__PURE__*/_react.default.createElement(_pages.LoginPage, { form: loginForm, logger: logger, logo: this.props.logo, title: this.props.title }))) : /*#__PURE__*/_react.default.createElement(_LoadingScreen.default, { role: "bootscreen", logo: this.props.logo, loading: booting })), /*#__PURE__*/_react.default.createElement(_Messages.MessagesController, { ref: this.controllers.MessagesController }), /*#__PURE__*/_react.default.createElement(_Alert.AlertController, { ref: this.controllers.AlertController })); } } var BananasAdmin = (0, _styles2.withStyles)(styles, { name: "BananasAdmin" })(Admin); class App extends _react.default.Component { render() { var { props } = this; var theme = (0, _themes.createBananasTheme)(props.theme); var pageTheme = props.pageTheme ? (0, _themes.createBananasTheme)(props.pageTheme) : undefined; logger.debug("Main Theme:", theme); logger.debug("Page Theme:", pageTheme); return /*#__PURE__*/_react.default.createElement(_styles.StyledEngineProvider, { injectFirst: true }, /*#__PURE__*/_react.default.createElement(_material.ThemeProvider, { theme: theme }, /*#__PURE__*/_react.default.createElement(_CssBaseline.default, null), /*#__PURE__*/_react.default.createElement(BananasAdmin, _objectSpread(_objectSpread({}, props), {}, { theme, pageTheme })))); } } _defineProperty(App, "propTypes", { api: _propTypes.default.oneOfType([_propTypes.default.string.isRequired, _propTypes.default.shape({ url: _propTypes.default.string.isRequired }).isRequired]).isRequired, pages: _propTypes.default.func.isRequired, prefix: _propTypes.default.string, logLevel: _propTypes.default.oneOfType([_propTypes.default.oneOf(["INFO", "DEBUG", "WARN", "ERROR", "OFF"]), _propTypes.default.object]), layout: _propTypes.default.oneOf(["horizontal", "vertical"]), permanent: _propTypes.default.bool, collapsed: _propTypes.default.bool, dense: _propTypes.default.bool, title: _propTypes.default.string, branding: _propTypes.default.string, version: _propTypes.default.string, logo: _propTypes.default.oneOfType([_propTypes.default.bool, _propTypes.default.string, _propTypes.default.node]), nav: _propTypes.default.oneOfType([_propTypes.default.arrayOf(_propTypes.default.string.isRequired), _propTypes.default.object]), theme: _propTypes.default.object, pageTheme: _propTypes.default.object, loginForm: _propTypes.default.func, editableSettings: _propTypes.default.bool, customizeContext: _propTypes.default.func, customizeUser: _propTypes.default.func, container: _propTypes.default.oneOfType([_propTypes.default.symbol, _propTypes.default.func, _propTypes.default.node]) }); _defineProperty(App, "defaultProps", { prefix: "", logLevel: "WARN", layout: "horizontal", dense: false, permanent: false, collapsed: false, title: "Bananas", branding: "Bananas", version: "v3.4.3", // TODO: Get package version logo: true, nav: undefined, theme: _themes.default.default, pageTheme: undefined, loginForm: undefined, editableSettings: false, customizeContext: undefined, customizeUser: undefined, container: _react.default.Fragment }); var _default = exports.default = App;