Lsx.jsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import React from 'react';
  2. import * as url from 'url';
  3. import { pathUtils } from '@growi/core';
  4. import axios from 'axios';
  5. import PropTypes from 'prop-types';
  6. // eslint-disable-next-line no-unused-vars
  7. import styles from '../../css/index.css';
  8. import { LsxContext } from '../util/LsxContext';
  9. import { TagCacheManagerFactory } from '../util/TagCacheManagerFactory';
  10. import { LsxListView } from './LsxPageList/LsxListView';
  11. import { PageNode } from './PageNode';
  12. export class Lsx extends React.Component {
  13. constructor(props) {
  14. super(props);
  15. this.state = {
  16. isLoading: false,
  17. isError: false,
  18. isCacheExists: false,
  19. nodeTree: undefined,
  20. basisViewersCount: undefined,
  21. errorMessage: '',
  22. };
  23. this.tagCacheManager = TagCacheManagerFactory.getInstance();
  24. }
  25. async componentDidMount() {
  26. const { lsxContext, forceToFetchData } = this.props;
  27. // get state object cache
  28. const stateCache = this.retrieveDataFromCache();
  29. if (stateCache != null) {
  30. this.setState({
  31. isCacheExists: true,
  32. nodeTree: stateCache.nodeTree,
  33. isError: stateCache.isError,
  34. errorMessage: stateCache.errorMessage,
  35. });
  36. // switch behavior by forceToFetchData
  37. if (!forceToFetchData) {
  38. return; // go to render()
  39. }
  40. }
  41. lsxContext.parse();
  42. this.setState({ isLoading: true });
  43. // add slash ensure not to forward match to another page
  44. // ex: '/Java/' not to match to '/JavaScript'
  45. const pagePath = pathUtils.addTrailingSlash(lsxContext.pagePath);
  46. try {
  47. const res = await axios.get('/_api/plugins/lsx', {
  48. params: {
  49. pagePath,
  50. options: lsxContext.options,
  51. },
  52. });
  53. if (res.data.ok) {
  54. const basisViewersCount = res.data.toppageViewersCount;
  55. const nodeTree = this.generatePageNodeTree(pagePath, res.data.pages);
  56. this.setState({ nodeTree, basisViewersCount });
  57. }
  58. }
  59. catch (error) {
  60. this.setState({ isError: true, errorMessage: error.message });
  61. }
  62. finally {
  63. this.setState({ isLoading: false });
  64. // store to sessionStorage
  65. this.tagCacheManager.cacheState(lsxContext, this.state);
  66. }
  67. }
  68. retrieveDataFromCache() {
  69. const { lsxContext } = this.props;
  70. // get state object cache
  71. const stateCache = this.tagCacheManager.getStateCache(lsxContext);
  72. // instanciate PageNode
  73. if (stateCache != null && stateCache.nodeTree != null) {
  74. stateCache.nodeTree = stateCache.nodeTree.map((obj) => {
  75. return PageNode.instanciateFrom(obj);
  76. });
  77. }
  78. return stateCache;
  79. }
  80. /**
  81. * generate tree structure
  82. *
  83. * @param {string} rootPagePath
  84. * @param {Page[]} pages Array of Page model
  85. *
  86. * @memberOf Lsx
  87. */
  88. generatePageNodeTree(rootPagePath, pages) {
  89. const pathToNodeMap = {};
  90. pages.forEach((page) => {
  91. // add slash ensure not to forward match to another page
  92. // e.g. '/Java/' not to match to '/JavaScript'
  93. const pagePath = pathUtils.addTrailingSlash(page.path);
  94. // exclude rootPagePath itself
  95. if (this.isEquals(pagePath, rootPagePath)) {
  96. return;
  97. }
  98. const node = this.generatePageNode(pathToNodeMap, rootPagePath, pagePath); // this will not be null
  99. // set the Page substance
  100. node.page = page;
  101. });
  102. // return root objects
  103. const rootNodes = [];
  104. Object.keys(pathToNodeMap).forEach((pagePath) => {
  105. // exclude '/'
  106. if (pagePath === '/') {
  107. return;
  108. }
  109. const parentPath = this.getParentPath(pagePath);
  110. // pick up what parent doesn't exist
  111. if ((parentPath === '/') || !(parentPath in pathToNodeMap)) {
  112. rootNodes.push(pathToNodeMap[pagePath]);
  113. }
  114. });
  115. return rootNodes;
  116. }
  117. /**
  118. * generate PageNode instances for target page and the ancestors
  119. *
  120. * @param {any} pathToNodeMap
  121. * @param {any} rootPagePath
  122. * @param {any} pagePath
  123. * @returns
  124. * @memberof Lsx
  125. */
  126. generatePageNode(pathToNodeMap, rootPagePath, pagePath) {
  127. // exclude rootPagePath itself
  128. if (this.isEquals(pagePath, rootPagePath)) {
  129. return null;
  130. }
  131. // return when already registered
  132. if (pathToNodeMap[pagePath] != null) {
  133. return pathToNodeMap[pagePath];
  134. }
  135. // generate node
  136. const node = new PageNode(pagePath);
  137. pathToNodeMap[pagePath] = node;
  138. /*
  139. * process recursively for ancestors
  140. */
  141. // get or create parent node
  142. const parentPath = this.getParentPath(pagePath);
  143. const parentNode = this.generatePageNode(pathToNodeMap, rootPagePath, parentPath);
  144. // associate to patent
  145. if (parentNode != null) {
  146. parentNode.children.push(node);
  147. }
  148. return node;
  149. }
  150. /**
  151. * compare whether path1 and path2 is the same
  152. *
  153. * @param {string} path1
  154. * @param {string} path2
  155. * @returns
  156. *
  157. * @memberOf Lsx
  158. */
  159. isEquals(path1, path2) {
  160. return pathUtils.removeTrailingSlash(path1) === pathUtils.removeTrailingSlash(path2);
  161. }
  162. getParentPath(path) {
  163. return pathUtils.addTrailingSlash(decodeURIComponent(url.resolve(path, '../')));
  164. }
  165. renderContents() {
  166. const lsxContext = this.props.lsxContext;
  167. const {
  168. isLoading, isError, isCacheExists, nodeTree,
  169. } = this.state;
  170. if (isError) {
  171. return (
  172. <div className="text-warning">
  173. <i className="fa fa-exclamation-triangle fa-fw"></i>
  174. {lsxContext.tagExpression} (-&gt; <small>{this.state.errorMessage}</small>)
  175. </div>
  176. );
  177. }
  178. return (
  179. <div className={isLoading ? 'lsx-blink' : ''}>
  180. { isLoading && (
  181. <div className="text-muted">
  182. <i className="fa fa-spinner fa-pulse mr-1"></i>
  183. {lsxContext.tagExpression}
  184. { isCacheExists && <small>&nbsp;(Showing cache..)</small> }
  185. </div>
  186. ) }
  187. { nodeTree && (
  188. <LsxListView nodeTree={this.state.nodeTree} lsxContext={this.props.lsxContext} basisViewersCount={this.state.basisViewersCount} />
  189. ) }
  190. </div>
  191. );
  192. }
  193. render() {
  194. return <div className="lsx">{this.renderContents()}</div>;
  195. }
  196. }
  197. Lsx.propTypes = {
  198. lsxContext: PropTypes.instanceOf(LsxContext).isRequired,
  199. forceToFetchData: PropTypes.bool,
  200. };