Lsx.tsx 6.8 KB

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