Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
enable_go_cache: false
enable_npm: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v9.2.1
uses: golangci/golangci-lint-action@v9.3.0
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v2.12.2
Expand Down
7 changes: 5 additions & 2 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@perses-dev/client",
"version": "0.54.0-beta.10",
"version": "0.54.0",
"description": "Functions as an API client or Data fetching Layer for interacting with a backend service",
"license": "Apache-2.0",
"homepage": "https://github.com/perses/perses/blob/main/README.md",
Expand All @@ -15,9 +15,12 @@
"main": "dist/cjs/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@perses-dev/spec": "0.2.0-beta.6",
"@perses-dev/spec": "0.2.0",
"zod": "^3.21.4"
},
"peerDependencies": {
"react": "^17.0.2 || ^18.0.0"
},
"scripts": {
"clean": "rimraf dist/",
"build": "concurrently \"npm:build:*\"",
Expand Down
90 changes: 90 additions & 0 deletions client/src/context/FetchContext.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { render, screen, waitFor } from '@testing-library/react';
import { FetchProvider, useFetch, FetchFn } from './FetchContext';

function TestConsumer(): React.ReactElement {
const { fetch } = useFetch();
return <button onClick={() => fetch('/test')}>fire</button>;
}

function TestJsonConsumer({ url }: { url: string }): React.ReactElement {
const { fetchJson } = useFetch();
return (
<button
onClick={async () => {
const data = await fetchJson<{ ok: boolean }>(url);
document.title = JSON.stringify(data);
}}
>
json
</button>
);
}

describe('FetchContext', () => {
describe('useFetch without provider', () => {
it('returns the default fetch wrapper from @perses-dev/client', () => {
let hookResult: ReturnType<typeof useFetch> | undefined;
function Capture(): React.ReactNode {
hookResult = useFetch();
return null;
}
render(<Capture />);
expect(hookResult).toBeDefined();
expect(typeof hookResult!.fetch).toBe('function');
expect(typeof hookResult!.fetchJson).toBe('function');
});
});

describe('FetchProvider with custom fetchFn', () => {
it('provides the custom fetch to useFetch consumers', async () => {
const customFetch: FetchFn = jest.fn().mockResolvedValue({
ok: true,
} as unknown as Response);

render(
<FetchProvider fetchFn={customFetch}>
<TestConsumer />
</FetchProvider>
);

screen.getByText('fire').click();

await waitFor(() => {
expect(customFetch).toHaveBeenCalledWith('/test');
});
});

it('derives fetchJson from the custom fetch', async () => {
const customFetch: FetchFn = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ ok: true }),
} as unknown as Response);

render(
<FetchProvider fetchFn={customFetch}>
<TestJsonConsumer url="/api/data" />
</FetchProvider>
);

screen.getByText('json').click();

await waitFor(() => {
expect(customFetch).toHaveBeenCalledWith('/api/data');
expect(document.title).toBe('{"ok":true}');
});
});
});
});
45 changes: 45 additions & 0 deletions client/src/context/FetchContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { createContext, ReactElement, ReactNode, useCallback, useContext } from 'react';
import { fetch as defaultFetch } from '../util/fetch';

export type FetchFn = (...args: Parameters<typeof globalThis.fetch>) => Promise<Response>;

const FetchContext = createContext<FetchFn>(defaultFetch);

export interface FetchProviderProps {
fetchFn: FetchFn;
children: ReactNode;
}

export function FetchProvider({ fetchFn, children }: FetchProviderProps): ReactElement {
return <FetchContext.Provider value={fetchFn}>{children}</FetchContext.Provider>;
}

export function useFetch(): {
fetch: FetchFn;
fetchJson: <T>(...args: Parameters<typeof globalThis.fetch>) => Promise<T>;
} {
const fetch = useContext(FetchContext);

const fetchJson = useCallback(
async <T,>(...args: Parameters<typeof globalThis.fetch>): Promise<T> => {
const response = await fetch(...args);
return await response.json();
},
[fetch]
);

return { fetch, fetchJson };
}
14 changes: 14 additions & 0 deletions client/src/context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

export * from './FetchContext';
1 change: 1 addition & 0 deletions client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
export * from './util';
export * from './model';
export * from './schema';
export * from './context';
7 changes: 3 additions & 4 deletions components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@perses-dev/components",
"version": "0.54.0-beta.10",
"version": "0.54.0",
"description": "Common UI components used across Perses features",
"license": "Apache-2.0",
"homepage": "https://github.com/perses/perses/blob/main/README.md",
Expand Down Expand Up @@ -34,8 +34,8 @@
"@date-fns/tz": "^1.4.1",
"@fontsource/inter": "^5.0.0",
"@mui/x-date-pickers": "^7.23.1",
"@perses-dev/spec": "0.2.0-beta.6",
"@perses-dev/client": "0.54.0-beta.10",
"@perses-dev/spec": "0.2.0",
"@perses-dev/client": "0.54.0",
"numbro": "^2.3.6",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-table": "^8.20.5",
Expand All @@ -49,7 +49,6 @@
"notistack": "^3.0.2",
"react-colorful": "^5.6.1",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.51.3",
"react-virtuoso": "^4.12.2"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions components/src/EChart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
// limitations under the License.

