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 11 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, cleanupTimeLeft: 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
7 changes: 4 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,9 @@ 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, cleanupTimeLeft: number) => void,
) => {
// states
const [command, setCommand] = useState('');
const [output, setOutput] = useState<string[]>([]);
Expand All @@ -27,11 +29,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
29 changes: 20 additions & 9 deletions apps/playground-web/components/Playground/TerminalUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { formatTime } from '@/shared/utils/commonUtils';
import { useTimer } from '@/shared/hooks/useTimer';
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));
export function TerminalUI() {
const [commandsLeft, setCommandsLeft] = useState(1000);
const [cleanupTimeLeft, setCleanupTimeLeft] = useState(15 * 60);
const handleCommandExecuted = (commands: number, cleanup: number) => {
setCommandsLeft(commands);
setCleanupTimeLeft(cleanup);
};

return (
<>
<div className="bg-gray-900 rounded-lg" data-testid="terminal-container">
Expand All @@ -23,16 +26,24 @@ 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} />
<TerminalCounter
commandsLeft={commandsLeft}
cleanupTimeLeft={cleanupTimeLeft}
/>
</>
);
}

function TerminalCounter({ commandsLeft }: { commandsLeft: number }) {
const { timeLeft } = useTimer(15 * 60);
function TerminalCounter({
commandsLeft,
cleanupTimeLeft,
}: {
commandsLeft: number;
cleanupTimeLeft: number;
}) {
return (
<div className="flex flex-col" data-testid="terminal-counter">
<div className="flex flex-row justify-between text-gray-900 mt-4">
Expand All @@ -41,7 +52,7 @@ function TerminalCounter({ commandsLeft }: { commandsLeft: number }) {
className="flex items-center justify-between border border-gray-400 text-sm bg-transparent p-3 rounded-lg"
data-testid="cleanup-timer"
>
<span>Cleanup in: {formatTime(timeLeft)} mins</span>
<span>Cleanup in: {formatTime(cleanupTimeLeft)} mins</span>
</div>
<Tooltip message="The time remaining until cleanup is initiated." />
</div>
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, cleanupTimeLeft: 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
7 changes: 4 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,9 @@ 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, cleanupTimeLeft: number) => void,
) => {
// states
const [command, setCommand] = useState('');
const [output, setOutput] = useState<string[]>([]);
Expand All @@ -32,11 +34,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
8 changes: 4 additions & 4 deletions apps/playground-web/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export const executeShellCommandOnServer = async (
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');
}
Expand Down
17 changes: 6 additions & 11 deletions apps/playground-web/services/webServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,14 @@ 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

return { headers: headers, body: body };
} catch (error) {
if (error instanceof Error) {
console.error(`Error with ${method} request: ${error.message}`);
Expand Down
15 changes: 10 additions & 5 deletions apps/playground-web/shared/utils/cliUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

import { executeShellCommandOnServer } from '@/lib/api';
import { CommandHandler } from '@/types';
import { handleResult } from '@/shared/utils/commonUtils';

export const handleCommand = async ({ command, setOutput }: CommandHandler) => {
export const handleCommand = async ({
command,
setOutput,
onCommandExecuted,
}: CommandHandler) => {
const newOutput = `dice > ${command}`;
let result: string;
let result: any;

const [cmd, ...args] = command.split(' ');
if (!cmd) {
Expand All @@ -26,7 +31,7 @@ export const handleCommand = async ({ command, setOutput }: CommandHandler) => {
const [key] = args;
const cmdOptions = { key: key };
result = await executeShellCommandOnServer(cmd, cmdOptions);
setOutput((prevOutput) => [...prevOutput, newOutput, result]);
handleResult(result, newOutput, setOutput, onCommandExecuted);
} catch (error: unknown) {
console.error('Error executing command:', error);
result = 'Error executing command';
Expand All @@ -40,7 +45,7 @@ export const handleCommand = async ({ command, setOutput }: CommandHandler) => {
try {
const cmdOptions = { key: key, value: value };
result = await executeShellCommandOnServer(cmd, cmdOptions);
setOutput((prevOutput) => [...prevOutput, newOutput, result]);
handleResult(result, newOutput, setOutput, onCommandExecuted);
} catch (error: unknown) {
console.error('Error executing command:', error);
result = 'Error executing command';
Expand All @@ -59,7 +64,7 @@ export const handleCommand = async ({ command, setOutput }: CommandHandler) => {
try {
const cmdOptions = { keys: [keys] };
result = await executeShellCommandOnServer(cmd, cmdOptions);
setOutput((prevOutput) => [...prevOutput, newOutput, result]);
handleResult(result, newOutput, setOutput, onCommandExecuted);
} catch (error: unknown) {
console.error('Error executing command:', error);
result = 'Error executing command';
Expand Down
25 changes: 25 additions & 0 deletions apps/playground-web/shared/utils/commonUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,28 @@ export const formatTime = (seconds: number): string => {
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
};

export const handleResult = (
result: any,
newOutput: string,
setOutput: any,
onCommandExecuted: any,
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 avoid the any here? very easy to make mistakes on this. Let me know if you need any help with the actual types

) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

when we have more than 3 arguments, let's convert it into an object, so that we can have named arguments. so instead of hanleResult(result, newOutput, setOutput, onComandExecuted) => hanleResult({ result, newOutput, setOutput, onComandExecuted })

if (result?.body?.data) {
setOutput((prevOutput: any) => [
...prevOutput,
newOutput,
result?.body?.data,
]);
} else if (result?.body?.error) {
setOutput((prevOutput: any) => [
...prevOutput,
newOutput,
result?.body?.error,
]);
}

const commandsLeft = result?.headers?.['x-ratelimit-remaining'];
const cleanupTimeLeft = 10;
onCommandExecuted(commandsLeft, cleanupTimeLeft);
};
11 changes: 8 additions & 3 deletions apps/playground-web/shared/utils/shellUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

import { executeShellCommandOnServer } from '@/lib/api';
import { CommandHandler } from '@/types';
import { handleResult } from '@/shared/utils/commonUtils';

export const handleCommand = async ({ command, setOutput }: CommandHandler) => {
export const handleCommand = async ({
command,
setOutput,
onCommandExecuted,
}: CommandHandler) => {
const newOutput = `dice > ${command}`;
let result: string;
let result: any;

const [cmd, ...args] = command.split(' ');

Expand All @@ -14,7 +19,7 @@ export const handleCommand = async ({ command, setOutput }: CommandHandler) => {
}
try {
result = await executeShellCommandOnServer(cmd, args);
setOutput((prevOutput) => [...prevOutput, newOutput, result]);
handleResult(result, newOutput, setOutput, onCommandExecuted);
} catch (error: unknown) {
console.error('Error executing command:', error);
result = 'Error executing command';
Expand Down
1 change: 1 addition & 0 deletions apps/playground-web/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import * as React from 'react';
export interface CommandHandler {
command: string;
setOutput: React.Dispatch<React.SetStateAction<string[]>>;
onCommandExecuted: (commandsLeft: number, cleanupTimeLeft: number) => void;
}
26 changes: 26 additions & 0 deletions src/lib/__tests__/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { executeShellCommandOnServer } from '../api';
import { WebService } from '@/services/webServices';
import { handleCommand } from '@/shared/utils/cliUtils';

// Mock WebService
jest.mock('@/services/webServices', () => ({
Expand Down Expand Up @@ -90,4 +91,29 @@ describe('executeShellCommandOnServer', () => {
);
expect(result).toEqual('Some Response');
});

it('should call onCommandExecuted with (1000, any) for GET command', async () => {
const command = 'GET testKey';
const mockResult = {
body: { data: 'mockData' },
headers: { 'x-ratelimit-remaining': 1000 },
}; // Updated mock result
(executeShellCommandOnServer as jest.Mock).mockResolvedValueOnce(
mockResult,
); // Mock the API response

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

await handleCommand({
command,
setOutput: setOutputMock,
onCommandExecuted: onCommandExecutedMock,
});

expect(onCommandExecutedMock).toHaveBeenCalledWith(
1000,
expect.any(Number),
);
});
});