Kaynağa Gözat

Merge branch 'master' into imprv/devide-admin-jsx

# Conflicts:
#	src/server/views/layout/admin.html
Yuki Takei 6 yıl önce
ebeveyn
işleme
aee226a998
32 değiştirilmiş dosya ile 289 ekleme ve 385 silme
  1. 13 4
      CHANGES.md
  2. 1 2
      config/webpack.common.js
  3. 1 1
      package.json
  4. 1 0
      resource/locales/en-US/translation.json
  5. 1 0
      resource/locales/ja/translation.json
  6. 8 8
      src/client/js/components/Admin/App/AppSetting.jsx
  7. 6 6
      src/client/js/components/Admin/App/AwsSetting.jsx
  8. 5 5
      src/client/js/components/Admin/App/MailSetting.jsx
  9. 4 4
      src/client/js/components/Admin/App/SiteUrlSetting.jsx
  10. 21 20
      src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx
  11. 15 3
      src/client/js/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  12. 3 2
      src/client/js/components/Admin/ElasticsearchManagement/ReconnectControls.jsx
  13. 6 2
      src/client/js/components/Admin/ElasticsearchManagement/StatusTable.jsx
  14. 13 3
      src/client/js/components/SavePageControls.jsx
  15. 3 3
      src/client/js/components/SearchPage.jsx
  16. 0 5
      src/client/js/legacy/crowi-admin.js
  17. 0 233
      src/client/js/legacy/thirdparty-js/jQuery.style.switcher.js
  18. 7 7
      src/client/js/services/AdminAppContainer.js
  19. 36 1
      src/client/js/services/AdminCustomizeContainer.js
  20. 2 17
      src/server/crowi/index.js
  21. 0 5
      src/server/routes/admin.js
  22. 5 5
      src/server/routes/apiv3/app-settings.js
  23. 47 4
      src/server/routes/apiv3/customize-setting.js
  24. 8 6
      src/server/routes/apiv3/healthcheck.js
  25. 27 9
      src/server/routes/apiv3/search.js
  26. 3 3
      src/server/routes/installer.js
  27. 4 8
      src/server/routes/search.js
  28. 23 4
      src/server/service/search-delegator/elasticsearch.js
  29. 2 2
      src/server/util/swigFunctions.js
  30. 23 11
      src/server/views/admin/customize.html
  31. 0 1
      src/server/views/layout/admin.html
  32. 1 1
      src/server/views/widget/headers/styles-theme.html

+ 13 - 4
CHANGES.md

@@ -1,20 +1,29 @@
 # CHANGES
 
-## v3.6.6-RC
+## 3.6.7-RC
 
-* 
+* Imprv: Show error toastr when saving page is failed because of empty document
+* Fix: Admin Customise missed preview functions
+    * Introduced by 3.6.2
+* Fix: AWS doesn't work
+    * Introduced by 3.6.4
+* Fix: Ensure not to get unrelated indices information in Elasticsearch Management
+    * Introduced by 3.6.6
 
-## v3.6.5
+## v3.6.6
 
 * Feature: Reconnect to Elasticsearch from Full Text Search Management
 * Feature: Normalize indices of Elasticsearch from Full Text Search Management
 * Improvement: Add 'spring' theme
 * Improvement: Reactify admin pages (Notification)
-* Impromvement: Add `checkMiddlewaresStrictly` query option to Healthcheck API
+* Impromvement: Add `checkMiddlewaresStrictly` option to Healthcheck API
 * Improvement: Accessibility for History component under dark themes
+* Fix: Warning on client console when developing /admin/app
 * Support: Upgrade libs
     * react-bootstrap-typeahead
 
+## v3.6.5 (Missing number)
+
 ## v3.6.4
 
 * Feature: Alert for stale page

+ 1 - 2
config/webpack.common.js

@@ -24,7 +24,6 @@ module.exports = (options) => {
       'js/admin':                     './src/client/js/admin',
       'js/installer':                 './src/client/js/installer',
       'js/legacy':                    './src/client/js/legacy/crowi',
-      'js/legacy-admin':              './src/client/js/legacy/crowi-admin',
       'js/legacy-presentation':       './src/client/js/legacy/crowi-presentation',
       'js/plugin':                    './src/client/js/plugin',
       'js/ie11-polyfill':             './src/client/js/ie11-polyfill',
@@ -159,7 +158,7 @@ module.exports = (options) => {
             test: /\.(sc|sa|c)ss$/,
             chunks: (chunk) => {
               // ignore patterns
-              return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-admin|legacy-presentation/);
+              return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-presentation/);
             },
             name: 'styles/style-commons',
             minSize: 1,

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.6.6-RC",
+  "version": "3.6.7-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 1 - 0
resource/locales/en-US/translation.json