export * from './EChart';
export * from './timezone-formatter';
79 changes: 79 additions & 0 deletions components/src/EChart/timezone-formatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { createTimezoneAwareAxisFormatter } from './timezone-formatter';

// Mock formatWithTimeZone since it's from @perses-dev/components
jest.mock('@perses-dev/components', () => ({
formatWithTimeZone: jest.fn((date: Date, format: string, timeZone: string) => {
// Simple mock that returns format pattern with timezone
return `${format}[${timeZone}]`;
}),
}));

describe('createTimezoneAwareAxisFormatter', () => {
const testTimestamp = 1640995200000; // 2022-01-01 00:00:00 UTC
const timeZone = 'America/New_York';

it('should format for ranges > 5 years with year format', () => {
const formatter = createTimezoneAwareAxisFormatter(6 * 365 * 24 * 60 * 60 * 1000, timeZone);
const result = formatter(testTimestamp);
expect(result).toBe('yyyy[America/New_York]');
});

it('should format for ranges > 6 months with month-year format', () => {
const formatter = createTimezoneAwareAxisFormatter(3 * 365 * 24 * 60 * 60 * 1000, timeZone);
const result = formatter(testTimestamp);
expect(result).toBe('MMM yyyy[America/New_York]');
});

it('should format for ranges between 10 days and 6 months with day-month format', () => {
const formatter = createTimezoneAwareAxisFormatter(30 * 24 * 60 * 60 * 1000, timeZone); // 30 days
const result = formatter(testTimestamp);
expect(result).toBe('dd.MM[America/New_York]');
});

it('should format for ranges between 2-10 days with day-month-time format', () => {
const formatter = createTimezoneAwareAxisFormatter(5 * 24 * 60 * 60 * 1000, timeZone); // 5 days
const result = formatter(testTimestamp);
expect(result).toBe('dd.MM HH:mm[America/New_York]');
});

it('should format for ranges <= 2 days with time format', () => {
const formatter = createTimezoneAwareAxisFormatter(6 * 60 * 60 * 1000, timeZone); // 6 hours
const result = formatter(testTimestamp);
expect(result).toBe('HH:mm[America/New_York]');
});

it('should handle different timezones', () => {
const formatter = createTimezoneAwareAxisFormatter(6 * 60 * 60 * 1000, 'Europe/Prague');
const result = formatter(testTimestamp);
expect(result).toBe('HH:mm[Europe/Prague]');
});

it('should handle edge case at exactly 5 years', () => {
const fiveYears = 5 * 365 * 24 * 60 * 60 * 1000;
const formatter = createTimezoneAwareAxisFormatter(fiveYears, timeZone);
const result = formatter(testTimestamp);
// Should use MMM yyyy format (not > 5 years)
expect(result).toBe('MMM yyyy[America/New_York]');
});

it('should handle edge case at exactly 2 days', () => {
const twoDays = 2 * 24 * 60 * 60 * 1000;
const formatter = createTimezoneAwareAxisFormatter(twoDays, timeZone);
const result = formatter(testTimestamp);
// Should use HH:mm format (not > 2 days)
expect(result).toBe('HH:mm[America/New_York]');
});
});
50 changes: 50 additions & 0 deletions components/src/EChart/timezone-formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { formatWithTimeZone } from '@perses-dev/components';

const DAY_MS = 1000 * 60 * 60 * 24;
const MONTH_MS = DAY_MS * 30;
const YEAR_MS = DAY_MS * 365;

/**
* Creates a timezone-aware axis formatter function for different time ranges
*/
export function createTimezoneAwareAxisFormatter(rangeMs: number, timeZone: string) {
return function (value: number): string {
const timeStamp = new Date(Number(value));

// more than 5 years
if (rangeMs > YEAR_MS * 5) {
return formatWithTimeZone(timeStamp, 'yyyy', timeZone);
}

// more than 6 months
if (rangeMs > MONTH_MS * 6) {
return formatWithTimeZone(timeStamp, 'MMM yyyy', timeZone);
}

// more than 10 days
if (rangeMs > DAY_MS * 10) {
return formatWithTimeZone(timeStamp, 'dd.MM', timeZone);
}

// more than 2 days
if (rangeMs > DAY_MS * 2) {
return formatWithTimeZone(timeStamp, 'dd.MM HH:mm', timeZone);
}

// less or equal 2 days
return formatWithTimeZone(timeStamp, 'HH:mm', timeZone);
};
}
1 change: 0 additions & 1 deletion components/src/LinksEditor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@
// See the License for the specific language governing permissions and
// limitations under the License.

export * from './LinksEditor';
export * from './LinkEditorForm';
Loading
Loading