This is an automated email from the ASF dual-hosted git repository.

wuzhiguo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/bigtop-manager.git


The following commit(s) were added to refs/heads/main by this push:
     new 32e96ddd BIGTOP-4507: Add dynamic charts for service metrics (#278)
32e96ddd is described below

commit 32e96ddd712ac9df98084a6e5d7b077e03c613b8
Author: Fdefined <[email protected]>
AuthorDate: Sat Oct 11 11:18:28 2025 +0800

    BIGTOP-4507: Add dynamic charts for service metrics (#278)
---
 .../services/doris/configuration/doris-be-conf.xml |   2 +-
 bigtop-manager-ui/src/api/metrics/index.ts         |   6 +-
 bigtop-manager-ui/src/api/metrics/types.ts         |  27 ++++
 .../components/service-configurator.vue            |  21 ++--
 .../src/features/metric/category-chart.vue         |  25 ++--
 .../src/features/service-management/configs.vue    |   9 +-
 .../src/features/service-management/index.vue      |   5 +-
 .../src/features/service-management/overview.vue   | 139 +++++++++++----------
 .../src/pages/cluster-manage/cluster/index.vue     |   4 +-
 .../src/pages/cluster-manage/hosts/overview.vue    |   6 +-
 .../src/store/create-service/validation.ts         |  16 +--
 bigtop-manager-ui/src/utils/tools.ts               |  57 ++++++++-
 12 files changed, 199 insertions(+), 118 deletions(-)

diff --git 
a/bigtop-manager-server/src/main/resources/stacks/extra/1.0.0/services/doris/configuration/doris-be-conf.xml
 
b/bigtop-manager-server/src/main/resources/stacks/extra/1.0.0/services/doris/configuration/doris-be-conf.xml
index 7299f346..e9c59b93 100644
--- 
a/bigtop-manager-server/src/main/resources/stacks/extra/1.0.0/services/doris/configuration/doris-be-conf.xml
+++ 
b/bigtop-manager-server/src/main/resources/stacks/extra/1.0.0/services/doris/configuration/doris-be-conf.xml
@@ -100,7 +100,7 @@ JAVA_OPTS_FOR_JDK_17="-Dfile.encoding=UTF-8 -Xmx4096m 
-DlogPath=$LOG_DIR/jni.log
 # 
https://github.com/apache/doris/blob/master/docs/zh-CN/community/developer-guide/debug-tool.md#jemalloc-heap-profile
 # https://jemalloc.net/jemalloc.3.html
 
JEMALLOC_CONF="percpu_arena:percpu,background_thread:true,metadata_thp:auto,muzzy_decay_ms:5000,dirty_decay_ms:5000,oversize_threshold:0,prof:false,lg_prof_interval:-1"
-JEMALLOC_PROF_PRFIX="jemalloc_heap_profile_"
+JEMALLOC_PROF_PREFIX="jemalloc_heap_profile_"
 
 # ports for admin, web, heartbeat service
 be_port = ${be_port}
diff --git a/bigtop-manager-ui/src/api/metrics/index.ts 
b/bigtop-manager-ui/src/api/metrics/index.ts
index e3502554..a788ad0a 100644
--- a/bigtop-manager-ui/src/api/metrics/index.ts
+++ b/bigtop-manager-ui/src/api/metrics/index.ts
@@ -18,7 +18,7 @@
  */
 
 import { get } from '@/api/request-util'
-import type { MetricsData, TimeRangeType } from './types'
+import type { MetricsData, ServiceMetrics, TimeRangeType } from './types'
 
 export const getClusterMetricsInfo = (paramsPath: { id: number }, params: { 
interval: TimeRangeType }) => {
   return get<MetricsData>(`/metrics/clusters/${paramsPath.id}`, params)
@@ -27,3 +27,7 @@ export const getClusterMetricsInfo = (paramsPath: { id: 
number }, params: { inte
 export const getHostMetricsInfo = (paramsPath: { id: number }, params: { 
interval: TimeRangeType }) => {
   return get<MetricsData>(`/metrics/hosts/${paramsPath.id}`, params)
 }
+
+export const getServiceMetricsInfo = (paramsPath: { id: number }, params: { 
interval: TimeRangeType }) => {
+  return get<ServiceMetrics>(`/metrics/services/${paramsPath.id}`, params)
+}
diff --git a/bigtop-manager-ui/src/api/metrics/types.ts 
b/bigtop-manager-ui/src/api/metrics/types.ts
index 86e4ec30..88421ea9 100644
--- a/bigtop-manager-ui/src/api/metrics/types.ts
+++ b/bigtop-manager-ui/src/api/metrics/types.ts
@@ -18,6 +18,7 @@
  */
 
 export type TimeRangeType = '1m' | '5m' | '15m' | '30m' | '1h' | '2h'
+
 export type MetricsData = {
   cpuUsageCur: string
   memoryUsageCur: string
@@ -34,3 +35,29 @@ export type MetricsData = {
   diskWrite: string[]
   timestamps: string[]
 }
+
+export type ServiceMetricType =
+  | 'NUMBER' // Numeric value, unitless
+  | 'PERCENT' // Percent, followed by %
+  | 'BYTE' // Byte, the front end can convert it through formatFromByte in 
storage util, the unit is B/KB/MB, etc.
+  | 'MILLISECOND' // milliseconds, followed by ms
+  | 'BPS' // Bytes per second, followed by B/s
+  | 'NPS' // Numeric value per second, followed by N/s
+
+type ServiceMetricItemSeries = {
+  name: string
+  type: string
+  data: any[]
+  [propName: string]: any
+}
+
+export type ServiceMetricItem = {
+  title: string
+  valueType: Lowercase<ServiceMetricType>
+  series: ServiceMetricItemSeries[]
+}
+
+export type ServiceMetrics = {
+  charts: ServiceMetricItem[]
+  timestamps: string[]
+}
diff --git 
a/bigtop-manager-ui/src/features/create-service/components/service-configurator.vue
 
b/bigtop-manager-ui/src/features/create-service/components/service-configurator.vue
index fccbaa30..311b4614 100644
--- 
a/bigtop-manager-ui/src/features/create-service/components/service-configurator.vue
+++ 
b/bigtop-manager-ui/src/features/create-service/components/service-configurator.vue
@@ -95,8 +95,8 @@
     for (const service of selectedServices.value) {
       if (!service.name || map.has(service.name)) continue
 
-      const requireds = extractRequireds(service.configs)
-      if (requireds.length > 0) map.set(service.name, requireds)
+      const requires = extractRequires(service.configs)
+      if (requires.length > 0) map.set(service.name, requires)
     }
 
     return map
@@ -168,10 +168,7 @@
   const matchKeyword = (keyword: string, prop: Property, config?: 
ServiceConfig) => {
     const lowerKeyword = keyword.toLowerCase()
     const includesProp =
-      prop.name?.toLowerCase().includes(lowerKeyword) ||
-      prop.value?.toLowerCase().includes(lowerKeyword) ||
-      prop.displayName?.toLowerCase().includes(lowerKeyword)
-
+      prop.name?.toLowerCase().includes(lowerKeyword) || 
prop.value?.toLowerCase().includes(lowerKeyword)
     if (config != undefined) {
       return config.name?.toLowerCase().includes(lowerKeyword) || includesProp
     }
@@ -186,13 +183,13 @@
    * @param configs - The service configurations to check.
    * @returns A list of configurations with their required properties.
    */
-  const extractRequireds = (configs?: ServiceConfig[]) => {
+  const extractRequires = (configs?: ServiceConfig[]) => {
     const result: ServiceConfig[] = []
 
     for (const config of configs ?? []) {
-      const requireds = config.properties?.filter((p) => p.attrs?.required && 
p.value == '') ?? []
-      if (requireds.length > 0) {
-        result.push({ [config.name!]: requireds })
+      const requires = config.properties?.filter((p) => p.attrs?.required && 
p.value == '') ?? []
+      if (requires.length > 0) {
+        result.push({ [config.name!]: requires })
       }
     }
 
@@ -299,12 +296,12 @@
                       />
                       <div
                         v-else
-                        :title="property.displayName ?? property.name"
+                        :title="property.name"
                         class="property-name"
                         :class="{ 'required-mark': property.attrs?.required }"
                       >
                         <span>
-                          {{ property.displayName ?? property.name }}
+                          {{ property.name }}
                         </span>
                       </div>
                     </a-form-item>
diff --git a/bigtop-manager-ui/src/features/metric/category-chart.vue 
b/bigtop-manager-ui/src/features/metric/category-chart.vue
index c64e4c78..5185df2e 100644
--- a/bigtop-manager-ui/src/features/metric/category-chart.vue
+++ b/bigtop-manager-ui/src/features/metric/category-chart.vue
@@ -147,10 +147,10 @@
    * @param legendMap - An array of [key, displayName] pairs.
    * @returns An array of ECharts series config objects with populated and 
formatted data.
    */
-  const generateChartSeries = <T,>(data: Partial<T>, legendMap: [string, 
string][]) => {
+  const generateChartSeries = <T,>(data: Partial<T>, legendMap: [string, 
string][], config = baseConfig) => {
     return legendMap.map(([key, name]) => ({
       name,
-      ...baseConfig,
+      ...config,
       data: (data[key] || []).map((v: unknown) => roundFixed(v))
     }))
   }
@@ -164,24 +164,25 @@
 
   watchEffect(() => {
     let series = [] as any,
-      legend = [] as any
+      legend = { data: [] } as any
+
+    const { series: temp_series = [] } = data.value
+    const xAxis = xAxisData.value?.map((v) => dayjs(Number(v) * 
1000).format('HH:mm')) ?? []
 
     if (legendMap.value) {
       legend = new Map(legendMap.value).values()
       series = generateChartSeries(data.value, legendMap.value)
     } else {
-      series = [
-        {
-          name: title.value.toLowerCase(),
-          data: data.value.map((v) => roundFixed(v))
-        }
-      ]
+      if (temp_series.length > 1) {
+        legend.data = temp_series.map((s) => s.name)
+        series = [...temp_series]
+      } else {
+        series = [{ name: title.value.toLowerCase(), data: data.value.map((v) 
=> roundFixed(v)) }]
+      }
     }
 
     setOptions({
-      xAxis: xAxisData.value
-        ? [{ data: xAxisData.value?.map((v) => dayjs(Number(v) * 
1000).format('HH:mm')) || [] }]
-        : [],
+      xAxis: [{ data: xAxis ?? xAxis }],
       ...config.value,
       legend,
       series
diff --git a/bigtop-manager-ui/src/features/service-management/configs.vue 
b/bigtop-manager-ui/src/features/service-management/configs.vue
index 00027c6d..a1db8c52 100644
--- a/bigtop-manager-ui/src/features/service-management/configs.vue
+++ b/bigtop-manager-ui/src/features/service-management/configs.vue
@@ -119,10 +119,7 @@
   const matchKeyword = (keyword: string, prop: Property, config?: 
ServiceConfig) => {
     const lowerKeyword = keyword.toLowerCase()
     const includesProp =
-      prop.name?.toLowerCase().includes(lowerKeyword) ||
-      prop.value?.toLowerCase().includes(lowerKeyword) ||
-      prop.displayName?.toLowerCase().includes(lowerKeyword)
-
+      prop.name?.toLowerCase().includes(lowerKeyword) || 
prop.value?.toLowerCase().includes(lowerKeyword)
     if (config != undefined) {
       return config.name?.toLowerCase().includes(lowerKeyword) || includesProp
     }
@@ -233,12 +230,12 @@
                       />
                       <div
                         v-else
-                        :title="property.displayName ?? property.name"
+                        :title="property.name"
                         class="property-name"
                         :class="{ 'required-mark': property.attrs?.required }"
                       >
                         <span>
-                          {{ property.displayName ?? property.name }}
+                          {{ property.name }}
                         </span>
                       </div>
                     </a-form-item>
diff --git a/bigtop-manager-ui/src/features/service-management/index.vue 
b/bigtop-manager-ui/src/features/service-management/index.vue
index 748fac7e..d0679b95 100644
--- a/bigtop-manager-ui/src/features/service-management/index.vue
+++ b/bigtop-manager-ui/src/features/service-management/index.vue
@@ -135,7 +135,10 @@
     <main-card v-model:active-key="activeKey" :tabs="tabs">
       <template #tab-item>
         <keep-alive>
-          <component :is="getCompName" v-bind="{ ...serviceDetail, clusterId: 
routeParams.id }"></component>
+          <component
+            :is="getCompName"
+            v-bind="{ ...serviceDetail, clusterId: routeParams.id, 
...routeParams }"
+          ></component>
         </keep-alive>
       </template>
     </main-card>
diff --git a/bigtop-manager-ui/src/features/service-management/overview.vue 
b/bigtop-manager-ui/src/features/service-management/overview.vue
index a55e2ca5..89e9001a 100644
--- a/bigtop-manager-ui/src/features/service-management/overview.vue
+++ b/bigtop-manager-ui/src/features/service-management/overview.vue
@@ -19,63 +19,42 @@
 
 <script setup lang="ts">
   import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import { Empty } from 'ant-design-vue'
+  import { formatFromByte } from '@/utils/storage.ts'
+  import { getServiceMetricsInfo } from '@/api/metrics'
+  import { isEmpty } from '@/utils/tools'
 
-  import GaugeChart from '@/features/metric/gauge-chart.vue'
   import CategoryChart from '@/features/metric/category-chart.vue'
 
-  import { Empty } from 'ant-design-vue'
   import type { ServiceVO, ServiceStatusType } from '@/api/service/types'
-
-  type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
-  type TimeRangeItem = {
-    text: TimeRangeText
-    time: string
-  }
+  import type { ServiceMetricItem, ServiceMetrics, ServiceMetricType, 
TimeRangeType } from '@/api/metrics/types'
 
   const { t } = useI18n()
-  const attrs = useAttrs() as unknown as Required<ServiceVO> & { clusterId: 
number }
-  const currTimeRange = ref<TimeRangeText>('15m')
-  const chartData = ref({
-    chart1: [],
-    chart2: [],
-    chart3: [],
-    chart4: []
-  })
+  const attrs = useAttrs() as any
+  const currTimeRange = ref<TimeRangeType>('5m')
+  const chartData = ref<Partial<ServiceMetrics>>({})
+
+  const timeRanges = shallowRef<TimeRangeType[]>(['1m', '5m', '15m', '30m', 
'1h', '2h'])
   const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof 
CommonStatusTexts>>({
     1: 'healthy',
     2: 'unhealthy',
     3: 'unknown'
   })
+
+  const unitMap: Record<Lowercase<ServiceMetricType>, string | ((value: 
number) => string)> = {
+    number: '',
+    percent: '%',
+    byte: (val) => formatFromByte(val, 0),
+    millisecond: 'ms',
+    bps: 'B/s',
+    nps: 'N/s'
+  }
+
+  const clusterId = computed(() => attrs.id)
+  const noChartData = computed(() => Object.values(chartData.value).length === 
0)
   const serviceKeys = computed(() => Object.keys(baseConfig.value) as (keyof 
ServiceVO)[])
-  const noChartData = computed(() => Object.values(chartData.value).every((v) 
=> v.length === 0))
-  const timeRanges = computed((): TimeRangeItem[] => [
-    {
-      text: '1m',
-      time: ''
-    },
-    {
-      text: '15m',
-      time: ''
-    },
-    {
-      text: '30m',
-      time: ''
-    },
-    {
-      text: '1h',
-      time: ''
-    },
-    {
-      text: '6h',
-      time: ''
-    },
-    {
-      text: '30h',
-      time: ''
-    }
-  ])
-  const baseConfig = computed((): Partial<Record<keyof ServiceVO, string>> => {
-    return {
+  const baseConfig = computed(
+    (): Partial<Record<keyof ServiceVO, string>> => ({
       status: t('overview.service_status'),
       displayName: t('overview.service_name'),
       version: t('overview.service_version'),
@@ -83,12 +62,45 @@
       restartFlag: t('service.required_restart'),
       metrics: t('overview.metrics'),
       kerberos: t('overview.kerberos')
+    })
+  )
+
+  const handleTimeRange = (time: TimeRangeType) => {
+    if (currTimeRange.value === time) return
+    currTimeRange.value = time
+    getServiceMetrics()
+  }
+
+  const getServiceMetrics = async () => {
+    try {
+      const res = await getServiceMetricsInfo({ id: attrs.serviceId }, { 
interval: currTimeRange.value })
+      chartData.value = { ...res }
+    } catch (error) {
+      console.log('Failed to fetch service metrics:', error)
     }
-  })
+  }
 
-  const handleTimeRange = (time: TimeRangeItem) => {
-    currTimeRange.value = time.text
+  const getChartFormatter = (chart: ServiceMetricItem) => {
+    const unit = unitMap[chart.valueType]
+    const valueWithUnit = (val: any) => (typeof unit === 'function' ? unit(val 
as number) : `${val} ${unit}`)
+    return {
+      tooltip: (val: any) => `${isEmpty(val) ? '--' : valueWithUnit(val)}`,
+      yAxis: valueWithUnit
+    }
   }
+
+  const { pause, resume } = useIntervalFn(getServiceMetrics, 30000, { 
immediate: true })
+
+  onActivated(() => {
+    if (clusterId.value == 0) return
+    getServiceMetrics()
+    resume()
+  })
+
+  onDeactivated(() => {
+    if (clusterId.value == 0) return
+    pause()
+  })
 </script>
 
 <template>
@@ -155,13 +167,13 @@
           <a-space :size="12">
             <div
               v-for="time in timeRanges"
-              :key="time.text"
+              :key="time"
               tabindex="0"
               class="time-range"
-              :class="{ 'time-range-activated': currTimeRange === time.text }"
+              :class="{ 'time-range-activated': currTimeRange === time }"
               @click="handleTimeRange(time)"
             >
-              {{ time.text }}
+              {{ time }}
             </div>
           </a-space>
         </div>
@@ -171,24 +183,15 @@
           </div>
         </template>
         <a-row v-else class="box-content">
-          <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
-            <div class="chart-item-wrp">
-              <gauge-chart chart-id="chart1" 
:title="t('overview.memory_usage')" />
-            </div>
-          </a-col>
-          <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
-            <div class="chart-item-wrp">
-              <gauge-chart chart-id="chart2" :title="t('overview.cpu_usage')" 
/>
-            </div>
-          </a-col>
-          <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
-            <div class="chart-item-wrp">
-              <category-chart chart-id="chart4" 
:title="t('overview.cpu_usage')" />
-            </div>
-          </a-col>
-          <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+          <a-col v-for="(chart, index) in chartData.charts" :key="index" 
:xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <category-chart chart-id="chart3" 
:title="t('overview.memory_usage')" />
+              <category-chart
+                :chart-id="`chart${index}`"
+                :x-axis-data="chartData?.timestamps"
+                :data="chart"
+                :title="chart.title"
+                :formatter="getChartFormatter(chart)"
+              />
             </div>
           </a-col>
         </a-row>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
index 5bb75204..3df28895 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
@@ -119,8 +119,8 @@
     />
     <main-card v-model:active-key="activeTab" :tabs="tabs">
       <template #tab-item>
-        <keep-alive>
-          <component :is="getCompName" :key="activeTab" v-bind="currCluster" 
v-model:payload="currCluster"></component>
+        <keep-alive :key="activeTab">
+          <component :is="getCompName" v-bind="currCluster" 
v-model:payload="currCluster"></component>
         </keep-alive>
       </template>
     </main-card>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
index ed8ef9e7..7e375cb0 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
@@ -20,7 +20,7 @@
 <script setup lang="ts">
   import { Empty } from 'ant-design-vue'
   import { formatFromByte } from '@/utils/storage.ts'
-  import { usePngImage } from '@/utils/tools'
+  import { usePngImage, isEmpty } from '@/utils/tools'
   import { CommonStatus, CommonStatusTexts } from '@/enums/state.ts'
   import { useServiceStore } from '@/store/service'
   import { useJobProgress } from '@/store/job-progress'
@@ -323,7 +323,7 @@
                 :data="chartData"
                 :title="t('overview.system_load')"
                 :formatter="{
-                  tooltip: (val) => `${val == null || val == '' ? '--' : val}`,
+                  tooltip: (val) => `${isEmpty(val) ? '--' : val}`,
                   yAxis: (val) => `${val}`
                 }"
                 :legend-map="[
@@ -342,7 +342,7 @@
                 :data="chartData"
                 :title="t('overview.disk_io')"
                 :formatter="{
-                  tooltip: (val) => `${val == null || val == '' ? '--' : 
formatFromByte(val as number, 0)}`,
+                  tooltip: (val) => `${isEmpty(val) ? '--' : 
formatFromByte(val as number, 0)}`,
                   yAxis: (val) => formatFromByte(val as number, 0)
                 }"
                 :legend-map="[
diff --git a/bigtop-manager-ui/src/store/create-service/validation.ts 
b/bigtop-manager-ui/src/store/create-service/validation.ts
index 5db870f9..3c751f36 100644
--- a/bigtop-manager-ui/src/store/create-service/validation.ts
+++ b/bigtop-manager-ui/src/store/create-service/validation.ts
@@ -33,17 +33,13 @@ export function useValidations() {
   /**
    * Validate services from infrastructure
    * @param targetService - The service being validated
-   * @param requireds - List of required services
+   * @param requires - List of required services
    * @param infraNames - List of available infrastructure services
    * @returns Whether there are missing services
    */
-  function validServiceFromInfra(
-    targetService: ExpandServiceVO,
-    requireds: string[] | undefined,
-    infraNames: string[]
-  ) {
+  function validServiceFromInfra(targetService: ExpandServiceVO, requires: 
string[] | undefined, infraNames: string[]) {
     const installedInfra = new 
Set(serviceStore.getInstalledNamesOrIdsOfServiceByKey('0', 'names'))
-    const missingServiceNames = (requireds ?? []).filter(
+    const missingServiceNames = (requires ?? []).filter(
       (name) => !installedInfra.has(name) && infraNames.includes(name)
     )
 
@@ -62,19 +58,19 @@ export function useValidations() {
    * Confirm dependency addition or removal
    * @param type - The action type ('add' or 'remove')
    * @param targetService - The target service
-   * @param requireds - The required service
+   * @param requires - The required service
    * @returns A promise resolving to the user's decision
    */
   function confirmDependencyAddition(
     type: 'add' | 'remove',
     targetService: ExpandServiceVO,
-    requireds: ExpandServiceVO
+    requires: ExpandServiceVO
   ) {
     const content = type === 'add' ? 'dependencies_add_msg' : 
'dependencies_remove_msg'
 
     return new Promise((resolve) => {
       confirmModal({
-        tipText: t(`service.${content}`, [targetService.displayName, 
requireds.displayName]),
+        tipText: t(`service.${content}`, [targetService.displayName, 
requires.displayName]),
         cancelText: t('common.no'),
         okText: t('common.yes'),
         onOk: () => resolve(true),
diff --git a/bigtop-manager-ui/src/utils/tools.ts 
b/bigtop-manager-ui/src/utils/tools.ts
index 91ea96d0..2b3cb572 100644
--- a/bigtop-manager-ui/src/utils/tools.ts
+++ b/bigtop-manager-ui/src/utils/tools.ts
@@ -17,6 +17,13 @@
  * under the License.
  */
 
+/**
+ * Copies the given text to the clipboard.
+ * Uses the Clipboard API if available, otherwise falls back to ClipboardJS.
+ *
+ * @param text - The text to copy.
+ * @returns A promise that resolves on success or rejects on failure.
+ */
 export function copyText(text: string): Promise<any> {
   if (navigator.clipboard) {
     return navigator.clipboard.writeText(text)
@@ -25,7 +32,7 @@ export function copyText(text: string): Promise<any> {
     try {
       const { default: ClipboardJS } = await import('clipboard')
       if (!ClipboardJS.isSupported()) {
-        reject(new Error('ClipboardJS not support!'))
+        reject(new Error('ClipboardJS not supported!'))
         return
       }
       const btn = document.createElement('button')
@@ -43,15 +50,26 @@ export function copyText(text: string): Promise<any> {
       })
       btn.click()
     } catch (error) {
-      console.log('copytext :>> ', error)
+      console.log('copyText :>> ', error)
     }
   })
 }
 
+/**
+ * Returns the URL of a PNG image from the assets folder.
+ *
+ * @param imageName - The name of the image (default is 'logo').
+ * @returns The URL of the image.
+ */
 export function usePngImage(imageName: string = 'logo'): string {
   return new URL(`../assets/images/${imageName}.png`, import.meta.url).href
 }
 
+/**
+ * Scrolls the given container element to the bottom.
+ *
+ * @param container - The container element to scroll.
+ */
 export function scrollToBottom(container: HTMLElement | null) {
   if (!container) {
     return
@@ -61,16 +79,35 @@ export function scrollToBottom(container: HTMLElement | 
null) {
   })
 }
 
+/**
+ * Generates a random number string based on the current timestamp.
+ *
+ * @param len - The length of the random string (default is 6).
+ * @returns A random string derived from the timestamp.
+ */
 export function getRandomFromTimestamp(len: number = 6) {
   return Date.now().toString().slice(-len)
 }
 
+/**
+ * Generates a random alphanumeric string of the specified length.
+ *
+ * @param length - The length of the random string (default is 8).
+ * @returns A random alphanumeric string.
+ */
 export function generateRandomId(length = 8) {
   return Math.random()
     .toString(36)
     .substring(2, length + 2)
 }
 
+/**
+ * Picks specific keys from an object and returns a new object with those keys.
+ *
+ * @param obj - The source object.
+ * @param keys - The keys to pick from the object.
+ * @returns A new object with the picked keys.
+ */
 export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): 
Pick<T, K> {
   return keys.reduce(
     (acc, key) => {
@@ -83,9 +120,25 @@ export function pick<T extends object, K extends keyof 
T>(obj: T, keys: K[]): Pi
   )
 }
 
+/**
+ * Creates a new object with a unique key added to it.
+ *
+ * @param item - The source object.
+ * @returns A new object with an added `__key` property.
+ */
 export function createKeyedItem<T extends object>(item: T): T & { __key: 
string } {
   return {
     ...item,
     __key: crypto.randomUUID?.() ?? `${Date.now()}-${Math.random()}`
   }
 }
+
+/**
+ * Checks if a value is empty (undefined, null, or an empty string).
+ *
+ * @param value - The value to check.
+ * @returns True if the value is empty, otherwise false.
+ */
+export function isEmpty<T>(value: T): boolean {
+  return value === undefined || value === null || value === ''
+}

Reply via email to