Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes to integrate rate-limit headers for Commands left component UI #67

Merged
merged 20 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
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
6 changes: 3 additions & 3 deletions apps/playground-web/components/CLI/CLI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
import { useCli } from './hooks/useCli';

interface CliProps {
decreaseCommandsLeft: () => void;
onCommandExecuted: (commandsLeft: number) => void;
}

export default function Cli({ decreaseCommandsLeft }: CliProps) {
export default function Cli({ onCommandExecuted }: CliProps) {
const {
handleInputChange,
handleKeyDown,
terminalRef,
inputRef,
output,
command,
} = useCli(decreaseCommandsLeft);
} = useCli(onCommandExecuted);
return (
<div
ref={terminalRef}
Expand Down
4 changes: 2 additions & 2 deletions apps/playground-web/components/CLI/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CLI from '../CLI';

const decreaseCommandsLeftMock = jest.fn();
const onCommandExecutedMock = jest.fn();

const dummyCommands = [
'set abc 100',
Expand All @@ -16,7 +16,7 @@ const dummyCommands = [

const setupTest = () => {
const user = userEvent.setup();
const utils = render(<CLI decreaseCommandsLeft={decreaseCommandsLeftMock} />);
const utils = render(<CLI onCommandExecuted={onCommandExecutedMock} />);

const terminalElement = screen.getByTestId('terminal');
const cliInputElement = screen.getByTestId<HTMLInputElement>('cli-input');
Expand Down
5 changes: 2 additions & 3 deletions apps/playground-web/components/CLI/hooks/useCli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useState, useEffect, useRef, KeyboardEvent, ChangeEvent } from 'react';
import { handleCommand } from '@/shared/utils/cliUtils';
import blocklistedCommands from '@/shared/utils/blocklist'; // Assuming you added blocklist here

export const useCli = (decreaseCommandsLeft: () => void) => {
export const useCli = (onCommandExecuted: (commandsLeft: number) => void) => {
// states
const [command, setCommand] = useState('');
const [output, setOutput] = useState<string[]>([]);
Expand All @@ -27,11 +27,10 @@ export const useCli = (decreaseCommandsLeft: () => void) => {
`(error) ERR unknown command '${commandName}'`,
]);
} else {
handleCommand({ command, setOutput }); // Execute if not blocklisted
handleCommand({ command, setOutput, onCommandExecuted }); // Execute if not blocklisted
}

setCommand(''); // Clear input
decreaseCommandsLeft(); // Call to update remaining commands
};

useEffect(() => {
Expand Down
7 changes: 4 additions & 3 deletions apps/playground-web/components/Playground/TerminalUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { useState } from 'react';
import Tooltip from '../Overlays/Tooltip';
export function TerminalUI({ initialCommandsLeft = 1000 }) {
const [commandsLeft, setCommandsLeft] = useState(initialCommandsLeft);
const decreaseCommandsLeft = () => {
setCommandsLeft((prev) => (prev > 0 ? prev - 1 : 0));
const handleCommandExecuted = (commands: number) => {
setCommandsLeft(commands);
};

return (
<>
<div className="bg-gray-900 rounded-lg" data-testid="terminal-container">
Expand All @@ -23,7 +24,7 @@ export function TerminalUI({ initialCommandsLeft = 1000 }) {
className="h-64 md:h-[30rem] bg-gray-100 rounded-lg overflow-hidden shadow-md"
data-testid="shell-container"
>
<Shell decreaseCommandsLeft={decreaseCommandsLeft} />
<Shell onCommandExecuted={handleCommandExecuted} />
</div>
</div>
<TerminalCounter commandsLeft={commandsLeft} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import userEvent from '@testing-library/user-event';
import { TerminalUI } from '../TerminalUI';
import { formatTime } from '@/shared/utils/commonUtils';

// Shared variable to store initialCommandsLeft
let sharedInitialCommandsLeft = 1000;

jest.mock('@/shared/hooks/useTimer', () => ({
useTimer: jest.fn(() => ({ timeLeft: 15 * 60 })), // Mock 15 minutes
}));

jest.mock('@/shared/utils/commonUtils', () => ({
formatTime: jest.fn(
(seconds) => `${Math.floor(seconds / 60)}:${seconds % 60}`,
),
}));

const setupTest = (initialCommandsLeft = 1000) => {
sharedInitialCommandsLeft = initialCommandsLeft;

const user = userEvent.setup();
const utils = render(
<TerminalUI initialCommandsLeft={initialCommandsLeft} />,
Expand All @@ -39,6 +38,21 @@ const setupTest = (initialCommandsLeft = 1000) => {
};
};

jest.mock('@/shared/utils/commonUtils', () => ({
formatTime: jest.fn(
(seconds) => `${Math.floor(seconds / 60)}:${seconds % 60}`,
),
handleResult: jest.fn(
({ result, newOutput, setOutput, onCommandExecuted }) => {
const mockData = result?.body?.data ? result.body.data : 'mock error';
setOutput([newOutput, mockData]);

const commandsLeft = Math.max(0, sharedInitialCommandsLeft - 1);
onCommandExecuted(commandsLeft);
},
),
}));

describe('TerminalUI Component', () => {
it('renders TerminalUI', () => {
const { terminalContainer, diceIcons, shellContainer, cliInputElement } =
Expand Down
6 changes: 3 additions & 3 deletions apps/playground-web/components/Shell/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
import { useShell } from './hooks/useShell';

interface ShellProps {
decreaseCommandsLeft: () => void;
onCommandExecuted: (commandsLeft: number) => void;
}

export default function Shell({ decreaseCommandsLeft }: ShellProps) {
export default function Shell({ onCommandExecuted }: ShellProps) {
const {
handleInputChange,
handleKeyDown,
terminalRef,
inputRef,
output,
command,
} = useShell(decreaseCommandsLeft);
} = useShell(onCommandExecuted);
return (
<div
ref={terminalRef}
Expand Down
6 changes: 2 additions & 4 deletions apps/playground-web/components/Shell/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Shell from '../Shell';

const decreaseCommandsLeftMock = jest.fn();
const onCommandExecutedMock = jest.fn();

const dummyCommands = [
'set abc 100',
Expand All @@ -16,9 +16,7 @@ const dummyCommands = [

const setupTest = () => {
const user = userEvent.setup();
const utils = render(
<Shell decreaseCommandsLeft={decreaseCommandsLeftMock} />,
);
const utils = render(<Shell onCommandExecuted={onCommandExecutedMock} />);

const terminalElement = screen.getByTestId('terminal');
const cliInputElement = screen.getByTestId<HTMLInputElement>('shell-input');
Expand Down
5 changes: 2 additions & 3 deletions apps/playground-web/components/Shell/hooks/useShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useState, useEffect, useRef, KeyboardEvent, ChangeEvent } from 'react';
import { handleCommand } from '@/shared/utils/shellUtils';
import blocklistedCommands from '@/shared/utils/blocklist';

export const useShell = (decreaseCommandsLeft: () => void) => {
export const useShell = (onCommandExecuted: (commandsLeft: number) => void) => {
// states
const [command, setCommand] = useState('');
const [output, setOutput] = useState<string[]>([]);
Expand All @@ -32,11 +32,10 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
`(error) ERR unknown command '${commandName}'`,
]);
} else {
handleCommand({ command, setOutput }); // Execute if not blocklisted
handleCommand({ command, setOutput, onCommandExecuted }); // Execute if not blocklisted
}

setCommand(''); // Clear input
decreaseCommandsLeft(); // Call to update remaining commands
};

useEffect(() => {
Expand Down
14 changes: 8 additions & 6 deletions apps/playground-web/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
// src/lib/api.ts
import { WebService } from '@/services/webServices';

import { MonoResponse } from './monoResponse';

export const executeShellCommandOnServer = async (
cmd: string,
cmdOptions: object,
) => {
): Promise<{ headers: { [key: string]: string }; body: MonoResponse }> => {
const cmdExecURL = `/shell/exec/${cmd}`;

try {
const response = await WebService.post(cmdExecURL, cmdOptions);

// Check if the response contains data or if it's an error response
if (response?.data) {
return response.data;
} else if (response?.error) {
return response.error;
if (response?.body?.data) {
return response;
} else if (response?.body?.error) {
return response;
} else {
throw new Error('Unexpected response structure');
}
} catch (error) {
// Propagate the error from the backend exactly as it is
console.error('Error executing command:', error);
return `${error}`; // Just return the error message directly
throw new Error(`${error}`);
}
};
4 changes: 4 additions & 0 deletions apps/playground-web/lib/monoResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface MonoResponse {
data: string | null;
error: string | null;
}
13 changes: 13 additions & 0 deletions apps/playground-web/lib/monoSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from 'zod';

const BodySchema = z.object({
data: z.string(),
});

// Define the schema for an error response
const ErrorSchema = z.object({
error: z.string(),
});

// Combine the schemas
export const MonoSchema = z.union([BodySchema, ErrorSchema]);
11 changes: 6 additions & 5 deletions apps/playground-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"test:coverage": "jest --coverage"
},
"dependencies": {
"@dicedb/ui": "workspace:*",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@mui/icons-material": "^6.1.1",
Expand All @@ -29,9 +30,12 @@
"react-dom": "^18",
"react-terminal-ui": "^1.3.0",
"sharp": "^0.33.5",
"@dicedb/ui": "workspace:*"
"zod": "^3.23.8"
},
"devDependencies": {
"@dicedb/eslint-config": "workspace:*",
"@dicedb/tailwind-config": "workspace:*",
"@dicedb/typescript-config": "workspace:*",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
Expand All @@ -48,9 +52,6 @@
"prettier": "^3.3.3",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"typescript": "^5",
"@dicedb/typescript-config": "workspace:*",
"@dicedb/eslint-config": "workspace:*",
"@dicedb/tailwind-config": "workspace:*"
"typescript": "^5"
}
}
54 changes: 41 additions & 13 deletions apps/playground-web/services/webServices.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { MonoSchema } from '@/lib/monoSchema';

import { MonoResponse } from '@/lib/monoResponse';

let PLAYGROUND_MONO_URL = process.env.NEXT_PUBLIC_PLAYGROUND_MONO_URL;

if (!PLAYGROUND_MONO_URL) {
Expand All @@ -24,7 +28,10 @@ export const WebService = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any = null,
headers: HeadersType,
) => {
): Promise<{
headers: { [key: string]: string };
body: MonoResponse;
}> => {
const options: RequestOptions = {
method,
headers: {
Expand All @@ -41,24 +48,45 @@ export const WebService = {
try {
const response = await fetch(`${PLAYGROUND_MONO_URL}${url}`, options);

// If the response is not OK, check if the response contains error information
if (!response.ok) {
const errorResponse = await response.json();
if (errorResponse?.error) {
throw errorResponse.error;
} else {
throw new Error(`HTTP error! Status: ${response.status}`);
}
}
const headers: { [key: string]: string } = {};
response?.headers?.forEach((value: string, key: string) => {
headers[key] = value;
});

// Parse the result as JSON
const result = await response.json();
return result;
const body = await response.json();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a type defined here? preferrably using zod schema. Idea is to ensure and catch early if there is an issue with the response.

right now, we just have a lot of any which should be avoided

const parsedBody = MonoSchema.safeParse(body);
if (!parsedBody.success) {
console.error('Invalid response data:', parsedBody.error);
return {
headers: headers,
body: { data: null, error: parsedBody?.error?.message },
};
}

if ('data' in parsedBody.data) {
return {
headers: headers,
body: { data: parsedBody?.data?.data, error: null },
};
} else {
return {
headers: headers,
body: { data: null, error: parsedBody.data.error },
};
}
} catch (error) {
if (error instanceof Error) {
console.error(`Error with ${method} request: ${error.message}`);
return {
headers: headers,
body: { error: `${error.message}`, data: null },
};
}
throw error;
return {
headers: headers,
body: { error: 'Unknown error occurred', data: null },
};
}
},

Expand Down
Loading
Loading