Przeglądaj źródła

Merge branch 'imprv/use-suspense-in-admin-page-for-merge' into imprv/fix-err-handling-about-suspense

# Conflicts:
#	src/client/js/components/Admin/App/AppSettingsPage.jsx
yusuketk 5 lat temu
rodzic
commit
35b1fdb200

+ 47 - 0
.github/workflows/list-unhealthy-branches.yml

@@ -0,0 +1,47 @@
+name: List Unhealthy Branches
+
+on:
+  schedule:
+    - cron: '0 16 * * wed'
+
+jobs:
+  list:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        fetch-depth: 0
+
+    - uses: actions/setup-node@v2-beta
+      with:
+        node-version: '14'
+
+    - name: List branches
+      id: list-branches
+      run: |
+        export SLACK_ATTACHMENTS_ILLEGAL=`node bin/github-actions/list-branches --illegal`
+        export SLACK_ATTACHMENTS_INACTIVE=`node bin/github-actions/list-branches --inactive`
+        echo ::set-output name=SLACK_ATTACHMENTS_ILLEGAL::$SLACK_ATTACHMENTS_ILLEGAL
+        echo ::set-output name=SLACK_ATTACHMENTS_INACTIVE::$SLACK_ATTACHMENTS_INACTIVE
+        echo ::set-output name=SLACK_ATTACHMENTS_LENGTH_ILLEGAL::$(echo $SLACK_ATTACHMENTS_ILLEGAL | jq '. | length')
+        echo ::set-output name=SLACK_ATTACHMENTS_LENGTH_INACTIVE::$(echo $SLACK_ATTACHMENTS_INACTIVE | jq '. | length')
+
+    - name: Slack Notification for illegal named branches
+      if: steps.list-branches.outputs.SLACK_ATTACHMENTS_LENGTH_ILLEGAL > 0
+      uses: tokorom/action-slack-incoming-webhook@master
+      env:
+        INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_FOR_DEV }}
+      with:
+        text: There is some *illegal named branches* on GitHub.
+        attachments: ${{ steps.list-branches.outputs.SLACK_ATTACHMENTS_ILLEGAL }}
+
+    - name: Slack Notification for inactive branches
+      if: steps.list-branches.outputs.SLACK_ATTACHMENTS_LENGTH_INACTIVE > 0
+      uses: tokorom/action-slack-incoming-webhook@master
+      env:
+        INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_FOR_DEV }}
+      with:
+        text: There is some *inactive branches* on GitHub.
+        attachments: ${{ steps.list-branches.outputs.SLACK_ATTACHMENTS_INACTIVE }}

+ 154 - 0
bin/github-actions/list-branches.js

@@ -0,0 +1,154 @@
+/* eslint-disable no-console */
+
+/*
+ * USAGE:
+ *  node list-branches [OPTION]
+ *
+ * OPTIONS:
+ *  --inactive : Return inactive branches (default)
+ *  --illegal : Return illegal named branches
+ */
+
+const { execSync } = require('child_process');
+const url = require('url');
+
+const EXCLUDE_TERM_DAYS = 14;
+const EXCLUDE_PATTERNS = [
+  /^feat\/custom-sidebar-2$/,
+  // https://regex101.com/r/Lnx7Pz/3
+  /^dev\/[\d.x]*$/,
+];
+const LEGAL_PATTERNS = [
+  /^master$/,
+  // https://regex101.com/r/p9xswM/4
+  /^(dev|feat|imprv|support|fix|rc|release|tmp)\/.+$/,
+];
+const GITHUB_REPOS_URI = 'https://github.com/weseek/growi/';
+
+class BranchSummary {
+
+  constructor(line) {
+    const splitted = line.split('\t'); // split with '%09'
+
+    this.authorDate = new Date(splitted[0].trim());
+    this.authorName = splitted[1].trim();
+    this.branchName = splitted[2].trim().replace(/^origin\//, '');
+    this.subject = splitted[3].trim();
+  }
+
+}
+
+function getExcludeTermDate() {
+  const date = new Date();
+  date.setDate(date.getDate() - EXCLUDE_TERM_DAYS);
+  return date;
+}
+
+function getBranchSummaries() {
+  // exec git for-each-ref
+  const out = execSync(`\
+    git for-each-ref refs/remotes \
+      --sort=-committerdate \
+      --format='%(authordate:iso) %09 %(authorname) %09 %(refname:short) %09 %(subject)'
+  `).toString();
+
+  // parse
+  const summaries = out
+    .split('\n')
+    .filter(v => v !== '') // trim empty string
+    .map(line => new BranchSummary(line))
+    .filter((summary) => { // exclude branches that matches to patterns
+      return !EXCLUDE_PATTERNS.some(pattern => pattern.test(summary.branchName));
+    });
+
+  return summaries;
+}
+
+function getGitHubCommitsUrl(branchName) {
+  return url.resolve(GITHUB_REPOS_URI, `commits/${branchName}`);
+}
+
+function getGitHubComparingLink(branchName) {
+  const label = `master <- ${branchName}`;
+  const link = url.resolve(GITHUB_REPOS_URI, `compare/${branchName}`);
+  return `<${link}|${label}>`;
+}
+
+/**
+ * @see https://api.slack.com/messaging/composing/layouts#building-attachments
+ * @see https://github.com/marketplace/actions/slack-incoming-webhook
+ *
+ * @param {string} mode
+ * @param {BranchSummary} summaries
+ */
+function printSlackAttachments(mode, summaries) {
+  const color = (mode === 'illegal') ? 'warning' : '#999999';
+
+  const attachments = summaries.map((summary) => {
+    const {
+      authorName, authorDate, branchName, subject,
+    } = summary;
+
+    return {
+      color,
+      title: branchName,
+      title_link: getGitHubCommitsUrl(branchName),
+      fields: [
+        {
+          title: 'Author Date',
+          value: authorDate,
+          short: true,
+        },
+        {
+          title: 'Author',
+          value: authorName,
+          short: true,
+        },
+        {
+          title: 'Last Commit Subject',
+          value: subject,
+        },
+        {
+          title: 'Comparing Link',
+          value: getGitHubComparingLink(branchName),
+        },
+      ],
+    };
+  });
+
+  console.log(JSON.stringify(attachments));
+}
+
+async function main(mode) {
+  const summaries = getBranchSummaries();
+
+  let filteredSummaries;
+
+  switch (mode) {
+    case 'illegal':
+      filteredSummaries = summaries
+        .filter((summary) => { // exclude branches that matches to patterns
+          return !LEGAL_PATTERNS.some(pattern => pattern.test(summary.branchName));
+        });
+      break;
+    default: {
+      const excludeTermDate = getExcludeTermDate();
+      filteredSummaries = summaries
+        .filter((summary) => {
+          return summary.authorDate < excludeTermDate;
+        });
+      break;
+    }
+  }
+
+  printSlackAttachments(mode, filteredSummaries);
+}
+
+const args = process.argv.slice(2);
+
+let mode = 'inactive';
+if (args.length > 0 && args[0] === '--illegal') {
+  mode = 'illegal';
+}
+
+main(mode);

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

@@ -4,6 +4,7 @@ import loggerFactory from '@alias/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
+import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 
 import AdminAppContainer from '../../../services/AdminAppContainer';
 
@@ -25,7 +26,7 @@ function AppSettingsPageWithContainerWithSuspense(props) {
   );
 }
 
