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