Просмотр исходного кода

support: Use SWR (#4487)

* install swr

* refactor api client code

* add interfaces for models

* use SWR to get recent changes data

* export old RecentChanges component temporaly
Yuki Takei 4 лет назад
Родитель
Сommit
6285250108

+ 1 - 0
packages/app/package.json

@@ -232,6 +232,7 @@
     "stylelint": "^13.2.0",
     "stylelint": "^13.2.0",
     "stylelint-config-recess-order": "^2.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "swagger2openapi": "^5.3.1",
     "swagger2openapi": "^5.3.1",
+    "swr": "^1.0.1",
     "terser-webpack-plugin": "^4.1.0",
     "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "toastr": "^2.1.2",

+ 8 - 3
packages/app/src/client/app.jsx

@@ -3,7 +3,10 @@ import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
 import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
 import { I18nextProvider } from 'react-i18next';
 
 
+import { SWRConfig } from 'swr';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 
 import ErrorBoundary from '../components/ErrorBoudary';
 import ErrorBoundary from '../components/ErrorBoudary';
 import Sidebar from '../components/Sidebar';
 import Sidebar from '../components/Sidebar';
@@ -156,9 +159,11 @@ Object.keys(componentMappings).forEach((key) => {
     ReactDOM.render(
     ReactDOM.render(
       <I18nextProvider i18n={i18n}>
       <I18nextProvider i18n={i18n}>
         <ErrorBoundary>
         <ErrorBoundary>
-          <Provider inject={injectableContainers}>
-            {componentMappings[key]}
-          </Provider>
+          <SWRConfig value={swrGlobalConfiguration}>
+            <Provider inject={injectableContainers}>
+              {componentMappings[key]}
+            </Provider>
+          </SWRConfig>
         </ErrorBoundary>
         </ErrorBoundary>
       </I18nextProvider>,
       </I18nextProvider>,
       elem,
       elem,

+ 19 - 96
packages/app/src/client/services/AppContainer.js

@@ -1,10 +1,13 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import urljoin from 'url-join';
-
-import axios from '~/utils/axios';
 import InterceptorManager from '~/services/interceptor-manager';
 import InterceptorManager from '~/services/interceptor-manager';
 
 
+import {
+  apiDelete, apiGet, apiPost, apiRequest,
+} from '../util/apiv1-client';
+import {
+  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
+} from '../util/apiv3-client';
 import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import GrowiRenderer from '../util/GrowiRenderer';
 import GrowiRenderer from '../util/GrowiRenderer';
 
 
@@ -12,10 +15,8 @@ import {
   mediaQueryListForDarkMode,
   mediaQueryListForDarkMode,
   applyColorScheme,
   applyColorScheme,
 } from '../util/color-scheme';
 } from '../util/color-scheme';
-import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
 
 
 import { i18nFactory } from '../util/i18n';
 import { i18nFactory } from '../util/i18n';
-import apiv3ErrorHandler from '../util/apiv3ErrorHandler';
 
 
 /**
 /**
  * Service container related to options for Application
  * Service container related to options for Application
@@ -28,9 +29,6 @@ export default class AppContainer extends Container {
 
 
     this.state = {
     this.state = {
       preferDarkModeByMediaQuery: false,
       preferDarkModeByMediaQuery: false,
-
-      // stetes for contents
-      recentlyUpdatedPages: [],
     };
     };
 
 
     const body = document.querySelector('body');
     const body = document.querySelector('body');
@@ -60,17 +58,21 @@ export default class AppContainer extends Container {
     this.componentInstances = {};
     this.componentInstances = {};
     this.rendererInstances = {};
     this.rendererInstances = {};
 
 
-    this.apiGet = this.apiGet.bind(this);
-    this.apiPost = this.apiPost.bind(this);
-    this.apiDelete = this.apiDelete.bind(this);
-    this.apiRequest = this.apiRequest.bind(this);
+    this.apiGet = apiGet;
+    this.apiPost = apiPost;
+    this.apiDelete = apiDelete;
+    this.apiRequest = apiRequest;
+
+    this.apiv3Get = apiv3Get;
+    this.apiv3Post = apiv3Post;
+    this.apiv3Put = apiv3Put;
+    this.apiv3Delete = apiv3Delete;
 
 
-    this.apiv3Root = '/_api/v3';
     this.apiv3 = {
     this.apiv3 = {
-      get: this.apiv3Get.bind(this),
-      post: this.apiv3Post.bind(this),
-      put: this.apiv3Put.bind(this),
-      delete: this.apiv3Delete.bind(this),
+      get: apiv3Get,
+      post: apiv3Post,
+      put: apiv3Put,
+      delete: apiv3Delete,
     };
     };
   }
   }
 
 
@@ -279,11 +281,6 @@ export default class AppContainer extends Container {
     });
     });
   }
   }
 
 
-  async retrieveRecentlyUpdated() {
-    const { data } = await this.apiv3Get('/pages/recent');
-    this.setState({ recentlyUpdatedPages: data.pages });
-  }
-
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
     let targetComponent;
     let targetComponent;
     switch (componentKind) {
     switch (componentKind) {
@@ -304,78 +301,4 @@ export default class AppContainer extends Container {
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
   }
   }
 
 
-  async apiGet(path, params) {
-    return this.apiRequest('get', path, { params });
-  }
-
-  async apiPost(path, params) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiRequest('post', path, params);
-  }
-
-  async apiDelete(path, params) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiRequest('delete', path, { data: params });
-  }
-
-  async apiRequest(method, path, params) {
-    const res = await axios[method](`/_api${path}`, params);
-    if (res.data.ok) {
-      return res.data;
-    }
-
-    // Return error code if code is exist
-    if (res.data.code != null) {
-      const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
-      throw error;
-    }
-
-    throw new Error(res.data.error);
-  }
-
-  async apiv3Request(method, path, params) {
-    try {
-      const res = await axios[method](urljoin(this.apiv3Root, path), params);
-      return res.data;
-    }
-    catch (err) {
-      const errors = apiv3ErrorHandler(err);
-      throw errors;
-    }
-  }
-
-  async apiv3Get(path, params) {
-    return this.apiv3Request('get', path, { params });
-  }
-
-  async apiv3Post(path, params = {}) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiv3Request('post', path, params);
-  }
-
-  async apiv3Put(path, params = {}) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiv3Request('put', path, params);
-  }
-
-  async apiv3Delete(path, params = {}) {
-    if (!params._csrf) {
-      params._csrf = this.csrfToken;
-    }
-
-    return this.apiv3Request('delete', path, { params });
-  }
-
 }
 }

+ 47 - 0
packages/app/src/client/util/apiv1-client.ts

@@ -0,0 +1,47 @@
+import * as urljoin from 'url-join';
+
+import axios from '~/utils/axios';
+
+const apiv1Root = '/_api';
+
+
+class Apiv1ErrorHandler extends Error {
+
+  code;
+
+  constructor(message = '', code = '') {
+    super();
+
+    this.message = message;
+    this.code = code;
+  }
+
+}
+
+export async function apiRequest(method: string, path: string, params: unknown): Promise<unknown> {
+  const res = await axios[method](urljoin(apiv1Root, path), params);
+
+  if (res.data.ok) {
+    return res.data;
+  }
+
+  // Return error code if code is exist
+  if (res.data.code != null) {
+    const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
+    throw error;
+  }
+
+  throw new Error(res.data.error);
+}
+
+export async function apiGet(path: string, params: unknown = {}): Promise<unknown> {
+  return apiRequest('get', path, { params });
+}
+
+export async function apiPost(path: string, params: unknown = {}): Promise<unknown> {
+  return apiRequest('post', path, params);
+}
+
+export async function apiDelete(path: string, params: unknown = {}): Promise<unknown> {
+  return apiRequest('delete', path, { data: params });
+}

+ 0 - 12
packages/app/src/client/util/apiv1ErrorHandler.js

@@ -1,12 +0,0 @@
-class Apiv1ErrorHandler extends Error {
-
-  constructor(message = '', code = '') {
-    super();
-
-    this.message = message;
-    this.code = code;
-  }
-
-}
-
-module.exports = Apiv1ErrorHandler;

+ 56 - 0
packages/app/src/client/util/apiv3-client.ts

@@ -0,0 +1,56 @@
+import * as urljoin from 'url-join';
+
+// eslint-disable-next-line no-restricted-imports
+import { AxiosResponse } from 'axios';
+
+import loggerFactory from '~/utils/logger';
+import axios from '~/utils/axios';
+import { toArrayIfNot } from '~/utils/array-utils';
+
+const apiv3Root = '/_api/v3';
+
+const logger = loggerFactory('growi:apiv3');
+
+const apiv3ErrorHandler = (_err) => {
+  // extract api errors from general 400 err
+  const err = _err.response ? _err.response.data.errors : _err;
+  const errs = toArrayIfNot(err);
+
+  for (const err of errs) {
+    logger.error(err.message);
+  }
+
+  return errs;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Request<T = any>(method: string, path: string, params: unknown): Promise<AxiosResponse<T>> {
+  try {
+    const res = await axios[method](urljoin(apiv3Root, path), params);
+    return res.data;
+  }
+  catch (err) {
+    const errors = apiv3ErrorHandler(err);
+    throw errors;
+  }
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Get<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+  return apiv3Request('get', path, { params });
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Post<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+  return apiv3Request('post', path, params);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Put<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+  return apiv3Request('put', path, params);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3Delete<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+  return apiv3Request('delete', path, { params });
+}

+ 0 - 21
packages/app/src/client/util/apiv3ErrorHandler.js

@@ -1,21 +0,0 @@
-// API v3 sends an array of errors in res.data.errors.
-// API v3 errors need to extracted from an error object in order to properly handle them.
-
-import loggerFactory from '~/utils/logger';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-const logger = loggerFactory('growi:apiv3');
-
-const apiv3ErrorHandler = (_err, header = 'Error') => {
-  // extract api errors from general 400 err
-  const err = _err.response ? _err.response.data.errors : _err;
-  const errs = toArrayIfNot(err);
-
-  for (const err of errs) {
-    logger.error(err.message);
-  }
-
-  return errs;
-};
-
-export default apiv3ErrorHandler;

+ 92 - 32
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -1,20 +1,23 @@
-import React from 'react';
+import React, {
+  useCallback, useEffect, useState,
+} from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { withTranslation } from 'react-i18next';
+import { useTranslation, withTranslation } from 'react-i18next';
 
 
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
 import { DevidedPagePath } from '@growi/core';
+
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { toastError } from '~/client/util/apiNotification';
+import { useSWRxRecentlyUpdated } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
 import FootstampIcon from '../FootstampIcon';
 import FootstampIcon from '../FootstampIcon';
 
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import { toastError } from '~/client/util/apiNotification';
 
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import FormattedDistanceDate from '../FormattedDistanceDate';
 
 
@@ -119,17 +122,82 @@ function SmallPageItem({ page }) {
 SmallPageItem.propTypes = {
 SmallPageItem.propTypes = {
   page: PropTypes.any,
   page: PropTypes.any,
 };
 };
-class RecentChanges extends React.Component {
+
+
+const RecentChanges = () => {
+
+  const { t } = useTranslation();
+  const { data: pages, error, mutate } = useSWRxRecentlyUpdated();
+
+  if (error != null) {
+    toastError(error, 'Error occurred in updating History');
+  }
+
+  const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
+
+  const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
+    if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
+      setIsRecentChangesSidebarSmall(true);
+    }
+  });
+
+  const changeSizeHandler = useCallback((e) => {
+    setIsRecentChangesSidebarSmall(e.target.checked);
+    window.localStorage.setItem('isRecentChangesSidebarSmall', e.target.checked);
+  }, []);
+
+  // componentDidMount
+  useEffect(() => {
+    retrieveSizePreferenceFromLocalStorage();
+  }, [retrieveSizePreferenceFromLocalStorage]);
+
+  return (
+    <>
+      <div className="grw-sidebar-content-header p-3 d-flex">
+        <h3 className="mb-0  text-nowrap">{t('Recent Changes')}</h3>
+        <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+          <i className="icon icon-reload"></i>
+        </button>
+        <div className="d-flex align-items-center">
+          <div className="grw-recent-changes-resize-button custom-control custom-switch ml-1">
+            <input
+              id="recentChangesResize"
+              className="custom-control-input"
+              type="checkbox"
+              checked={isRecentChangesSidebarSmall}
+              onChange={changeSizeHandler}
+            />
+            <label className="custom-control-label" htmlFor="recentChangesResize">
+            </label>
+          </div>
+        </div>
+      </div>
+      <div className="grw-sidebar-content-body grw-recent-changes p-3">
+        <ul className="list-group list-group-flush">
+          {(pages || []).map(page => (isRecentChangesSidebarSmall
+            ? <SmallPageItem key={page._id} page={page} />
+            : <LargePageItem key={page._id} page={page} />))}
+        </ul>
+      </div>
+    </>
+  );
+
+};
+
+// export default RecentChanges;
+
+
+class DeprecatedRecentChanges extends React.Component {
 
 
   static propTypes = {
   static propTypes = {
     t: PropTypes.func.isRequired, // i18next
     t: PropTypes.func.isRequired, // i18next
-    appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   };
   };
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
     this.state = {
     this.state = {
       isRecentChangesSidebarSmall: false,
       isRecentChangesSidebarSmall: false,
+      recentlyUpdatedPages: [],
     };
     };
     this.reloadData = this.reloadData.bind(this);
     this.reloadData = this.reloadData.bind(this);
   }
   }
@@ -143,10 +211,9 @@ class RecentChanges extends React.Component {
   }
   }
 
 
   async reloadData() {
   async reloadData() {
-    const { appContainer } = this.props;
-
     try {
     try {
-      await appContainer.retrieveRecentlyUpdated();
+      const { data } = await apiv3Get('/pages/recent');
+      this.setState({ recentlyUpdatedPages: data.pages });
     }
     }
     catch (error) {
     catch (error) {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
@@ -171,32 +238,30 @@ class RecentChanges extends React.Component {
 
 
   render() {
   render() {
     const { t } = this.props;
     const { t } = this.props;
-    const { recentlyUpdatedPages } = this.props.appContainer.state;
 
 
     return (
     return (
       <>
       <>
         <div className="grw-sidebar-content-header p-3 d-flex">
         <div className="grw-sidebar-content-header p-3 d-flex">
-          <h3 className="mb-0  text-nowrap">{t('Recent Changes')}</h3>
-          <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={this.reloadData}>
+          <h3 className="mb-0">{t('Recent Changes')}</h3>
+          {/* <h3 className="mb-0">{t('Recent Created')}</h3> */} {/* TODO: impl switching */}
+          <button type="button" className="btn btn-sm ml-auto grw-btn-reload-rc" onClick={this.reloadData}>
             <i className="icon icon-reload"></i>
             <i className="icon icon-reload"></i>
           </button>
           </button>
-          <div className="d-flex align-items-center">
-            <div className="grw-recent-changes-resize-button custom-control custom-switch ml-1">
-              <input
-                id="recentChangesResize"
-                className="custom-control-input"
-                type="checkbox"
-                checked={this.state.isRecentChangesSidebarSmall}
-                onChange={this.changeSizeHandler}
-              />
-              <label className="custom-control-label" htmlFor="recentChangesResize">
-              </label>
-            </div>
+          <div className="grw-recent-changes-resize-button custom-control custom-switch ml-2">
+            <input
+              id="recentChangesResize"
+              className="custom-control-input"
+              type="checkbox"
+              checked={this.state.isRecentChangesSidebarSmall}
+              onChange={this.changeSizeHandler}
+            />
+            <label className="custom-control-label" htmlFor="recentChangesResize">
+            </label>
           </div>
           </div>
         </div>
         </div>
         <div className="grw-sidebar-content-body grw-recent-changes p-3">
         <div className="grw-sidebar-content-body grw-recent-changes p-3">
           <ul className="list-group list-group-flush">
           <ul className="list-group list-group-flush">
-            {recentlyUpdatedPages.map(page => (this.state.isRecentChangesSidebarSmall
+            {this.state.recentlyUpdatedPages.map(page => (this.state.isRecentChangesSidebarSmall
               ? <SmallPageItem key={page._id} page={page} />
               ? <SmallPageItem key={page._id} page={page} />
               : <LargePageItem key={page._id} page={page} />))}
               : <LargePageItem key={page._id} page={page} />))}
           </ul>
           </ul>
@@ -207,10 +272,5 @@ class RecentChanges extends React.Component {
 
 
 }
 }
 
 
-/**
- * Wrapper component for using unstated
- */
-const RecentChangesWrapper = withUnstatedContainers(RecentChanges, [AppContainer]);
-
 
 
-export default withTranslation()(RecentChangesWrapper);
+export default withTranslation()(DeprecatedRecentChanges);

+ 7 - 0
packages/app/src/interfaces/page-tag-relation.ts

@@ -0,0 +1,7 @@
+import { IPage } from './page';
+import { ITag } from './tag';
+
+export type IPageTagRelation = {
+  relatedPage: IPage,
+  relatedTag: ITag,
+}

+ 14 - 0
packages/app/src/interfaces/page.ts

@@ -0,0 +1,14 @@
+import { IUser } from './user';
+import { IRevision } from './revision';
+import { ITag } from './tag';
+
+export type IPage = {
+  path: string,
+  status: string,
+  revision: IRevision,
+  tags: ITag[],
+  creator: IUser,
+  createdAt: Date,
+  updatedAt: Date,
+  seenUsers: string[]
+}

+ 9 - 0
packages/app/src/interfaces/revision.ts

@@ -0,0 +1,9 @@
+import { IUser } from './user';
+
+export type IRevision = {
+  body: string,
+  author: IUser,
+  hasDiffToPrev: boolean;
+  createdAt: Date,
+  updatedAt: Date,
+}

+ 4 - 0
packages/app/src/interfaces/tag.ts

@@ -0,0 +1,4 @@
+export type ITag = {
+  name: string,
+  createdAt: Date;
+}

+ 18 - 0
packages/app/src/interfaces/user.ts

@@ -0,0 +1,18 @@
+export type IUser = {
+  name: string;
+  username: string;
+  imageUrlCached: string;
+  admin: boolean;
+}
+
+export type IUserGroupRelation = {
+  relatedGroup: IUserGroup,
+  relatedUser: IUser,
+  createdAt: Date,
+}
+
+export type IUserGroup = {
+  userGroupId:string;
+  name: string;
+  createdAt: Date;
+}

+ 13 - 0
packages/app/src/stores/page.tsx

@@ -0,0 +1,13 @@
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+
+import { IPage } from '~/interfaces/page';
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const useSWRxRecentlyUpdated = <Data, Error>(): SWRResponse<IPage[], Error> => {
+  return useSWR(
+    '/pages/recent',
+    endpoint => apiv3Get<{ pages: IPage[] }>(endpoint).then(response => response.data?.pages),
+  );
+};

+ 9 - 0
packages/app/src/utils/swr-utils.ts

@@ -0,0 +1,9 @@
+import { SWRConfiguration } from 'swr';
+
+import axios from './axios';
+
+export const swrGlobalConfiguration: SWRConfiguration = {
+  fetcher: url => axios.get(url).then(res => res.data),
+  revalidateOnFocus: false,
+  errorRetryCount: 1,
+};

+ 12 - 0
yarn.lock

@@ -7222,6 +7222,11 @@ deprecation@^2.0.0, deprecation@^2.3.1:
   resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
   resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
   integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
   integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
 
 
+dequal@2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
+  integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
+
 des.js@^1.0.0:
 des.js@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
   resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
@@ -20127,6 +20132,13 @@ swig-templates@^2.0.2:
     optimist "~0.6"
     optimist "~0.6"
     uglify-js "2.6.0"
     uglify-js "2.6.0"
 
 
+swr@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/swr/-/swr-1.0.1.tgz#15f62846b87ee000e52fa07812bb65eb62d79483"
+  integrity sha512-EPQAxSjoD4IaM49rpRHK0q+/NzcwoT8c0/Ylu/u3/6mFj/CWnQVjNJ0MV2Iuw/U+EJSd2TX5czdAwKPYZIG0YA==
+  dependencies:
+    dequal "2.0.2"
+
 symbol-observable@1.0.1:
 symbol-observable@1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"