-let retrieveError = null;
+let retrieveErrors = null;
 function AppSettingsPage(props) {
   if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitle) {
     throw (async() => {
@@ -33,18 +34,19 @@ function AppSettingsPage(props) {
         await props.adminAppContainer.retrieveAppSettingsData();
       }
       catch (err) {
-        toastError(err);
-        logger.error(err);
+        const errs = toArrayIfNot(err);
+        toastError(errs);
+        logger.error(errs);
         props.adminAppContainer.setState({
           title: props.adminAppContainer.dummyTitleForError,
         });
-        retrieveError = err;
+        retrieveErrors = errs;
       }
     })();
   }
 
   if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitleForError) {
-    throw new Error(retrieveError[0].message);
+    throw new Error(`${retrieveErrors.length} errors occured`);
   }
 
   return <AppSettingsPageContents />;

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

@@ -21,6 +21,12 @@ class MailSetting extends React.Component {
       isInitializeValueModalOpen: false,
     };
 
+    this.emailInput = React.createRef();
+    this.hostInput = React.createRef();
+    this.portInput = React.createRef();
+    this.userInput = React.createRef();
+    this.passwordInput = React.createRef();
+
     this.openInitializeValueModal = this.openInitializeValueModal.bind(this);
     this.closeInitializeValueModal = this.closeInitializeValueModal.bind(this);
     this.submitHandler = this.submitHandler.bind(this);
@@ -52,8 +58,14 @@ class MailSetting extends React.Component {
     const { t, adminAppContainer } = this.props;
 
     try {
-      await adminAppContainer.initializeMailSettingHandler();
+      const mailSettingParams = await adminAppContainer.initializeMailSettingHandler();
       toastSuccess(t('toaster.initialize_successed', { target: t('admin:app_setting.mail_settings') }));
+      // convert values to '' if value is null for overwriting values of inputs with refs
+      this.emailInput.current.value = mailSettingParams.fromAddress || '';
+      this.hostInput.current.value = mailSettingParams.smtpHost || '';
+      this.portInput.current.value = mailSettingParams.smtpPort || '';
+      this.userInput.current.value = mailSettingParams.smtpUser || '';
+      this.passwordInput.current.value = mailSettingParams.smtpPassword || '';
       this.closeInitializeValueModal();
     }
     catch (err) {
@@ -74,6 +86,7 @@ class MailSetting extends React.Component {
             <input
               className="form-control"
               type="text"
+              ref={this.emailInput}
               placeholder={`${t('eg')} mail@growi.org`}
               defaultValue={adminAppContainer.state.fromAddress || ''}
               onChange={(e) => { adminAppContainer.changeFromAddress(e.target.value) }}
@@ -88,6 +101,7 @@ class MailSetting extends React.Component {
             <input
               className="form-control"
               type="text"
+              ref={this.hostInput}
               defaultValue={adminAppContainer.state.smtpHost || ''}
               onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
             />
@@ -96,6 +110,7 @@ class MailSetting extends React.Component {
             <label>{t('admin:app_setting.port')}</label>
             <input
               className="form-control"
+              ref={this.portInput}
               defaultValue={adminAppContainer.state.smtpPort || ''}
               onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
             />
@@ -108,6 +123,7 @@ class MailSetting extends React.Component {
             <input
               className="form-control"
               type="text"
+              ref={this.userInput}
               defaultValue={adminAppContainer.state.smtpUser || ''}
               onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
             />
@@ -117,6 +133,7 @@ class MailSetting extends React.Component {
             <input
               className="form-control"
               type="password"
+              ref={this.passwordInput}
               defaultValue={adminAppContainer.state.smtpPassword || ''}
               onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
             />

+ 1 - 0
src/client/js/services/AdminAppContainer.js

@@ -266,6 +266,7 @@ export default class AdminAppContainer extends Container {
       mailSettingParams,
     } = response.data;
     this.setState(mailSettingParams);
+    return mailSettingParams;
   }
 
   /**