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 === ''
+}