@@ -566,6 +566,7 @@
   "full_text_search_management": {
     "elasticsearch_management": "Elasticsearch Management",
     "connection_status": "Connection Status",
+    "connection_status_label_unconfigured": "UNCONFIGURED",
     "connection_status_label_connected": "CONNECTED",
     "connection_status_label_disconnected": "DISCONNECTED",
     "indices_status": "Indices Status",

+ 1 - 0
resource/locales/ja/translation.json

@@ -549,6 +549,7 @@
   "full_text_search_management": {
     "elasticsearch_management": "Elasticsearch 管理",
     "connection_status": "接続の状態",
+    "connection_status_label_unconfigured": "設定されていません",
     "connection_status_label_connected": "接続されています",
     "connection_status_label_disconnected": "切断されています",
     "indices_status": "インデックスの状態",

+ 8 - 8
src/client/js/components/Admin/App/AppSetting.jsx

@@ -38,13 +38,13 @@ class AppSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <div className="row md-5">
+        <div className="row mb-5">
           <label className="col-xs-3 control-label">{t('admin:app_setting.site_name')}</label>
           <div className="col-xs-6">
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.title}
+              defaultValue={adminAppContainer.state.title || ''}
               onChange={(e) => { adminAppContainer.changeTitle(e.target.value) }}
               placeholder="GROWI"
             />
@@ -52,13 +52,13 @@ class AppSetting extends React.Component {
           </div>
         </div>
 
-        <div className="row md-5">
+        <div className="row mb-5">
           <label className="col-xs-3 control-label">{t('admin:app_setting.confidential_name')}</label>
           <div className="col-xs-6">
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.confidential}
+              defaultValue={adminAppContainer.state.confidential || ''}
               onChange={(e) => { adminAppContainer.changeConfidential(e.target.value) }}
               placeholder={t('admin:app_setting.confidential_example')}
             />
@@ -66,7 +66,7 @@ class AppSetting extends React.Component {
           </div>
         </div>
 
-        <div className="row md-5">
+        <div className="row mb-5">
           <label className="col-xs-3 control-label">{t('admin:app_setting.default_language')}</label>
           <div className="col-xs-6">
             <div className="radio radio-primary radio-inline">
@@ -76,7 +76,7 @@ class AppSetting extends React.Component {
                 name="globalLang"
                 value="en-US"
                 checked={adminAppContainer.state.globalLang === 'en-US'}
-                onClick={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
+                onChange={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
               />
               <label htmlFor="radioLangEn">{t('English')}</label>
             </div>
@@ -87,14 +87,14 @@ class AppSetting extends React.Component {
                 name="globalLang"
                 value="ja"
                 checked={adminAppContainer.state.globalLang === 'ja'}
-                onClick={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
+                onChange={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
               />
               <label htmlFor="radioLangJa">{t('Japanese')}</label>
             </div>
           </div>
         </div>
 
-        <div className="row md-5">
+        <div className="row mb-5">
           <label className="col-xs-3 control-label">{t('admin:app_setting.file_uploading')}</label>
           <div className="col-xs-6">
             <div className="checkbox checkbox-info">

+ 6 - 6
src/client/js/components/Admin/App/AwsSetting.jsx

@@ -58,7 +58,7 @@ class AwsSetting extends React.Component {
             <input
               className="form-control"
               placeholder={`${t('eg')} ap-northeast-1`}
-              defaultValue={adminAppContainer.state.region}
+              defaultValue={adminAppContainer.state.region || ''}
               onChange={(e) => {
                 adminAppContainer.changeRegion(e.target.value);
               }}
@@ -75,7 +75,7 @@ class AwsSetting extends React.Component {
               className="form-control"
               type="text"
               placeholder={`${t('eg')} http://localhost:9000`}
-              defaultValue={adminAppContainer.state.customEndpoint}
+              defaultValue={adminAppContainer.state.customEndpoint || ''}
               onChange={(e) => {
                 adminAppContainer.changeCustomEndpoint(e.target.value);
               }}
@@ -93,7 +93,7 @@ class AwsSetting extends React.Component {
               className="form-control"
               type="text"
               placeholder={`${t('eg')} crowi`}
-              defaultValue={adminAppContainer.state.bucket}
+              defaultValue={adminAppContainer.state.bucket || ''}
               onChange={(e) => {
                 adminAppContainer.changeBucket(e.target.value);
               }}
@@ -109,7 +109,7 @@ class AwsSetting extends React.Component {
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.accessKeyId}
+              defaultValue={adminAppContainer.state.accessKeyId || ''}
               onChange={(e) => {
                 adminAppContainer.changeAccessKeyId(e.target.value);
               }}
@@ -125,9 +125,9 @@ class AwsSetting extends React.Component {
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.secretKey}
+              defaultValue={adminAppContainer.state.secretAccessKey || ''}
               onChange={(e) => {
-                adminAppContainer.changeSecretKey(e.target.value);
+                adminAppContainer.changeSecretAccessKey(e.target.value);
               }}
             />
           </div>

+ 5 - 5
src/client/js/components/Admin/App/MailSetting.jsx

@@ -46,7 +46,7 @@ class MailSetting extends React.Component {
               className="form-control"
               type="text"
               placeholder={`${t('eg')} mail@growi.org`}
-              defaultValue={adminAppContainer.state.fromAddress}
+              defaultValue={adminAppContainer.state.fromAddress || ''}
               onChange={(e) => { adminAppContainer.changeFromAddress(e.target.value) }}
             />
           </div>
@@ -59,7 +59,7 @@ class MailSetting extends React.Component {
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.smtpHost}
+              defaultValue={adminAppContainer.state.smtpHost || ''}
               onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
             />
           </div>
@@ -67,7 +67,7 @@ class MailSetting extends React.Component {
             <label>{t('admin:app_setting.port')}</label>
             <input
               className="form-control"
-              defaultValue={adminAppContainer.state.smtpPort}
+              defaultValue={adminAppContainer.state.smtpPort || ''}
               onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
             />
           </div>
@@ -79,7 +79,7 @@ class MailSetting extends React.Component {
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.SmtpUser}
+              defaultValue={adminAppContainer.state.SmtpUser || ''}
               onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
             />
           </div>
@@ -88,7 +88,7 @@ class MailSetting extends React.Component {
             <input
               className="form-control"
               type="password"
-              defaultValue={adminAppContainer.state.smtpPassword}
+              defaultValue={adminAppContainer.state.smtpPassword || ''}
               onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
             />
           </div>

+ 4 - 4
src/client/js/components/Admin/App/SiteUrlSetting.jsx

@@ -63,20 +63,20 @@ class SiteUrlSetting extends React.Component {
                         className="form-control"
                         type="text"
                         name="settingForm[app:siteUrl]"
-                        defaultValue={adminAppContainer.state.siteUrl}
+                        defaultValue={adminAppContainer.state.siteUrl || ''}
                         onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
                         placeholder="e.g. https://my.growi.org"
                       />
                       <p className="help-block">
                         {/* eslint-disable-next-line react/no-danger */}
-                        <div dangerouslySetInnerHTML={{ __html: t('admin:app_setting.siteurl_help') }} />
+                        <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.siteurl_help') }} />
                       </p>
                     </td>
                     <td>
-                      <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl} readOnly />
+                      <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl || ''} readOnly />
                       <p className="help-block">
                         {/* eslint-disable-next-line react/no-danger */}
-                        <div dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
+                        <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
                       </p>
                     </td>
                   </tr>

+ 21 - 20
src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx

@@ -36,21 +36,26 @@ class CustomizeHighlightSetting extends React.Component {
     }
   }
 
-  getDemoFunction() {
-    return `function $initHighlight(block, cls) {
-    try {
+  renderHljsDemo() {
+    const { adminCustomizeContainer } = this.props;
 
-      if (cls.search(/\bno\-highlight\b/) !== -1) {
-        return \`\${process(block, true, 0x0F)} class="\${cls}"\`;
-      }
-    }
-    catch (e) {
-      /* handle exception */
-    }
-    for (let i = 0 / 2; i < classes.length; i++) {
-      if (checkCondition(classes[i]) === undefined) { console.log('undefined') }
-    }
-  };`;
+    /* eslint-disable max-len */
+    const html = `<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MersenneTwister</span>(<span class="hljs-params">seed</span>) </span>{
+  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">arguments</span>.length == <span class="hljs-number">0</span>) {
+    seed = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();
+  }
+
+  <span class="hljs-keyword">this</span>._mt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Array</span>(<span class="hljs-number">624</span>);
+  <span class="hljs-keyword">this</span>.setSeed(seed);
+}</span>`;
+    /* eslint-enable max-len */
+
+    return (
+      <pre className={`hljs ${!adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
+        {/* eslint-disable-next-line react/no-danger */}
+        <code dangerouslySetInnerHTML={{ __html: html }}></code>
+      </pre>
+    );
   }
 
   render() {
@@ -65,7 +70,7 @@ class CustomizeHighlightSetting extends React.Component {
 
       menuItem.push(
         <li key={styleId} role="presentation" type="button" onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}>
-          <a role="menuitem">{styleName}</a>
+          <a role="button">{styleName}</a>
         </li>,
       );
     });
@@ -115,11 +120,7 @@ class CustomizeHighlightSetting extends React.Component {
         <div className="help-block">
           <label>Examples:</label>
           <div className="wiki">
-            <pre className={`hljs ${!adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
-              <code className="highlightjs-demo">
-                {this.getDemoFunction()}
-              </code>
-            </pre>
+            {this.renderHljsDemo()}
           </div>
         </div>
 

+ 15 - 3
src/client/js/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx

@@ -18,6 +18,7 @@ class ElasticsearchManagement extends React.Component {
     super(props);
 
     this.state = {
+      isConfigured: null,
       isConnected: null,
       isRebuildingProcessing: false,
       isRebuildingCompleted: false,
@@ -68,6 +69,7 @@ class ElasticsearchManagement extends React.Component {
       const { info } = await appContainer.apiv3Get('/search/indices');
 
       this.setState({
+        isConfigured: true,
         isConnected: true,
 
         indicesData: info.indices,
@@ -75,9 +77,17 @@ class ElasticsearchManagement extends React.Component {
         isNormalized: info.isNormalized,
       });
     }
-    catch (e) {
+    catch (errors) {
       this.setState({ isConnected: false });
-      toastError(e);
+
+      // evaluate whether configured or not
+      for (const error of errors) {
+        if (error.code === 'search-service-unconfigured') {
+          this.setState({ isConfigured: false });
+        }
+      }
+
+      toastError(errors);
     }
   }
 
@@ -130,7 +140,7 @@ class ElasticsearchManagement extends React.Component {
   render() {
     const { t } = this.props;
     const {
-      isConnected, isRebuildingProcessing, isRebuildingCompleted,
+      isConfigured, isConnected, isRebuildingProcessing, isRebuildingCompleted,
       isNormalized, indicesData, aliasesData,
     } = this.state;
 
@@ -139,6 +149,7 @@ class ElasticsearchManagement extends React.Component {
         <div className="row">
           <div className="col-xs-12">
             <StatusTable
+              isConfigured={isConfigured}
               isConnected={isConnected}
               isNormalized={isNormalized}
               indicesData={indicesData}
@@ -154,6 +165,7 @@ class ElasticsearchManagement extends React.Component {
           <label className="col-xs-3 control-label">{ t('full_text_search_management.reconnect') }</label>
           <div className="col-xs-6">
             <ReconnectControls
+              isConfigured={isConfigured}
               isConnected={isConnected}
               onReconnectingRequested={this.reconnect}
             />

+ 3 - 2
src/client/js/components/Admin/ElasticsearchManagement/ReconnectControls.jsx

@@ -7,9 +7,9 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 class ReconnectControls extends React.PureComponent {
 
   render() {
-    const { t, isConnected } = this.props;
+    const { t, isConfigured, isConnected } = this.props;
 
-    const isEnabled = (isConnected != null) && !isConnected;
+    const isEnabled = (isConfigured == null || isConfigured === true) && isConnected === false;
 
     return (
       <>
@@ -41,6 +41,7 @@ const ReconnectControlsWrapper = (props) => {
 ReconnectControls.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
+  isConfigured: PropTypes.bool,
   isConnected: PropTypes.bool,
   onReconnectingRequested: PropTypes.func.isRequired,
 };

+ 6 - 2
src/client/js/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -95,11 +95,14 @@ class StatusTable extends React.PureComponent {
 
   render() {
     const { t } = this.props;
-    const { isConnected, isNormalized } = this.props;
+    const { isConfigured, isConnected, isNormalized } = this.props;
 
 
     let connectionStatusLabel = <span className="label label-default">――</span>;
-    if (isConnected != null) {
+    if (isConfigured != null && !isConfigured) {
+      connectionStatusLabel = <span className="label label-default">{ t('full_text_search_management.connection_status_label_unconfigured') }</span>;
+    }
+    else if (isConnected != null) {
       connectionStatusLabel = isConnected
         ? <span className="label label-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
         : <span className="label label-danger">{ t('full_text_search_management.connection_status_label_disconnected') }</span>;
@@ -146,6 +149,7 @@ const StatusTableWrapper = (props) => {
 StatusTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
+  isConfigured: PropTypes.bool,
   isConnected: PropTypes.bool,
   isNormalized: PropTypes.bool,
   indicesData: PropTypes.object,

+ 13 - 3
src/client/js/components/SavePageControls.jsx

@@ -7,6 +7,8 @@ import ButtonToolbar from 'react-bootstrap/es/ButtonToolbar';
 import SplitButton from 'react-bootstrap/es/SplitButton';
 import MenuItem from 'react-bootstrap/es/MenuItem';
 
+import loggerFactory from '@alias/logger';
+
 import PageContainer from '../services/PageContainer';
 import AppContainer from '../services/AppContainer';
 import EditorContainer from '../services/EditorContainer';
@@ -15,6 +17,7 @@ import { createSubscribedElement } from './UnstatedUtils';
 import SlackNotification from './SlackNotification';
 import GrantSelector from './SavePageControls/GrantSelector';
 
+const logger = loggerFactory('growi:SavePageControls');
 
 class SavePageControls extends React.Component {
 
@@ -45,12 +48,19 @@ class SavePageControls extends React.Component {
     this.props.editorContainer.setState(data);
   }
 
-  save() {
+  async save() {
     const { pageContainer, editorContainer } = this.props;
     // disable unsaved warning
     editorContainer.disableUnsavedWarning();
-    // save
-    pageContainer.saveAndReload(editorContainer.getCurrentOptionsToSave());
+
+    try {
+      // save
+      await pageContainer.saveAndReload(editorContainer.getCurrentOptionsToSave());
+    }
+    catch (error) {
+      logger.error('failed to save', error);
+      pageContainer.showErrorToastr(error);
+    }
   }
 
   saveAndOverwriteScopesOfDescendants() {

+ 3 - 3
src/client/js/components/SearchPage.jsx

@@ -7,6 +7,8 @@ import { withTranslation } from 'react-i18next';
 import { createSubscribedElement } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 
+import { toastError } from '../util/apiNotification';
+
 import SearchPageForm from './SearchPage/SearchPageForm';
 import SearchResult from './SearchPage/SearchResult';
 
@@ -83,9 +85,7 @@ class SearchPage extends React.Component {
         });
       })
       .catch((err) => {
-        // TODO error
-        // this.setState({
-        // });
+        toastError(err);
       });
   }
 

+ 0 - 5
src/client/js/legacy/crowi-admin.js

@@ -1,5 +0,0 @@
-require('./thirdparty-js/jQuery.style.switcher');
-
-$(() => {
-  $('#styleOptions').styleSwitcher();
-});

+ 0 - 233
src/client/js/legacy/thirdparty-js/jQuery.style.switcher.js

@@ -1,233 +0,0 @@
-/**
-@author Cameron Manavian
-jQuery Style Switcher
-
-The MIT License (MIT)
-
-Copyright (c) 2014 Cameron Manavian
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-**/
-(function ($) {
-	var jStyleSwitcher,
-		_defaultOptions = {
-			hasPreview: true,
-			defaultThemeId: 'jssDefault',
-			fullPath: 'css/',
-			cookie: {
-				expires: 30,
-				isManagingLoad: true
-			}
-		},
-		// private
-		_cookieKey = 'jss_selected',
-		_docCookies = {};
-
-	/*\
-	|*|
-	|*|  :: cookies.js ::
-	|*|
-	|*|  A complete cookies reader/writer framework with full unicode support.
-	|*|
-	|*|  revision #1
-	|*|
-	|*|  https://developer.mozilla.org/en-US/docs/Web/API/document.cookie
-	|*|
-	|*|  This framework is released under the GNU Public License, version 3 or later.
-	|*|  http://www.gnu.org/licenses/gpl-3.0-standalone.html
-	|*|
-	|*|  Syntaxes:
-	|*|
-	|*|  * docCookies.setItem(name, value[, end[, path[, domain[, secure]]]])
-	|*|  * docCookies.getItem(name)
-	|*|  * docCookies.removeItem(name[, path[, domain]])
-	|*|  * docCookies.hasItem(name)
-	|*|  * docCookies.keys()
-	|*|
-	\*/
-	_docCookies = {
-		getItem: function (sKey) {
-			if (!sKey) {
-				return null;
-			}
-			return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
-		},
-		setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
-			if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) {
-				return false;
-			}
-			var sExpires = "";
-			if (vEnd) {
-				switch (vEnd.constructor) {
-					case Number:
-						sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
-						break;
-					case String:
-						sExpires = "; expires=" + vEnd;
-						break;
-					case Date:
-						sExpires = "; expires=" + vEnd.toUTCString();
-						break;
-				}
-			}
-			document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
-			return true;
-		},
-		removeItem: function (sKey, sPath, sDomain) {
-			if (!this.hasItem(sKey)) {
-				return false;
-			}
-			document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "");
-			return true;
-		},
-		hasItem: function (sKey) {
-			if (!sKey) {
-				return false;
-			}
-			return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
-		},
-		keys: function () {
-			var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
-			for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) {
-				aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
-			}
-			return aKeys;
-		}
-	};
-
-	jStyleSwitcher = function ($root, config) {
-		return this.init($root, config);
-	};
-
-	jStyleSwitcher.prototype = {
-
-		/**
-		 * {Object} DOM reference to style option list
-		 */
-		$root: null,
-
-		/**
-		 * {Object} configs for the style switcher
-		 */
-		config: {},
-
-		/**
-		 * {Object} jQuery reference to <link> tag for swapping CSS
-		 */
-		$themeCss: null,
-
-		/**
-		 * {String} default theme page was loaded with
-		 */
-		defaultTheme: null,
-
-		init: function ($root, config) {
-			this.$root = $root;
-			this.config = config ? config : {};
-			this.setDefaultTheme();
-			if(this.defaultTheme) {
-				// try cookies
-				if (this.config.cookie) {
-					this.checkCookie();
-				}
-				// try hover
-				if (this.config.hasPreview) {
-					this.addHoverEvents();
-				}
-				// finally, add click events
-				this.addClickEvents();
-			} else {
-				this.$root.addClass('jssError error level0');
-			}
-		},
-
-		setDefaultTheme: function () {
-			this.$themeCss = $('link[id=' + this.config.defaultThemeId + ']');
-			if(this.$themeCss.length) {
-				this.defaultTheme = this.$themeCss.attr('href');
-			}
-		},
-
-		resetStyle: function() {
-			this.updateStyle(this.defaultTheme);
-		},
-
-		updateStyle: function(newStyle) {
-			this.$themeCss.attr('href', newStyle);
-		},
-
-		getFullAssetPath: function(asset) {
-			return this.config.fullPath + asset;
-		},
-
-		checkCookie: function () {
-			var styleCookie;
-			// if using cookies and using JavaScript to load css
-			if (this.config.cookie && this.config.cookie.isManagingLoad) {
-				// check if css is set in cookie
-				styleCookie = _docCookies.getItem(_cookieKey);
-				if (styleCookie) {
-					newStyle = this.getFullAssetPath(styleCookie);
-					// update link tag
-					this.updateStyle(newStyle);
-					// update default ref
-					this.defaultTheme = newStyle;
-				}
-			}
-		},
-
-		addHoverEvents: function () {
-			var self = this;
-			this.$root.find('a').hover(
-				function () {
-					var asset = $(this).data('theme'),
-						newStyle = self.getFullAssetPath(asset);
-					// update link tag
-					self.updateStyle(newStyle);
-				},
-				function () {
-					// reset link tag
-					self.resetStyle();
-				}
-			);
-		},
-
-		addClickEvents: function () {
-			var self = this;
-			this.$root.find('a').click(
-				function () {
-					var asset = $(this).data('theme'),
-            newStyle = self.getFullAssetPath(asset);
-					// update link tag
-					self.updateStyle(newStyle);
-					// update default ref
-					self.defaultTheme = newStyle;
-					// try to store cookie
-					if (self.config.cookie) {
-						_docCookies.setItem(_cookieKey, asset, self.config.cookie.expires, '/');
-					}
-				}
-			);
-		}
-	};
-
-	$.fn.styleSwitcher = function (options) {
-		return new jStyleSwitcher(this, $.extend(true, _defaultOptions, options));
-	};
-})(jQuery);

+ 7 - 7
src/client/js/services/AdminAppContainer.js

@@ -35,7 +35,7 @@ export default class AdminAppContainer extends Container {
       customEndpoint: '',
       bucket: '',
       accessKeyId: '',
-      secretKey: '',
+      secretAccessKey: '',
       isEnabledPlugins: true,
     };
 
@@ -53,7 +53,7 @@ export default class AdminAppContainer extends Container {
     this.changeCustomEndpoint = this.changeCustomEndpoint.bind(this);
     this.changeBucket = this.changeBucket.bind(this);
     this.changeAccessKeyId = this.changeAccessKeyId.bind(this);
-    this.changeSecretKey = this.changeSecretKey.bind(this);
+    this.changeSecretAccessKey = this.changeSecretAccessKey.bind(this);
     this.changeIsEnabledPlugins = this.changeIsEnabledPlugins.bind(this);
     this.updateAppSettingHandler = this.updateAppSettingHandler.bind(this);
     this.updateSiteUrlSettingHandler = this.updateSiteUrlSettingHandler.bind(this);
@@ -94,7 +94,7 @@ export default class AdminAppContainer extends Container {
         customEndpoint: appSettingsParams.customEndpoint,
         bucket: appSettingsParams.bucket,
         accessKeyId: appSettingsParams.accessKeyId,
-        secretKey: appSettingsParams.secretKey,
+        secretAccessKey: appSettingsParams.secretAccessKey,
         isEnabledPlugins: appSettingsParams.isEnabledPlugins,
       });
 
@@ -205,10 +205,10 @@ export default class AdminAppContainer extends Container {
   }
 
   /**
-   * Change secret key
+   * Change secret access key
    */
-  changeSecretKey(secretKey) {
-    this.setState({ secretKey });
+  changeSecretAccessKey(secretAccessKey) {
+    this.setState({ secretAccessKey });
   }
 
   /**
@@ -276,7 +276,7 @@ export default class AdminAppContainer extends Container {
       customEndpoint: this.state.customEndpoint,
       bucket: this.state.bucket,
       accessKeyId: this.state.accessKeyId,
-      secretKey: this.state.secretKey,
+      secretAccessKey: this.state.secretAccessKey,
     });
     const { awsSettingParams } = response.data;
     return awsSettingParams;

+ 36 - 1
src/client/js/services/AdminCustomizeContainer.js

@@ -109,6 +109,11 @@ export default class AdminCustomizeContainer extends Container {
       return;
     }
     this.setState({ currentTheme: themeName });
+
+    // preview if production
+    if (process.env.NODE_ENV !== 'development') {
+      this.previewTheme(themeName);
+    }
   }
 
   /**
@@ -161,6 +166,8 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ currentHighlightJsStyleName: styleName });
     // recommended settings are applied
     this.setState({ isHighlightJsStyleBorderEnabled: isBorderEnable });
+
+    this.previewHighlightJsStyle(styleId);
   }
 
   /**
@@ -198,6 +205,34 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ currentCustomizeScript: inpuValue });
   }
 
+  /**
+   * Preview theme
+   * @param {string} themeName
+   */
+  async previewTheme(themeName) {
+    try {
+      // get theme asset path
+      const response = await this.appContainer.apiv3.get('/customize-setting/layout-theme/asset-path', { themeName });
+      const { assetPath } = response.data;
+
+      const themeLink = document.getElementById('grw-theme-link');
+      themeLink.setAttribute('href', assetPath);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  /**
+   * Preview hljs style
+   * @param {string} styleId
+   */
+  previewHighlightJsStyle(styleId) {
+    const styleLInk = document.querySelectorAll('#grw-hljs-container-for-demo link')[0];
+    // replace css url
+    // see https://regex101.com/r/gBNZYu/4
+    styleLInk.href = styleLInk.href.replace(/[^/]+\.css$/, `${styleId}.css`);
+  }
 
   /**
    * Update layout
@@ -205,7 +240,7 @@ export default class AdminCustomizeContainer extends Container {
    * @return {Array} Appearance
    */
   async updateCustomizeLayoutAndTheme() {
-    const response = await this.appContainer.apiv3.put('/customize-setting/layoutTheme', {
+    const response = await this.appContainer.apiv3.put('/customize-setting/layout-theme', {
       layoutType: this.state.currentLayout,
       themeType: this.state.currentTheme,
     });

+ 2 - 17
src/server/crowi/index.js

@@ -38,7 +38,6 @@ function Crowi(rootdir) {
 
   this.config = {};
   this.configManager = null;
-  this.searcher = null;
   this.mailer = {};
   this.passportService = null;
   this.globalNotificationService = null;
@@ -51,6 +50,7 @@ function Crowi(rootdir) {
   this.growiBridgeService = null;
   this.exportService = null;
   this.importService = null;
+  this.searchService = null;
   this.cdnResourcesService = new CdnResourcesService();
   this.interceptorManager = new InterceptorManager();
   this.xss = new Xss();
@@ -279,10 +279,6 @@ Crowi.prototype.scanRuntimeVersions = async function() {
   });
 };
 
-Crowi.prototype.getSearcher = function() {
-  return this.searcher;
-};
-
 Crowi.prototype.getMailer = function() {
   return this.mailer;
 };
@@ -331,18 +327,7 @@ Crowi.prototype.setupPassport = async function() {
 
 Crowi.prototype.setupSearcher = async function() {
   const SearchService = require('@server/service/search');
-  const searchService = new SearchService(this);
-
-  if (searchService.isConfigured) {
-    try {
-      this.searchService = searchService;
-      this.searcher = searchService; // TODO: use `searchService` instead of `searcher`
-    }
-    catch (e) {
-      logger.error('Error on setup searcher', e);
-      this.searcher = null;
-    }
-  }
+  this.searchService = new SearchService(this);
 };
 
 Crowi.prototype.setupMailer = async function() {

+ 0 - 5
src/server/routes/admin.js

@@ -224,11 +224,6 @@ module.exports = function(crowi, app) {
 
   actions.search = {};
   actions.search.index = function(req, res) {
-    const search = crowi.getSearcher();
-    if (!search) {
-      return res.redirect('/admin');
-    }
-
     return res.render('admin/search', {});
   };
 

+ 5 - 5
src/server/routes/apiv3/app-settings.js

@@ -83,7 +83,7 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          accessKeyId:
  *            type: string
  *            description: accesskey id for authentification of AWS
- *          secretKey:
+ *          secretAccessKey:
  *            type: string
  *            description: secret key for authentification of AWS
  *      PluginSettingParams:
@@ -126,7 +126,7 @@ module.exports = (crowi) => {
       body('customEndpoint').trim().matches(/^(https?:\/\/[^/]+|)$/).withMessage('カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。'),
       body('bucket').trim(),
       body('accessKeyId').trim().matches(/^[\da-zA-Z]+$/),
-      body('secretKey').trim(),
+      body('secretAccessKey').trim(),
     ],
     pluginSetting: [
       body('isEnabledPlugins').isBoolean(),
@@ -170,7 +170,7 @@ module.exports = (crowi) => {
       customEndpoint: crowi.configManager.getConfig('crowi', 'aws:customEndpoint'),
       bucket: crowi.configManager.getConfig('crowi', 'aws:bucket'),
       accessKeyId: crowi.configManager.getConfig('crowi', 'aws:accessKeyId'),
-      secretKey: crowi.configManager.getConfig('crowi', 'aws:secretKey'),
+      secretAccessKey: crowi.configManager.getConfig('crowi', 'aws:secretAccessKey'),
       isEnabledPlugins: crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins'),
     };
     return res.apiv3({ appSettingsParams });
@@ -410,7 +410,7 @@ module.exports = (crowi) => {
       'aws:customEndpoint': req.body.customEndpoint,
       'aws:bucket': req.body.bucket,
       'aws:accessKeyId': req.body.accessKeyId,
-      'aws:secretKey': req.body.secretKey,
+      'aws:secretAccessKey': req.body.secretAccessKey,
     };
 
     try {
@@ -420,7 +420,7 @@ module.exports = (crowi) => {
         customEndpoint: crowi.configManager.getConfig('crowi', 'aws:customEndpoint'),
         bucket: crowi.configManager.getConfig('crowi', 'aws:bucket'),
         accessKeyId: crowi.configManager.getConfig('crowi', 'aws:accessKeyId'),
-        secretKey: crowi.configManager.getConfig('crowi', 'aws:secretKey'),
+        secretAccessKey: crowi.configManager.getConfig('crowi', 'aws:secretAccessKey'),
       };
       return res.apiv3({ awsSettingParams });
     }

+ 47 - 4
src/server/routes/apiv3/customize-setting.js

@@ -7,7 +7,7 @@ const express = require('express');
 
 const router = express.Router();
 
-const { body } = require('express-validator/check');
+const { body, query } = require('express-validator');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
@@ -92,6 +92,11 @@ module.exports = (crowi) => {
   const { ApiV3FormValidator } = crowi.middlewares;
 
   const validator = {
+    themeAssetPath: [
+      query('themeName').isString().isIn([
+        'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'default-dark', 'future', 'blue-night', 'halloween', 'spring',
+      ]),
+    ],
     layoutTheme: [
       body('layoutType').isString().isIn(['growi', 'kibela', 'crowi']),
       body('themeType').isString().isIn([
@@ -173,11 +178,49 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /customize-setting/layoutTheme:
+   *    /customize-setting/layout-theme/asset-path:
+   *      put:
+   *        tags: [CustomizeSetting]
+   *        operationId: getLayoutThemeAssetPath
+   *        summary: /customize-setting/layout-theme/asset-path
+   *        description: Get layout theme asset path
+   *        parameters:
+   *          - name: themeName
+   *            in: query
+   *            required: true
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Succeeded to update layout and theme
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    assetPath:
+   *                      type: string
+   */
+  router.get('/layout-theme/asset-path', loginRequiredStrictly, adminRequired, validator.themeAssetPath, ApiV3FormValidator, async(req, res) => {
+    const themeName = req.query.themeName;
+
+    const webpackAssetKey = `styles/theme-${themeName}.css`;
+    const assetPath = res.locals.webpack_asset(webpackAssetKey);
+
+    if (assetPath == null) {
+      return res.apiv3Err(new ErrorV3(`The asset for '${webpackAssetKey}' is undefined.`, 'invalid-asset'));
+    }
+
+    return res.apiv3({ assetPath });
+  });
+
+  /**
+   * @swagger
+   *
+   *    /customize-setting/layout-theme:
    *      put:
    *        tags: [CustomizeSetting]
    *        operationId: updateLayoutThemeCustomizeSetting
-   *        summary: /customize-setting/layoutTheme
+   *        summary: /customize-setting/layout-theme
    *        description: Update layout and theme
    *        requestBody:
    *          required: true
@@ -193,7 +236,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/CustomizeLayoutTheme'
    */
-  router.put('/layoutTheme', loginRequiredStrictly, adminRequired, csrf, validator.layoutTheme, ApiV3FormValidator, async(req, res) => {
+  router.put('/layout-theme', loginRequiredStrictly, adminRequired, csrf, validator.layoutTheme, ApiV3FormValidator, async(req, res) => {
     const requestParams = {
       'customize:layout': req.body.layoutType,
       'customize:theme': req.body.themeType,

+ 8 - 6
src/server/routes/apiv3/healthcheck.js

@@ -116,12 +116,14 @@ module.exports = (crowi) => {
     }
 
     // connect to search service
-    try {
-      const search = crowi.getSearcher();
-      info.searchInfo = await search.getInfo();
-    }
-    catch (err) {
-      errors.push(new ErrorV3(`The Search Service is not connectable - ${err.message}`, 'healthcheck-search-unhealthy', err.stack));
+    const { searchService } = crowi;
+    if (searchService.isConfigured) {
+      try {
+        info.searchInfo = await searchService.getInfo();
+      }
+      catch (err) {
+        errors.push(new ErrorV3(`The Search Service is not connectable - ${err.message}`, 'healthcheck-search-unhealthy', err.stack));
+      }
     }
 
     if (errors.length > 0) {

+ 27 - 9
src/server/routes/apiv3/search.js

@@ -9,6 +9,7 @@ const router = express.Router();
 
 const helmet = require('helmet');
 
+const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
  * @swagger
@@ -42,9 +43,14 @@ module.exports = (crowi) => {
    *                    type: object
    */
   router.get('/indices', helmet.noCache(), accessTokenParser, loginRequired, adminRequired, async(req, res) => {
+    const { searchService } = crowi;
+
+    if (!searchService.isConfigured) {
+      return res.apiv3Err(new ErrorV3('SearchService is not configured', 'search-service-unconfigured'), 503);
+    }
+
     try {
-      const search = crowi.getSearcher();
-      const info = await search.getInfoForAdmin();
+      const info = await searchService.getInfoForAdmin();
       return res.status(200).send({ info });
     }
     catch (err) {
@@ -65,9 +71,14 @@ module.exports = (crowi) => {
    *          description: Successfully connected
    */
   router.post('/connection', accessTokenParser, loginRequired, adminRequired, async(req, res) => {
+    const { searchService } = crowi;
+
+    if (!searchService.isConfigured) {
+      return res.apiv3Err(new ErrorV3('SearchService is not configured', 'search-service-unconfigured'));
+    }
+
     try {
-      const search = crowi.getSearcher();
-      await search.initClient();
+      await searchService.initClient();
       return res.status(200).send();
     }
     catch (err) {
@@ -106,24 +117,31 @@ module.exports = (crowi) => {
   router.put('/indices', accessTokenParser, loginRequired, adminRequired, csrf, validatorForPutIndices, ApiV3FormValidator, async(req, res) => {
     const operation = req.body.operation;
 
-    try {
-      const search = crowi.getSearcher();
+    const { searchService } = crowi;
+
+    if (!searchService.isConfigured) {
+      return res.apiv3Err(new ErrorV3('SearchService is not configured', 'search-service-unconfigured'));
+    }
+    if (!searchService.isReachable) {
+      return res.apiv3Err(new ErrorV3('SearchService is not reachable', 'search-service-unreachable'));
+    }
 
+    try {
       switch (operation) {
         case 'normalize':
           // wait the processing is terminated
-          await search.normalizeIndices();
+          await searchService.normalizeIndices();
           return res.status(200).send({ message: 'Operation is successfully processed.' });
         case 'rebuild':
           // NOT wait the processing is terminated
-          search.rebuildIndex();
+          searchService.rebuildIndex();
           return res.status(200).send({ message: 'Operation is successfully requested.' });
         default:
           throw new Error(`Unimplemented operation: ${operation}`);
       }
     }
     catch (err) {
-      return res.apiv3Err(err);
+      return res.apiv3Err(err, 503);
     }
   });
 

+ 3 - 3
src/server/routes/installer.js

@@ -12,12 +12,12 @@ module.exports = function(crowi, app) {
   const actions = {};
 
   async function initSearchIndex() {
-    const search = crowi.getSearcher();
-    if (search == null) {
+    const { searchService } = crowi;
+    if (!searchService.isReachable) {
       return;
     }
 
-    await search.rebuildIndex();
+    await searchService.rebuildIndex();
   }
 
   async function createInitialPages(owner, lang) {

+ 4 - 8
src/server/routes/search.js

@@ -35,10 +35,6 @@ module.exports = function(crowi, app) {
 
   actions.searchPage = function(req, res) {
     const keyword = req.query.q || null;
-    const search = crowi.getSearcher();
-    if (!search) {
-      return res.json(ApiResponse.error('Configuration of ELASTICSEARCH_URI is required.'));
-    }
 
     return res.render('search', {
       q: keyword,
@@ -125,9 +121,9 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('keyword should not empty.'));
     }
 
-    const search = crowi.getSearcher();
-    if (!search) {
-      return res.json(ApiResponse.error('Configuration of ELASTICSEARCH_URI is required.'));
+    const { searchService } = crowi;
+    if (!searchService.isReachable) {
+      return res.json(ApiResponse.error('SearchService is not reachable.'));
     }
 
     let userGroups = [];
@@ -140,7 +136,7 @@ module.exports = function(crowi, app) {
 
     const result = {};
     try {
-      const esResult = await search.searchKeyword(keyword, user, userGroups, searchOpts);
+      const esResult = await searchService.searchKeyword(keyword, user, userGroups, searchOpts);
 
       // create score map for sorting
       // key: id , value: score

+ 23 - 4
src/server/service/search-delegator/elasticsearch.js

@@ -126,17 +126,36 @@ class ElasticsearchDelegator {
     return { esVersion, esNodeInfos };
   }
 
+  /**
+   * Return information for Admin Full Text Search Management page
+   */
   async getInfoForAdmin() {
     const { client, indexName, aliasName } = this;
 
     const tmpIndexName = `${indexName}-tmp`;
 
-    const { indices } = await client.indices.stats({ index: `${indexName}*`, ignore_unavailable: true, metric: ['docs', 'store', 'indexing'] });
+    // check existence
+    const isExistsMainIndex = await client.indices.exists({ index: indexName });
+    const isExistsTmpIndex = await client.indices.exists({ index: tmpIndexName });
+
+    // create indices name list
+    const existingIndices = [];
+    if (isExistsMainIndex) { existingIndices.push(indexName) }
+    if (isExistsTmpIndex) { existingIndices.push(tmpIndexName) }
+
+    // results when there is no indices
+    if (existingIndices.length === 0) {
+      return {
+        indices: [],
+        aliases: [],
+        isNormalized: false,
+      };
+    }
+
+    const { indices } = await client.indices.stats({ index: existingIndices, ignore_unavailable: true, metric: ['docs', 'store', 'indexing'] });
+    const aliases = await client.indices.getAlias({ index: existingIndices });
 
-    const aliases = await client.indices.getAlias({ index: `${indexName}*` });
-    const isExistsMainIndex = aliases[indexName] != null;
     const isMainIndexHasAlias = isExistsMainIndex && aliases[indexName].aliases != null && aliases[indexName].aliases[aliasName] != null;
-    const isExistsTmpIndex = aliases[tmpIndexName] != null;
     const isTmpIndexHasAlias = isExistsTmpIndex && aliases[tmpIndexName].aliases != null && aliases[tmpIndexName].aliases[aliasName] != null;
 
     const isNormalized = isExistsMainIndex && isMainIndexHasAlias && !isExistsTmpIndex && !isTmpIndexHasAlias;

+ 2 - 2
src/server/util/swigFunctions.js

@@ -115,8 +115,8 @@ module.exports = function(crowi, req, locals) {
   };
 
   locals.isSearchServiceConfigured = function() {
-    const searchService = crowi.getSearcher();
-    return searchService != null && searchService.isConfigured;
+    const { searchService } = crowi;
+    return searchService.isConfigured;
   };
 
   locals.isHackmdSetup = function() {

+ 23 - 11
src/server/views/admin/customize.html

@@ -1,9 +1,8 @@
-{% extends '../layout/admin.html' %} {% block html_title %}{{ customizeService.generateCustomTitle(t('Customize')) }} {% endblock %} {% block theme_css_block %}
-{% set themeName = getConfig('crowi', 'customize:theme') %} {% if env === 'development' %}
-<script src="{{ webpack_asset('styles/theme-' + themeName + '.js') }}"></script>
-{% else %}
-<link rel="stylesheet" id="jssDefault" {# append id for theme selector #} href="{{ webpack_asset('styles/theme-' + themeName + '.css') }}" />
-{% endif %} {% endblock %} {% block html_additional_headers %} {% parent %}
+{% extends '../layout/admin.html' %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Customize')) }}{% endblock %}
+
+{% block html_additional_headers %}
+{% parent %}
 <!-- CodeMirror -->
 {{ cdnStyleTag('jquery-ui') }}
 <style>
@@ -11,24 +10,35 @@
     border: 1px solid #eee;
   }
 </style>
-{% endblock %} {% block content_header %}
+{% endblock %}
+
+{% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
     <h1 id="admin-title" class="title">{{ t('Customize') }}</h1>
   </header>
 </div>
-{% endblock %} {% block content_main %}
+{% endblock %}
+
+{% block content_main %}
 <div class="content-main admin-customize">
-  {% set smessage = req.flash('successMessage') %} {% if smessage.length %}
+  {% set smessage = req.flash('successMessage') %}
+  {% if smessage.length %}
   <div class="alert alert-success">
     {{ smessage }}
   </div>
-  {% endif %} {% set emessage = req.flash('errorMessage') %} {% if emessage.length %}
+  {% endif %}
+  {% set emessage = req.flash('errorMessage') %}
+  {% if emessage.length %}
   <div class="alert alert-danger">
     {{ emessage }}
   </div>
   {% endif %}
 
+  <div id="grw-hljs-container-for-demo">
+    {{ cdnHighlightJsStyleTag(getConfig('crowi', 'customize:highlightJsStyle')) }}
+  </div>
+
   <div class="row">
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'customize'} %}
@@ -37,5 +47,7 @@
       <div id="admin-customize"></div>
     </div>
   </div>
-  {% endblock content_main %} {% block content_footer %} {% endblock content_footer %}
 </div>
+{% endblock content_main %}
+
+{% block content_footer %} {% endblock content_footer %}

+ 0 - 1
src/server/views/layout/admin.html

@@ -7,7 +7,6 @@
 {% block html_additional_headers %}
   {% parent %}
   <script src="{{ webpack_asset('js/admin.js') }}" defer></script>
-  <script src="{{ webpack_asset('js/legacy-admin.js') }}" defer></script>
 {% endblock %}
 
 {# disable custom script in admin page #}

+ 1 - 1
src/server/views/widget/headers/styles-theme.html

@@ -1,5 +1,5 @@
 {% if env === 'development' %}
   <script src="{{ webpack_asset('styles/theme-' + themeName + '.js') }}"></script>
 {% else %}
-  <link rel="stylesheet" href="{{ webpack_asset('styles/theme-' + themeName + '.css') }}">
+  <link rel="stylesheet" id="grw-theme-link" href="{{ webpack_asset('styles/theme-' + themeName + '.css') }}">
 {% endif %}