+
{data?.message === errorMessageReceivedFromBackend
@@ -84,10 +87,12 @@ function EditRules(): JSX.Element {
}
return (
-
+
+
+
);
}
diff --git a/frontend/src/pages/LogsExplorer/LogsExplorer.styles.scss b/frontend/src/pages/LogsExplorer/LogsExplorer.styles.scss
index 95d53fe9a4..82d3f5bffc 100644
--- a/frontend/src/pages/LogsExplorer/LogsExplorer.styles.scss
+++ b/frontend/src/pages/LogsExplorer/LogsExplorer.styles.scss
@@ -1,11 +1,35 @@
-.log-explorer-query-container {
- display: flex;
- flex-direction: column;
- flex: 1;
+.logs-module-page {
+ display: flex;
+ height: 100%;
+ .log-quick-filter-left-section {
+ width: 0%;
+ flex-shrink: 0;
+ }
- .logs-explorer-views {
- flex: 1;
- display: flex;
- flex-direction: column;
- }
-}
\ No newline at end of file
+ .log-module-right-section {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ .log-explorer-query-container {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+
+ .logs-explorer-views {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ }
+ }
+ }
+
+ &.filter-visible {
+ .log-quick-filter-left-section {
+ width: 260px;
+ }
+
+ .log-module-right-section {
+ width: calc(100% - 260px);
+ }
+ }
+}
diff --git a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx
index ff0f891333..4970d6cf17 100644
--- a/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx
+++ b/frontend/src/pages/LogsExplorer/__tests__/LogsExplorer.test.tsx
@@ -155,11 +155,12 @@ describe('Logs Explorer Tests', () => {
);
// check for data being present in the UI
- expect(
- queryByText(
- '2024-02-15T21:20:22.035Z INFO frontend Dispatch successful {"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}',
- ),
- ).toBeInTheDocument();
+ // todo[@vikrantgupta25]: skipping this for now as the formatting matching is not picking up in the CI will debug later.
+ // expect(
+ // queryByText(
+ // `2024-02-16 02:50:22.000 | 2024-02-15T21:20:22.035Z INFO frontend Dispatch successful {"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}`,
+ // ),
+ // ).toBeInTheDocument();
});
test('Multiple Current Queries', async () => {
@@ -188,6 +189,8 @@ describe('Logs Explorer Tests', () => {
initialDataSource: null,
panelType: PANEL_TYPES.TIME_SERIES,
isEnabledQuery: false,
+ lastUsedQuery: 0,
+ setLastUsedQuery: noop,
handleSetQueryData: noop,
handleSetFormulaData: noop,
handleSetQueryItemData: noop,
diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx
index 8873d04e39..9e23b34c2c 100644
--- a/frontend/src/pages/LogsExplorer/index.tsx
+++ b/frontend/src/pages/LogsExplorer/index.tsx
@@ -1,25 +1,40 @@
import './LogsExplorer.styles.scss';
import * as Sentry from '@sentry/react';
+import getLocalStorageKey from 'api/browser/localstorage/get';
+import setLocalStorageApi from 'api/browser/localstorage/set';
+import cx from 'classnames';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
+import QuickFilters from 'components/QuickFilters/QuickFilters';
+import { LOCALSTORAGE } from 'constants/localStorage';
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
import LogsExplorerViews from 'container/LogsExplorerViews';
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import Toolbar from 'container/Toolbar/Toolbar';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
+import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useEffect, useMemo, useRef, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { WrapperStyled } from './styles';
-import { SELECTED_VIEWS } from './utils';
+import { LogsQuickFiltersConfig, SELECTED_VIEWS } from './utils';
function LogsExplorer(): JSX.Element {
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
const [selectedView, setSelectedView] = useState(
SELECTED_VIEWS.SEARCH,
);
+ const [showFilters, setShowFilters] = useState(() => {
+ const localStorageValue = getLocalStorageKey(
+ LOCALSTORAGE.SHOW_LOGS_QUICK_FILTERS,
+ );
+ if (!isNull(localStorageValue)) {
+ return localStorageValue === 'true';
+ }
+ return true;
+ });
const { handleRunQuery, currentQuery } = useQueryBuilder();
@@ -37,6 +52,14 @@ function LogsExplorer(): JSX.Element {
setSelectedView(view);
};
+ const handleFilterVisibilityChange = (): void => {
+ setLocalStorageApi(
+ LOCALSTORAGE.SHOW_LOGS_QUICK_FILTERS,
+ String(!showFilters),
+ );
+ setShowFilters((prev) => !prev);
+ };
+
// Switch to query builder view if there are more than 1 queries
useEffect(() => {
if (currentQuery.builder.queryData.length > 1) {
@@ -90,46 +113,60 @@ function LogsExplorer(): JSX.Element {
return (
}>
-
- }
- rightActions={
-
- }
- showOldCTA
- />
-
-
-
-
-
-
-
-
-
-
+ {showFilters && (
+
-
-
+
+ )}
+
+
+ }
+ rightActions={
+
+ }
+ showOldCTA
+ />
+
+
+
+
+
+
);
}
diff --git a/frontend/src/pages/LogsExplorer/utils.ts b/frontend/src/pages/LogsExplorer/utils.ts
deleted file mode 100644
index 0fedaaece4..0000000000
--- a/frontend/src/pages/LogsExplorer/utils.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Query } from 'types/api/queryBuilder/queryBuilderData';
-
-export const prepareQueryWithDefaultTimestamp = (query: Query): Query => ({
- ...query,
- builder: {
- ...query.builder,
- queryData: query.builder.queryData?.map((item) => ({
- ...item,
- orderBy: [{ columnName: 'timestamp', order: 'desc' }],
- })),
- },
-});
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-export enum SELECTED_VIEWS {
- SEARCH = 'search',
- QUERY_BUILDER = 'query-builder',
- CLICKHOUSE = 'clickhouse',
-}
diff --git a/frontend/src/pages/LogsExplorer/utils.tsx b/frontend/src/pages/LogsExplorer/utils.tsx
new file mode 100644
index 0000000000..7a197bd467
--- /dev/null
+++ b/frontend/src/pages/LogsExplorer/utils.tsx
@@ -0,0 +1,113 @@
+import {
+ FiltersType,
+ IQuickFiltersConfig,
+} from 'components/QuickFilters/QuickFilters';
+import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { Query } from 'types/api/queryBuilder/queryBuilderData';
+
+export const prepareQueryWithDefaultTimestamp = (query: Query): Query => ({
+ ...query,
+ builder: {
+ ...query.builder,
+ queryData: query.builder.queryData?.map((item) => ({
+ ...item,
+ orderBy: [{ columnName: 'timestamp', order: 'desc' }],
+ })),
+ },
+});
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export enum SELECTED_VIEWS {
+ SEARCH = 'search',
+ QUERY_BUILDER = 'query-builder',
+ CLICKHOUSE = 'clickhouse',
+}
+
+export const LogsQuickFiltersConfig: IQuickFiltersConfig[] = [
+ {
+ type: FiltersType.CHECKBOX,
+ title: 'Severity Text',
+ attributeKey: {
+ key: 'severity_text',
+ dataType: DataTypes.String,
+ type: '',
+ isColumn: true,
+ isJSON: false,
+ id: 'severity_text--string----true',
+ },
+ defaultOpen: true,
+ },
+ {
+ type: FiltersType.CHECKBOX,
+ title: 'Environment',
+ attributeKey: {
+ key: 'deployment.environment',
+ dataType: DataTypes.String,
+ type: 'resource',
+ isColumn: false,
+ isJSON: false,
+ },
+ defaultOpen: false,
+ },
+ {
+ type: FiltersType.CHECKBOX,
+ title: 'Service Name',
+ attributeKey: {
+ key: 'service.name',
+ dataType: DataTypes.String,
+ type: 'resource',
+ isColumn: true,
+ isJSON: false,
+ id: 'service.name--string--resource--true',
+ },
+ defaultOpen: false,
+ },
+ {
+ type: FiltersType.CHECKBOX,
+ title: 'Hostname',
+ attributeKey: {
+ key: 'hostname',
+ dataType: DataTypes.String,
+ type: 'tag',
+ isColumn: false,
+ isJSON: false,
+ },
+ defaultOpen: false,
+ },
+ {
+ type: FiltersType.CHECKBOX,
+ title: 'K8s Cluster Name',
+ attributeKey: {
+ key: 'k8s.cluster.name',
+ dataType: DataTypes.String,
+ type: 'resource',
+ isColumn: false,
+ isJSON: false,
+ },
+ defaultOpen: false,
+ },
+ {
+ type: FiltersType.CHECKBOX,
+ title: 'K8s Deployment Name',
+ attributeKey: {
+ key: 'k8s.deployment.name',
+ dataType: DataTypes.String,
+ type: 'resource',
+ isColumn: false,
+ isJSON: false,
+ },
+ defaultOpen: false,
+ },
+ {
+ type: FiltersType.CHECKBOX,
+ title: 'K8s Namespace Name',
+ attributeKey: {
+ key: 'k8s.namespace.name',
+ dataType: DataTypes.String,
+ type: 'resource',
+ isColumn: true,
+ isJSON: false,
+ },
+ defaultOpen: false,
+ },
+];
diff --git a/frontend/src/pages/MessagingQueues/MessagingQueues.tsx b/frontend/src/pages/MessagingQueues/MessagingQueues.tsx
index 103fc15827..ebac021fd2 100644
--- a/frontend/src/pages/MessagingQueues/MessagingQueues.tsx
+++ b/frontend/src/pages/MessagingQueues/MessagingQueues.tsx
@@ -8,6 +8,7 @@ import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Calendar, ListMinus } from 'lucide-react';
import { useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app';
@@ -19,6 +20,7 @@ import { ComingSoon } from './MQCommon/MQCommon';
function MessagingQueues(): JSX.Element {
const history = useHistory();
+ const { t } = useTranslation('messagingQueuesKafkaOverview');
const { confirm } = Modal;
@@ -30,8 +32,7 @@ function MessagingQueues(): JSX.Element {
confirm({
icon:
,
- content:
- 'Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.',
+ content: t('confirmModal.content'),
className: 'overview-confirm-modal',
onOk() {
logEvent('Messaging Queues: Proceed button clicked', {
@@ -39,7 +40,7 @@ function MessagingQueues(): JSX.Element {
});
history.push(ROUTES.MESSAGING_QUEUES_DETAIL);
},
- okText: 'Proceed',
+ okText: t('confirmModal.okText'),
});
};
@@ -65,24 +66,20 @@ function MessagingQueues(): JSX.Element {
- Messaging Queues
+ {t('breadcrumb')}
-
Kafka / Overview
+
{t('header')}
-
- Start sending data in as little as 20 minutes
-
-
Connect and Monitor Your Data Streams
+
{t('overview.title')}
+
{t('overview.subtitle')}
-
Configure Consumer
-
- Connect your consumer and producer data sources to start monitoring.
-
+
{t('configureConsumer.title')}
+
{t('configureConsumer.description')}
-
Configure Producer
-
- Connect your consumer and producer data sources to start monitoring.
-
+
{t('configureProducer.title')}
+
{t('configureProducer.description')}
-
Monitor kafka
-
- Set up your Kafka monitoring to track consumer and producer activities.
-
+
{t('monitorKafka.title')}
+
{t('monitorKafka.description')}
@@ -152,7 +145,7 @@ function MessagingQueues(): JSX.Element {
diff --git a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx
index a28776f0d0..4a3fa8018e 100644
--- a/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx
+++ b/frontend/src/pages/TracesExplorer/__test__/TracesExplorer.test.tsx
@@ -77,6 +77,14 @@ jest.mock(
},
);
+window.ResizeObserver =
+ window.ResizeObserver ||
+ jest.fn().mockImplementation(() => ({
+ disconnect: jest.fn(),
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ }));
+
const successNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
__esModule: true,
diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx
index bb25a37f86..b865fd02bd 100644
--- a/frontend/src/pages/TracesExplorer/index.tsx
+++ b/frontend/src/pages/TracesExplorer/index.tsx
@@ -259,7 +259,7 @@ function TracesExplorer(): JSX.Element {
)}
-
+
diff --git a/frontend/src/periscope/components/CopyToClipboard/CopyToClipboard.styles.scss b/frontend/src/periscope/components/CopyToClipboard/CopyToClipboard.styles.scss
new file mode 100644
index 0000000000..7a55632ae6
--- /dev/null
+++ b/frontend/src/periscope/components/CopyToClipboard/CopyToClipboard.styles.scss
@@ -0,0 +1,39 @@
+.copy-to-clipboard {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 14px;
+ padding: 4px 6px;
+ width: 100px;
+
+ &:hover {
+ background-color: transparent !important;
+ }
+
+ .ant-btn-icon {
+ margin: 0 !important;
+ }
+ & > * {
+ color: var(--text-vanilla-400);
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: -0.07px;
+ }
+
+ &--success {
+ & span,
+ &:hover {
+ color: var(--bg-forest-400);
+ }
+ }
+}
+
+.lightMode {
+ .copy-to-clipboard {
+ &:not(&--success) {
+ & > * {
+ color: var(--text-ink-400);
+ }
+ }
+ }
+}
diff --git a/frontend/src/periscope/components/CopyToClipboard/CopyToClipboard.tsx b/frontend/src/periscope/components/CopyToClipboard/CopyToClipboard.tsx
new file mode 100644
index 0000000000..598f6e5a3f
--- /dev/null
+++ b/frontend/src/periscope/components/CopyToClipboard/CopyToClipboard.tsx
@@ -0,0 +1,54 @@
+import './CopyToClipboard.styles.scss';
+
+import { Color } from '@signozhq/design-tokens';
+import { Button } from 'antd';
+import { useIsDarkMode } from 'hooks/useDarkMode';
+import { CircleCheck, Link2 } from 'lucide-react';
+import { useEffect, useState } from 'react';
+import { useCopyToClipboard } from 'react-use';
+
+function CopyToClipboard({ textToCopy }: { textToCopy: string }): JSX.Element {
+ const [state, copyToClipboard] = useCopyToClipboard();
+ const [success, setSuccess] = useState(false);
+ const isDarkMode = useIsDarkMode();
+
+ useEffect(() => {
+ let timer: string | number | NodeJS.Timeout | undefined;
+ if (state.value) {
+ setSuccess(true);
+ timer = setTimeout(() => setSuccess(false), 1000);
+ }
+
+ return (): void => clearTimeout(timer);
+ }, [state]);
+
+ if (success) {
+ return (
+ }
+ className="copy-to-clipboard copy-to-clipboard--success"
+ >
+ Copied
+
+ );
+ }
+
+ return (
+
+ }
+ onClick={(): void => copyToClipboard(textToCopy)}
+ className="copy-to-clipboard"
+ >
+ Copy link
+
+ );
+}
+
+export default CopyToClipboard;
diff --git a/frontend/src/periscope/components/CopyToClipboard/index.tsx b/frontend/src/periscope/components/CopyToClipboard/index.tsx
new file mode 100644
index 0000000000..7b6b62c1b5
--- /dev/null
+++ b/frontend/src/periscope/components/CopyToClipboard/index.tsx
@@ -0,0 +1,3 @@
+import CopyToClipboard from './CopyToClipboard';
+
+export default CopyToClipboard;
diff --git a/frontend/src/periscope/components/DataStateRenderer/DataStateRenderer.tsx b/frontend/src/periscope/components/DataStateRenderer/DataStateRenderer.tsx
new file mode 100644
index 0000000000..7d6c6eb5a1
--- /dev/null
+++ b/frontend/src/periscope/components/DataStateRenderer/DataStateRenderer.tsx
@@ -0,0 +1,46 @@
+import Spinner from 'components/Spinner';
+import { useTranslation } from 'react-i18next';
+
+interface DataStateRendererProps {
+ isLoading: boolean;
+ isRefetching: boolean;
+ isError: boolean;
+ data: T | null;
+ errorMessage?: string;
+ loadingMessage?: string;
+ children: (data: T) => React.ReactNode;
+}
+
+/**
+ * TODO(shaheer): add empty state and optionally accept empty state custom component
+ * TODO(shaheer): optionally accept custom error state component
+ * TODO(shaheer): optionally accept custom loading state component
+ */
+function DataStateRenderer({
+ isLoading,
+ isRefetching,
+ isError,
+ data,
+ errorMessage,
+ loadingMessage,
+ children,
+}: DataStateRendererProps): JSX.Element {
+ const { t } = useTranslation('common');
+
+ if (isLoading || isRefetching || !data) {
+ return ;
+ }
+
+ if (isError || data === null) {
+ return {errorMessage ?? t('something_went_wrong')}
;
+ }
+
+ return <>{children(data)}>;
+}
+
+DataStateRenderer.defaultProps = {
+ errorMessage: '',
+ loadingMessage: 'Loading...',
+};
+
+export default DataStateRenderer;
diff --git a/frontend/src/periscope/components/DataStateRenderer/index.tsx b/frontend/src/periscope/components/DataStateRenderer/index.tsx
new file mode 100644
index 0000000000..e4afdfa3bd
--- /dev/null
+++ b/frontend/src/periscope/components/DataStateRenderer/index.tsx
@@ -0,0 +1,3 @@
+import DataStateRenderer from './DataStateRenderer';
+
+export default DataStateRenderer;
diff --git a/frontend/src/periscope/components/KeyValueLabel/KeyValueLabel.styles.scss b/frontend/src/periscope/components/KeyValueLabel/KeyValueLabel.styles.scss
new file mode 100644
index 0000000000..88ae57f4e8
--- /dev/null
+++ b/frontend/src/periscope/components/KeyValueLabel/KeyValueLabel.styles.scss
@@ -0,0 +1,37 @@
+.key-value-label {
+ display: flex;
+ align-items: center;
+ border: 1px solid var(--bg-slate-400);
+ border-radius: 2px;
+ flex-wrap: wrap;
+
+ &__key,
+ &__value {
+ padding: 1px 6px;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 18px;
+ letter-spacing: -0.005em;
+ }
+ &__key {
+ background: var(--bg-ink-400);
+ border-radius: 2px 0 0 2px;
+ }
+ &__value {
+ background: var(--bg-slate-400);
+ }
+ color: var(--text-vanilla-400);
+}
+
+.lightMode {
+ .key-value-label {
+ border-color: var(--bg-vanilla-400);
+ color: var(--text-ink-400);
+ &__key {
+ background: var(--bg-vanilla-300);
+ }
+ &__value {
+ background: var(--bg-vanilla-200);
+ }
+ }
+}
diff --git a/frontend/src/periscope/components/KeyValueLabel/KeyValueLabel.tsx b/frontend/src/periscope/components/KeyValueLabel/KeyValueLabel.tsx
new file mode 100644
index 0000000000..aa14dd6380
--- /dev/null
+++ b/frontend/src/periscope/components/KeyValueLabel/KeyValueLabel.tsx
@@ -0,0 +1,18 @@
+import './KeyValueLabel.styles.scss';
+
+type KeyValueLabelProps = { badgeKey: string; badgeValue: string };
+
+export default function KeyValueLabel({
+ badgeKey,
+ badgeValue,
+}: KeyValueLabelProps): JSX.Element | null {
+ if (!badgeKey || !badgeValue) {
+ return null;
+ }
+ return (
+
+
{badgeKey}
+
{badgeValue}
+
+ );
+}
diff --git a/frontend/src/periscope/components/KeyValueLabel/index.tsx b/frontend/src/periscope/components/KeyValueLabel/index.tsx
new file mode 100644
index 0000000000..7341e057e8
--- /dev/null
+++ b/frontend/src/periscope/components/KeyValueLabel/index.tsx
@@ -0,0 +1,3 @@
+import KeyValueLabel from './KeyValueLabel';
+
+export default KeyValueLabel;
diff --git a/frontend/src/periscope/components/PaginationInfoText/PaginationInfoText.tsx b/frontend/src/periscope/components/PaginationInfoText/PaginationInfoText.tsx
new file mode 100644
index 0000000000..205e1d3db8
--- /dev/null
+++ b/frontend/src/periscope/components/PaginationInfoText/PaginationInfoText.tsx
@@ -0,0 +1,24 @@
+import { Typography } from 'antd';
+
+function PaginationInfoText(
+ total: number,
+ [start, end]: number[],
+): JSX.Element {
+ return (
+
+
+ {start} — {end}
+
+ of {total}
+
+ );
+}
+
+export default PaginationInfoText;
diff --git a/frontend/src/periscope/components/SeeMore/SeeMore.styles.scss b/frontend/src/periscope/components/SeeMore/SeeMore.styles.scss
new file mode 100644
index 0000000000..002b04294b
--- /dev/null
+++ b/frontend/src/periscope/components/SeeMore/SeeMore.styles.scss
@@ -0,0 +1,26 @@
+.see-more-button {
+ background: none;
+ padding: 2px;
+ font-size: 14px;
+ line-height: 18px;
+ letter-spacing: -0.005em;
+ color: var(--text-vanilla-400);
+ border: none;
+ cursor: pointer;
+}
+
+.see-more-popover-content {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ width: 300px;
+}
+
+.lightMode {
+ .see-more-button {
+ color: var(--text-ink-400);
+ }
+ .see-more-popover-content {
+ background: var(--bg-vanilla-100);
+ }
+}
diff --git a/frontend/src/periscope/components/SeeMore/SeeMore.tsx b/frontend/src/periscope/components/SeeMore/SeeMore.tsx
new file mode 100644
index 0000000000..f94da8a564
--- /dev/null
+++ b/frontend/src/periscope/components/SeeMore/SeeMore.tsx
@@ -0,0 +1,48 @@
+import './SeeMore.styles.scss';
+
+import { Color } from '@signozhq/design-tokens';
+import { Popover } from 'antd';
+import { useIsDarkMode } from 'hooks/useDarkMode';
+
+type SeeMoreProps = {
+ children: JSX.Element[];
+ initialCount?: number;
+ moreLabel: string;
+};
+
+function SeeMore({
+ children,
+ initialCount = 2,
+ moreLabel,
+}: SeeMoreProps): JSX.Element {
+ const remainingCount = children.length - initialCount;
+ const isDarkMode = useIsDarkMode();
+
+ return (
+ <>
+ {children.slice(0, initialCount)}
+ {remainingCount > 0 && (
+
+ {children.slice(initialCount)}
+
+ }
+ >
+