فهرست منبع

Merge pull request #1592 from weseek/fix/error-without-searchservice

Fix/error without searchservice
Yuki Takei 6 سال پیش
والد
کامیت
38dd4fb4e4

+ 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": "インデックスの状態",

+ 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,

+ 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);
       });
   }
 

+ 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', {});
   };
 

+ 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

+ 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() {

+ 26 - 8
src/server/views/admin/customize.html

@@ -1,9 +1,18 @@
-{% 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' %}
+{% 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 %}
+{% endif %}
+{% endblock %}
+
+{% block html_additional_headers %}
+{% parent %}
 <!-- CodeMirror -->
 {{ cdnStyleTag('jquery-ui') }}
 <style>
@@ -11,19 +20,26 @@
     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>
@@ -37,5 +53,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 %}