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

Merge remote-tracking branch 'origin/dev/7.2.x'

Yuki Takei 7 месяцев назад
Родитель
Сommit
95a9b440c8

+ 14 - 1
CHANGELOG.md

@@ -1,9 +1,22 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.2.8...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.2.9...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.2.9](https://github.com/weseek/growi/compare/v7.2.8...v7.2.9) - 2025-07-01
+
+### 🚀 Improvement
+
+* imprv(ai): Assistant instructions (#10129) @yuki-takei
+* imprv: OpenTelemetry phase 2 (#10095) @yuki-takei
+* imprv: Adjust margin-top for .main at md and lg breakpoints (#10131) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Sharelink expiration date parsing when the date is cleared by the calendar UI (#10132) @yuki-takei
+* fix: Cannot change file upload destination to "MongoDB (GridFS)" or "local" for dev/7.2.x (#10119) @miya
+
 ## [v7.2.8](https://github.com/weseek/growi/compare/v7.2.7...v7.2.8) - 2025-06-26
 ## [v7.2.8](https://github.com/weseek/growi/compare/v7.2.7...v7.2.8) - 2025-06-26
 
 
 ### 💎 Features
 ### 💎 Features

+ 3 - 1
apps/app/.env.development

@@ -30,7 +30,9 @@ OGP_URI="http://ogp:8088"
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 
 
-# OpenTelemetry Official Configuration
+SERVICE_TYPE=dev
+
+# OpenTelemetry Official Configuration for dev
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 OPENTELEMETRY_ENABLED=false
 OPENTELEMETRY_ENABLED=false
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

+ 1 - 1
apps/app/.env.production

@@ -7,6 +7,6 @@ MIGRATIONS_DIR=dist/migrations/
 
 
 # OpenTelemetry Official Configuration
 # OpenTelemetry Official Configuration
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
-OTEL_TRACES_SAMPLER_ARG=0.1
+OTEL_TRACES_SAMPLER_ARG=0.01
 OTEL_METRIC_EXPORT_INTERVAL=300000
 OTEL_METRIC_EXPORT_INTERVAL=300000
 OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.growi.org"
 OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.growi.org"

+ 27 - 18
apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.jsx → apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.tsx

@@ -1,10 +1,9 @@
 import React, {
 import React, {
-  useState, useMemo, useCallback,
+  useState, useMemo, useCallback, type ReactNode, type CSSProperties,
 } from 'react';
 } from 'react';
 
 
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import {
 import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
@@ -15,9 +14,26 @@ import styles from './CopyDropdown.module.scss';
 
 
 const { encodeSpaces } = pagePathUtils;
 const { encodeSpaces } = pagePathUtils;
 
 
+interface DropdownItemContentsProps {
+  title: string;
+  contents: ReactNode;
+  className?: string;
+  style?: CSSProperties;
+}
+
+interface CopyDropdownProps {
+  children: ReactNode;
+  dropdownToggleId: string;
+  pagePath: string;
+  pageId?: string;
+  dropdownToggleClassName?: string;
+  dropdownMenuContainer?: string | HTMLElement | React.RefObject<HTMLElement>;
+  isShareLinkMode?: boolean;
+}
+
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
-const DropdownItemContents = ({
-  title, contents, className, style,
+const DropdownItemContents: React.FC<DropdownItemContentsProps> = ({
+  title, contents, className = '', style,
 }) => (
 }) => (
   <>
   <>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
@@ -27,7 +43,7 @@ const DropdownItemContents = ({
 /* eslint-enable react/prop-types */
 /* eslint-enable react/prop-types */
 
 
 
 
-export const CopyDropdown = (props) => {
+export const CopyDropdown: React.FC<CopyDropdownProps> = (props) => {
   const [dropdownOpen, setDropdownOpen] = useState(false);
   const [dropdownOpen, setDropdownOpen] = useState(false);
   const [tooltipOpen, setTooltipOpen] = useState(false);
   const [tooltipOpen, setTooltipOpen] = useState(false);
   const [isParamsAppended, setParamsAppended] = useState(!props.isShareLinkMode);
   const [isParamsAppended, setParamsAppended] = useState(!props.isShareLinkMode);
@@ -105,7 +121,10 @@ export const CopyDropdown = (props) => {
    */
    */
   const { t } = useTranslation('commons');
   const { t } = useTranslation('commons');
   const {
   const {
-    dropdownToggleId, pageId, dropdownToggleClassName, children, isShareLinkMode,
+    dropdownToggleId, pageId,
+    dropdownToggleClassName,
+    dropdownMenuContainer,
+    children, isShareLinkMode,
   } = props;
   } = props;
 
 
   const customSwitchForParamsId = `customSwitchForParams_${dropdownToggleId}`;
   const customSwitchForParamsId = `customSwitchForParams_${dropdownToggleId}`;
@@ -128,7 +147,7 @@ export const CopyDropdown = (props) => {
         <DropdownMenu
         <DropdownMenu
           className={`${styles['copy-clipboard-dropdown-menu']}`}
           className={`${styles['copy-clipboard-dropdown-menu']}`}
           strategy="fixed"
           strategy="fixed"
-          container="body"
+          container={dropdownMenuContainer}
         >
         >
           <div className="d-flex align-items-center justify-content-between">
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">
             <DropdownItem header className="px-3">
@@ -209,7 +228,7 @@ export const CopyDropdown = (props) => {
           { pageId && (
           { pageId && (
             <CopyToClipboard text={markdownLink} onCopy={showToolTip}>
             <CopyToClipboard text={markdownLink} onCopy={showToolTip}>
               <DropdownItem className="px-3 text-wrap">
               <DropdownItem className="px-3 text-wrap">
-                <DropdownItemContents title={t('copy_to_clipboard.Markdown link')} contents={markdownLink} isContentsWrap />
+                <DropdownItemContents title={t('copy_to_clipboard.Markdown link')} contents={markdownLink} />
               </DropdownItem>
               </DropdownItem>
             </CopyToClipboard>
             </CopyToClipboard>
           )}
           )}
@@ -223,13 +242,3 @@ export const CopyDropdown = (props) => {
     </>
     </>
   );
   );
 };
 };
-
-CopyDropdown.propTypes = {
-  children: PropTypes.node.isRequired,
-  dropdownToggleId: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-
-  pageId: PropTypes.string,
-  dropdownToggleClassName: PropTypes.string,
-  isShareLinkMode: PropTypes.bool,
-};

+ 1 - 0
apps/app/src/client/components/PageHeader/PageTitleHeader.tsx

@@ -166,6 +166,7 @@ export const PageTitleHeader = (props: Props): JSX.Element => {
           pagePath={currentPage.path}
           pagePath={currentPage.path}
           dropdownToggleId={`copydropdown-in-pagetitleheader-${currentPage._id}`}
           dropdownToggleId={`copydropdown-in-pagetitleheader-${currentPage._id}`}
           dropdownToggleClassName="p-1"
           dropdownToggleClassName="p-1"
+          dropdownMenuContainer="body"
         >
         >
           <span className="material-symbols-outlined fs-6">content_paste</span>
           <span className="material-symbols-outlined fs-6">content_paste</span>
         </CopyDropdown>
         </CopyDropdown>

+ 7 - 1
apps/app/src/components/Common/PagePathNav/PagePathNavLayout.tsx

@@ -64,7 +64,13 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
               <span className="badge text-bg-secondary">WIP</span>
               <span className="badge text-bg-secondary">WIP</span>
             )}
             )}
             <span className="grw-page-path-nav-copydropdown">
             <span className="grw-page-path-nav-copydropdown">
-              <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
+              <CopyDropdown
+                pageId={pageId}
+                pagePath={pagePath}
+                dropdownToggleId={copyDropdownId}
+                dropdownToggleClassName="p-2"
+                dropdownMenuContainer="body"
+              >
                 <span className="material-symbols-outlined">content_paste</span>
                 <span className="material-symbols-outlined">content_paste</span>
               </CopyDropdown>
               </CopyDropdown>
             </span>
             </span>

+ 6 - 1
apps/app/src/features/mermaid/components/MermaidViewer.tsx

@@ -6,6 +6,11 @@ import { v7 as uuidV7 } from 'uuid';
 import { useNextThemes } from '~/stores-universal/use-next-themes';
 import { useNextThemes } from '~/stores-universal/use-next-themes';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+const logger = loggerFactory('growi:features:mermaid:MermaidViewer');
+import { v7 as uuidV7 } from 'uuid';
+
+import loggerFactory from '~/utils/logger';
+
 const logger = loggerFactory('growi:features:mermaid:MermaidViewer');
 const logger = loggerFactory('growi:features:mermaid:MermaidViewer');
 
 
 type MermaidViewerProps = {
 type MermaidViewerProps = {
@@ -20,7 +25,7 @@ export const MermaidViewer = React.memo((props: MermaidViewerProps): JSX.Element
   const ref = useRef<HTMLDivElement>(null);
   const ref = useRef<HTMLDivElement>(null);
 
 
   useEffect(() => {
   useEffect(() => {
-    (async() => {
+    (async () => {
       if (ref.current != null && value != null) {
       if (ref.current != null && value != null) {
         mermaid.initialize({
         mermaid.initialize({
           theme: isDarkMode ? 'dark' : undefined,
           theme: isDarkMode ? 'dark' : undefined,

+ 60 - 6
apps/app/src/features/opentelemetry/server/node-sdk.spec.ts

@@ -3,7 +3,7 @@ import { NodeSDK } from '@opentelemetry/sdk-node';
 
 
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 
 
-import { setupAdditionalResourceAttributes, initInstrumentation } from './node-sdk';
+import { setupAdditionalResourceAttributes, initInstrumentation, startOpenTelemetry } from './node-sdk';
 import { getResource } from './node-sdk-resource';
 import { getResource } from './node-sdk-resource';
 
 
 // Only mock configManager as it's external to what we're testing
 // Only mock configManager as it's external to what we're testing
@@ -67,15 +67,10 @@ describe('node-sdk', () => {
 
 
   describe('initInstrumentation', () => {
   describe('initInstrumentation', () => {
     it('should call setupCustomMetrics when instrumentation is enabled', async() => {
     it('should call setupCustomMetrics when instrumentation is enabled', async() => {
-      const { setupCustomMetrics } = await import('./custom-metrics');
-
       // Mock instrumentation as enabled
       // Mock instrumentation as enabled
       mockInstrumentationEnabled();
       mockInstrumentationEnabled();
 
 
       await initInstrumentation();
       await initInstrumentation();
-
-      // Verify setupCustomMetrics was called
-      expect(setupCustomMetrics).toHaveBeenCalledOnce();
     });
     });
 
 
     it('should not call setupCustomMetrics when instrumentation is disabled', async() => {
     it('should not call setupCustomMetrics when instrumentation is disabled', async() => {
@@ -203,4 +198,63 @@ describe('node-sdk', () => {
       await expect(setupAdditionalResourceAttributes()).resolves.toBeUndefined();
       await expect(setupAdditionalResourceAttributes()).resolves.toBeUndefined();
     });
     });
   });
   });
+
+  describe('startOpenTelemetry', () => {
+    it('should start SDK and call setupCustomMetrics when instrumentation is enabled and SDK instance exists', async() => {
+      const { setupCustomMetrics } = await import('./custom-metrics');
+
+      // Mock instrumentation as enabled
+      mockInstrumentationEnabled();
+
+      // Initialize SDK first
+      await initInstrumentation();
+
+      // Get SDK instance and mock its start method
+      const { __testing__ } = await import('./node-sdk');
+      const sdkInstance = __testing__.getSdkInstance();
+      expect(sdkInstance).toBeDefined();
+
+      if (sdkInstance != null) {
+        const startSpy = vi.spyOn(sdkInstance, 'start');
+
+        // Call startOpenTelemetry
+        startOpenTelemetry();
+
+        // Verify that start method was called
+        expect(startSpy).toHaveBeenCalledOnce();
+
+        // Verify that setupCustomMetrics was called
+        expect(setupCustomMetrics).toHaveBeenCalledOnce();
+      }
+    });
+
+    it('should not start SDK when instrumentation is disabled', async() => {
+      const { setupCustomMetrics } = await import('./custom-metrics');
+
+      // Mock instrumentation as disabled
+      mockInstrumentationDisabled();
+
+      // Initialize SDK (should not create instance)
+      await initInstrumentation();
+
+      // Call startOpenTelemetry
+      startOpenTelemetry();
+
+      // Verify that setupCustomMetrics was not called
+      expect(setupCustomMetrics).not.toHaveBeenCalled();
+    });
+
+    it('should not start SDK when SDK instance does not exist', async() => {
+      const { setupCustomMetrics } = await import('./custom-metrics');
+
+      // Mock instrumentation as enabled but don't initialize SDK
+      mockInstrumentationEnabled();
+
+      // Call startOpenTelemetry without initializing SDK
+      startOpenTelemetry();
+
+      // Verify that setupCustomMetrics was not called
+      expect(setupCustomMetrics).not.toHaveBeenCalled();
+    });
+  });
 });
 });

+ 3 - 2
apps/app/src/features/opentelemetry/server/node-sdk.ts

@@ -72,8 +72,6 @@ For more information, see https://docs.growi.org/en/admin-guide/admin-cookbook/t
 
 
     const sdkConfig = generateNodeSDKConfiguration({ enableAnonymization });
     const sdkConfig = generateNodeSDKConfiguration({ enableAnonymization });
 
 
-    setupCustomMetrics();
-
     sdkInstance = new NodeSDK(sdkConfig);
     sdkInstance = new NodeSDK(sdkConfig);
   }
   }
 };
 };
@@ -106,6 +104,9 @@ export const startOpenTelemetry = (): void => {
       throw new Error('OpenTelemetry instrumentation is not initialized');
       throw new Error('OpenTelemetry instrumentation is not initialized');
     }
     }
     sdkInstance.start();
     sdkInstance.start();
+
+    // setup custom metrics after SDK start
+    setupCustomMetrics();
   }
   }
 };
 };
 
 

+ 1 - 1
apps/app/src/server/service/config-manager/config-definition.ts

@@ -503,7 +503,7 @@ export const CONFIG_DEFINITIONS = {
   }),
   }),
   'app:deploymentType': defineConfig<GrowiDeploymentType>({
   'app:deploymentType': defineConfig<GrowiDeploymentType>({
     envVarName: 'DEPLOYMENT_TYPE',
     envVarName: 'DEPLOYMENT_TYPE',
-    defaultValue: GrowiDeploymentType.others,
+    defaultValue: GrowiDeploymentType.node,
   }),
   }),
   'app:ssrMaxRevisionBodyLength': defineConfig<number>({
   'app:ssrMaxRevisionBodyLength': defineConfig<number>({
     envVarName: 'SSR_MAX_REVISION_BODY_LENGTH',
     envVarName: 'SSR_MAX_REVISION_BODY_LENGTH',

+ 1 - 0
packages/core/src/consts/system.ts

@@ -2,6 +2,7 @@ export const GrowiServiceType = {
   cloud: 'cloud',
   cloud: 'cloud',
   privateCloud: 'private-cloud',
   privateCloud: 'private-cloud',
   onPremise: 'on-premise',
   onPremise: 'on-premise',
+  dev: 'dev',
   others: 'others',
   others: 'others',
 } as const;
 } as const;