diff --git a/.eslintrc.json b/.eslintrc.json index 3dd1e4fa..da41e36d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -172,10 +172,6 @@ "prefer-rest-params": "error", "prefer-spread": "error", "prefer-template": "error", - "quotes": [ - "error", - "single" - ], "rest-spread-spacing": [ "error", "never" diff --git a/public/index.html b/public/index.html index 275a8833..77b11d48 100644 --- a/public/index.html +++ b/public/index.html @@ -9,6 +9,7 @@ +
diff --git a/src/App.module.css b/src/App.module.css index 9ef9f6ad..89ef9f2f 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -5,11 +5,17 @@ height: calc(100vh); } -.App-body { +.AppBody { color: white; padding: 15px; } +.loading { + align-items: center; + display: flex; + justify-content: center; +} + * { margin: 0; padding: 0; diff --git a/src/App.test.tsx b/src/App.test.tsx index f0c9c2d3..ecd1e403 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,11 +1,19 @@ import { render, screen } from '@testing-library/react' import App from './App' -import { webviewEventType } from './api/webviewEvent' -import { IDependencyPage, IEosPage, IIaCPage, ISecretsPage, PageType } from './model/webviewPages' +import { + IDependencyPage, + IEosPage, + IIaCPage, + ILoginPage, + ISecretsPage, + PageType +} from './model/webviewPages' import { ISeverity } from './model/severity' import { IImpactGraph } from './model/impactGraph' import { sendWebviewPage } from './utils/testUtils' import { IAnalysisStep } from './model/analysisStep' +import { WebviewReceiveEventType } from './api' +import { LoginConnectionType, LoginProgressStatus } from './model/login' describe('App component', () => { test('renders the Dependency page when the page type is "Dependency"', async () => { @@ -13,7 +21,7 @@ describe('App component', () => { // Load Dependency. const pageData = { - type: webviewEventType.ShowPage, + type: WebviewReceiveEventType.ShowPage, pageData: { id: 'Dependency-ID', pageType: PageType.Dependency, @@ -36,7 +44,7 @@ describe('App component', () => { // Load Eos page. const pageData = { - type: webviewEventType.ShowPage, + type: WebviewReceiveEventType.ShowPage, pageData: { pageType: PageType.Eos, header: 'Header-eos', @@ -54,7 +62,7 @@ describe('App component', () => { // Load IaC page. const pageData = { - type: webviewEventType.ShowPage, + type: WebviewReceiveEventType.ShowPage, pageData: { pageType: PageType.IaC, header: 'Header-iac', @@ -74,7 +82,7 @@ describe('App component', () => { // Load Secrets page. const pageData = { - type: webviewEventType.ShowPage, + type: WebviewReceiveEventType.ShowPage, pageData: { pageType: PageType.Secrets, header: 'Header-secret', @@ -89,10 +97,25 @@ describe('App component', () => { expect(screen.getByText('Header-secret')).toBeInTheDocument() }) - test('renders "Nothing to show" when the page type is not recognized', () => { + test('renders the Login page when the page type is "Login"', async () => { render() - // Assert that "Nothing to show" text is rendered. - expect(screen.getByText('Nothing to show')).toBeInTheDocument() + const pageData = { + type: WebviewReceiveEventType.ShowPage, + pageData: { + pageType: PageType.Login, + url: 'www.example.com', + status: LoginProgressStatus.Initial, + connectionType: LoginConnectionType.BasicAuthOrToken + } as ILoginPage + } + await sendWebviewPage(pageData) + + expect(screen.getByText('Welcome to JFrog')).toBeInTheDocument() + }) + + test('renders loading spinner initially', () => { + const { container } = render() + expect(container.querySelector('.loader')).toBeInTheDocument() }) }) diff --git a/src/App.tsx b/src/App.tsx index 5c6ccb9d..a9ad394d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,36 +7,46 @@ import Secrets from './components/Page/Secrets/Secrets' import { WebviewPage, PageType } from './model/webviewPages' import { EventManager } from './api/eventManager' import { eventManagerContext } from './store/eventContext' +import { Spinner, State } from './components/UI/Spinner/Spinner' +import { Login } from './components/Page/Login/Login' /** * The main page on which the Webview will be drawn based on the incoming request page type. */ function App(): JSX.Element { - const [data, setDependencyData] = useState({} as WebviewPage) - const eventManager = useMemo(() => new EventManager(setDependencyData), []) + const [pageData, setPageData] = useState({} as WebviewPage) + + const eventManager = useMemo(() => new EventManager(setPageData), []) let page - switch (data.pageType) { + switch (pageData.pageType) { case PageType.Dependency: - page = + page = break case PageType.Eos: - page = + page = break case PageType.IaC: - page = + page = break case PageType.Secrets: - page = + page = + break + case PageType.Login: + page = break default: - page = <>Nothing to show + page = ( +
+ +
+ ) } return (
-
{page}
+
{page}
) diff --git a/src/api/eventManager.test.ts b/src/api/eventManager.test.ts index 186d62da..f841d61d 100644 --- a/src/api/eventManager.test.ts +++ b/src/api/eventManager.test.ts @@ -1,6 +1,6 @@ +import { WebviewReceiveEventType } from '.' import { IPageData, sendWebviewPage } from '../utils/testUtils' import { EventManager } from './eventManager' -import { webviewEventType } from './webviewEvent' describe('EventManager util', () => { let setPageStateMock: jest.Mock @@ -16,7 +16,7 @@ describe('EventManager util', () => { test('sets the event receiver and handles ShowPage event', async () => { const pageData = { - type: webviewEventType.ShowPage, + type: WebviewReceiveEventType.ShowPage, pageData: { title: 'Test Page' } } await sendWebviewPage(pageData) @@ -29,7 +29,7 @@ describe('EventManager util', () => { const consoleLogSpy = jest.spyOn(global.console, 'warn') const emitterFunc = 'return console.warn' const setEmitterEvent = { - type: webviewEventType.SetEmitter, + type: WebviewReceiveEventType.SetEmitter, emitterFunc } as IPageData diff --git a/src/api/eventManager.ts b/src/api/eventManager.ts index 6ef7c929..fba62a03 100644 --- a/src/api/eventManager.ts +++ b/src/api/eventManager.ts @@ -1,28 +1,35 @@ +import { + WebviewReceiveEvent, + WebviewReceiveEventType, + WebviewSendEvent, + WebviewSendEventType +} from '.' import { IAnalysisStep } from '../model/analysisStep' +import { ISendLoginEventData } from '../model/login' import { WebviewPage } from '../model/webviewPages' -import { IdeEvent, IdeEventType, JumpToCodeEvent } from './ideEvent' -import { WebviewEvent, webviewEventType } from './webviewEvent' +import { SendJumpToCodeEvent } from './sendEvent/jumpToCode' +import { SendLoginEvent } from './sendEvent/login' export class EventManager { - private sendFunc = new Function('request', 'console.log(request)') + protected sendFunc = new Function('request', 'console.log(request)') constructor(private setPageState: React.Dispatch>) { this.setEventReceiver() } - private sendEvent = (req: IdeEvent): void => { + private sendEvent = (req: WebviewSendEvent): void => { this.sendFunc(req) } private setEventReceiver(): void { window.addEventListener('message', event => { - const eventData: WebviewEvent = event.data + const eventData: WebviewReceiveEvent = event.data switch (eventData.type) { - case webviewEventType.SetEmitter: + case WebviewReceiveEventType.SetEmitter: this.sendFunc = new Function(eventData.emitterFunc)() break - case webviewEventType.ShowPage: + case WebviewReceiveEventType.ShowPage: this.setPageState(eventData.pageData) break } @@ -30,6 +37,10 @@ export class EventManager { } public jumpToCode(data: IAnalysisStep): void { - this.sendEvent({ type: IdeEventType.JUMP_TO_CODE, data: data } as JumpToCodeEvent) + this.sendEvent({ type: WebviewSendEventType.JumpToCode, data: data } as SendJumpToCodeEvent) + } + + public Login(data: ISendLoginEventData): void { + this.sendEvent({ type: WebviewSendEventType.Login, data: data } as SendLoginEvent) } } diff --git a/src/api/ideEvent.test.ts b/src/api/ideEvent.test.ts deleted file mode 100644 index 233181df..00000000 --- a/src/api/ideEvent.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { IAnalysisStep } from '../model/analysisStep' -import { IdeEvent, IdeEventType, JumpToCodeEvent } from './ideEvent' - -describe('Event Types', () => { - test('defines IdeEventType enum correctly', () => { - expect(IdeEventType.JUMP_TO_CODE).toEqual('SHOW_CODE') - }) - - test('defines JumpToCodeEvent interface correctly', () => { - const eventData: JumpToCodeEvent = { - type: IdeEventType.JUMP_TO_CODE, - data: { file: 'file' } as IAnalysisStep - } - - expect(eventData.type).toEqual(IdeEventType.JUMP_TO_CODE) - expect(eventData.data).toBeDefined() - }) - - test('defines IdeEvent union type correctly', () => { - const event: IdeEvent = { - type: IdeEventType.JUMP_TO_CODE, - data: { file: 'file' } as IAnalysisStep - } - - expect(event.type).toEqual(IdeEventType.JUMP_TO_CODE) - expect(event.data).toBeDefined() - }) -}) diff --git a/src/api/ideEvent.ts b/src/api/ideEvent.ts deleted file mode 100644 index a3e12a2b..00000000 --- a/src/api/ideEvent.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IAnalysisStep } from '../model/analysisStep' - -export interface JumpToCodeEvent { - type: IdeEventType.JUMP_TO_CODE - data: IAnalysisStep -} - -export enum IdeEventType { - JUMP_TO_CODE = 'SHOW_CODE' -} - -export type IdeEvent = JumpToCodeEvent diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 00000000..31a6691b --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,20 @@ +import { SendLoginEvent } from './sendEvent/login' +import { SendJumpToCodeEvent } from './sendEvent/jumpToCode' +import { ReceiveSetEmitterEvent } from './receiveEvent/setEmitter' +import { ReceiveShowPageEvent } from './receiveEvent/showPage' + +// Send event to the IDEs. +export enum WebviewSendEventType { + JumpToCode = 'SHOW_CODE', + Login = 'LOGIN' +} + +export type WebviewSendEvent = SendJumpToCodeEvent | SendLoginEvent + +// Receive events from the IDEs. +export enum WebviewReceiveEventType { + SetEmitter = 'SET_EMITTER', + ShowPage = 'SHOW_DATA' +} + +export type WebviewReceiveEvent = ReceiveShowPageEvent | ReceiveSetEmitterEvent diff --git a/src/api/receiveEvent/index.ts b/src/api/receiveEvent/index.ts new file mode 100644 index 00000000..14f4a716 --- /dev/null +++ b/src/api/receiveEvent/index.ts @@ -0,0 +1,2 @@ +export { ReceiveSetEmitterEvent } from './setEmitter' +export { ReceiveShowPageEvent } from './showPage' diff --git a/src/api/receiveEvent/setEmitter.test.ts b/src/api/receiveEvent/setEmitter.test.ts new file mode 100644 index 00000000..15fd2aff --- /dev/null +++ b/src/api/receiveEvent/setEmitter.test.ts @@ -0,0 +1,18 @@ +import { ReceiveSetEmitterEvent } from '.' +import { WebviewReceiveEventType } from '..' + +describe('Event Types', () => { + test('defines webviewEventType enum correctly', () => { + expect(WebviewReceiveEventType.SetEmitter).toEqual('SET_EMITTER') + }) + + test('defines SetEmitterEvent interface correctly', () => { + const eventData: ReceiveSetEmitterEvent = { + type: WebviewReceiveEventType.SetEmitter, + emitterFunc: 'exampleEmitterFunc' + } + + expect(eventData.type).toEqual(WebviewReceiveEventType.SetEmitter) + expect(eventData.emitterFunc).toEqual('exampleEmitterFunc') + }) +}) diff --git a/src/api/receiveEvent/setEmitter.ts b/src/api/receiveEvent/setEmitter.ts new file mode 100644 index 00000000..37d87532 --- /dev/null +++ b/src/api/receiveEvent/setEmitter.ts @@ -0,0 +1,6 @@ +import { WebviewReceiveEventType } from '..' + +export interface ReceiveSetEmitterEvent { + type: WebviewReceiveEventType.SetEmitter + emitterFunc: string +} diff --git a/src/api/receiveEvent/showPage.test.ts b/src/api/receiveEvent/showPage.test.ts new file mode 100644 index 00000000..23df4798 --- /dev/null +++ b/src/api/receiveEvent/showPage.test.ts @@ -0,0 +1,29 @@ +import { WebviewReceiveEventType } from '..' +import { IDependencyPage } from '../../model/webviewPages' +import { ReceiveShowPageEvent } from './showPage' + +describe('Event Types', () => { + test('defines webviewEventType enum correctly', () => { + expect(WebviewReceiveEventType.SetEmitter).toEqual('SET_EMITTER') + }) + + test('defines ShowPageEvent interface correctly', () => { + const eventData: ReceiveShowPageEvent = { + type: WebviewReceiveEventType.ShowPage, + pageData: {} as IDependencyPage + } + + expect(eventData.type).toEqual(WebviewReceiveEventType.ShowPage) + expect(eventData.pageData).toBeDefined() + }) + + test('defines WebviewEvent union type correctly', () => { + const event: ReceiveShowPageEvent = { + type: WebviewReceiveEventType.ShowPage, + pageData: {} as IDependencyPage + } + + expect(event.type).toEqual(WebviewReceiveEventType.ShowPage) + expect(event.pageData).toBeDefined() + }) +}) diff --git a/src/api/receiveEvent/showPage.ts b/src/api/receiveEvent/showPage.ts new file mode 100644 index 00000000..c7dbcb1e --- /dev/null +++ b/src/api/receiveEvent/showPage.ts @@ -0,0 +1,7 @@ +import { WebviewReceiveEventType } from '..' +import { WebviewPage } from '../../model/webviewPages' + +export interface ReceiveShowPageEvent { + type: WebviewReceiveEventType.ShowPage + pageData: WebviewPage +} diff --git a/src/api/sendEvent/index.ts b/src/api/sendEvent/index.ts new file mode 100644 index 00000000..88422b6a --- /dev/null +++ b/src/api/sendEvent/index.ts @@ -0,0 +1,2 @@ +export { SendJumpToCodeEvent } from './jumpToCode' +export { SendLoginEvent } from './login' diff --git a/src/api/sendEvent/jumpToCode.test.ts b/src/api/sendEvent/jumpToCode.test.ts new file mode 100644 index 00000000..b99cd2b6 --- /dev/null +++ b/src/api/sendEvent/jumpToCode.test.ts @@ -0,0 +1,19 @@ +import { WebviewSendEventType } from '..' +import { IAnalysisStep } from '../../model/analysisStep' +import { SendJumpToCodeEvent } from './jumpToCode' + +describe('Event Types', () => { + test('defines IdeEventType enum correctly', () => { + expect(WebviewSendEventType.JumpToCode).toEqual('SHOW_CODE') + }) + + test('defines JumpToCodeEvent interface correctly', () => { + const eventData: SendJumpToCodeEvent = { + type: WebviewSendEventType.JumpToCode, + data: { file: 'file' } as IAnalysisStep + } + + expect(eventData.type).toEqual(WebviewSendEventType.JumpToCode) + expect(eventData.data).toBeDefined() + }) +}) diff --git a/src/api/sendEvent/jumpToCode.ts b/src/api/sendEvent/jumpToCode.ts new file mode 100644 index 00000000..8283ed2d --- /dev/null +++ b/src/api/sendEvent/jumpToCode.ts @@ -0,0 +1,7 @@ +import { WebviewSendEventType } from '..' +import { IAnalysisStep } from '../../model/analysisStep' + +export interface SendJumpToCodeEvent { + type: WebviewSendEventType.JumpToCode + data: IAnalysisStep +} diff --git a/src/api/sendEvent/login.ts b/src/api/sendEvent/login.ts new file mode 100644 index 00000000..e4db6ac9 --- /dev/null +++ b/src/api/sendEvent/login.ts @@ -0,0 +1,7 @@ +import { WebviewSendEventType } from '..' +import { ISendLoginEventData } from '../../model/login' + +export interface SendLoginEvent { + type: WebviewSendEventType.Login + data: ISendLoginEventData +} diff --git a/src/api/webviewEvent.test.ts b/src/api/webviewEvent.test.ts deleted file mode 100644 index cdac1236..00000000 --- a/src/api/webviewEvent.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { IDependencyPage } from '../model/webviewPages' -import { webviewEventType, ShowPageEvent, SetEmitterEvent, WebviewEvent } from './webviewEvent' - -describe('Event Types', () => { - test('defines webviewEventType enum correctly', () => { - expect(webviewEventType.SetEmitter).toEqual('SET_EMITTER') - expect(webviewEventType.ShowPage).toEqual('SHOW_DATA') - }) - - test('defines ShowPageEvent interface correctly', () => { - const eventData: ShowPageEvent = { - type: webviewEventType.ShowPage, - pageData: {} as IDependencyPage - } - - expect(eventData.type).toEqual(webviewEventType.ShowPage) - expect(eventData.pageData).toBeDefined() - }) - - test('defines SetEmitterEvent interface correctly', () => { - const eventData: SetEmitterEvent = { - type: webviewEventType.SetEmitter, - emitterFunc: 'exampleEmitterFunc' - } - - expect(eventData.type).toEqual(webviewEventType.SetEmitter) - expect(eventData.emitterFunc).toEqual('exampleEmitterFunc') - }) - - test('defines WebviewEvent union type correctly', () => { - const event: WebviewEvent = { - type: webviewEventType.ShowPage, - pageData: {} as IDependencyPage - } - - expect(event.type).toEqual(webviewEventType.ShowPage) - expect(event.pageData).toBeDefined() - }) -}) diff --git a/src/api/webviewEvent.ts b/src/api/webviewEvent.ts deleted file mode 100644 index b9fa5234..00000000 --- a/src/api/webviewEvent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { WebviewPage } from '../model/webviewPages' - -export interface ShowPageEvent { - type: webviewEventType.ShowPage - pageData: WebviewPage -} - -export interface SetEmitterEvent { - type: webviewEventType.SetEmitter - emitterFunc: string -} - -export enum webviewEventType { - SetEmitter = 'SET_EMITTER', - ShowPage = 'SHOW_DATA' -} - -export type WebviewEvent = ShowPageEvent | SetEmitterEvent diff --git a/src/components/Page/Eos/Research.tsx b/src/components/Page/Eos/Research.tsx index 43ca9187..308f0937 100644 --- a/src/components/Page/Eos/Research.tsx +++ b/src/components/Page/Eos/Research.tsx @@ -12,7 +12,7 @@ export default function Research(props: Props): JSX.Element { <> {props.description && props.description.length > 0 && ( - + )} {props.remediation && props.remediation.length > 0 && ( diff --git a/src/components/Page/Login/AccessToken.test.tsx b/src/components/Page/Login/AccessToken.test.tsx new file mode 100644 index 00000000..056ce693 --- /dev/null +++ b/src/components/Page/Login/AccessToken.test.tsx @@ -0,0 +1,43 @@ +import { render, fireEvent } from '@testing-library/react' +import { AccessToken, Props } from './AccessToken' + +describe('AccessToken component', () => { + const mockHandleAccessToken = jest.fn() + const mockHandlePasswordSwitch = jest.fn() + + const defaultProps: Props = { + handleAccessToken: mockHandleAccessToken, + handlePasswordSwitch: mockHandlePasswordSwitch, + inputError: false + } + + test('renders without errors', () => { + render() + }) + + test('calls handleAccessToken on input change', () => { + const { getByLabelText } = render() + const inputElement = getByLabelText('Access Token') + + fireEvent.change(inputElement, { target: { value: 'example-token' } }) + + expect(mockHandleAccessToken).toHaveBeenCalledTimes(1) + expect(mockHandleAccessToken).toHaveBeenCalledWith(expect.any(Object)) + }) + + test('calls handlePasswordSwitch on button click', () => { + const { getByText } = render() + const buttonElement = getByText('Have Password?') + + fireEvent.click(buttonElement) + + expect(mockHandlePasswordSwitch).toHaveBeenCalledTimes(1) + }) + + test('applies inputError class when inputError prop is true', () => { + const { container } = render() + const inputElement = container.querySelector('input') + + expect(inputElement).toHaveClass('inputError') + }) +}) diff --git a/src/components/Page/Login/AccessToken.tsx b/src/components/Page/Login/AccessToken.tsx new file mode 100644 index 00000000..63c9259a --- /dev/null +++ b/src/components/Page/Login/AccessToken.tsx @@ -0,0 +1,29 @@ +import css from './Form.module.css' +import { ChangeEvent } from 'react' + +export interface Props { + handleAccessToken: (event: ChangeEvent) => void + handlePasswordSwitch: () => void + inputError: boolean +} + +export function AccessToken(props: Props): JSX.Element { + return ( +
+
+ + +
+ +
+ ) +} diff --git a/src/components/Page/Login/Footer.test.tsx b/src/components/Page/Login/Footer.test.tsx new file mode 100644 index 00000000..f94da5d2 --- /dev/null +++ b/src/components/Page/Login/Footer.test.tsx @@ -0,0 +1,57 @@ +import { render, fireEvent } from '@testing-library/react' +import { Footer, Props } from './Footer' +import { LoginConnectionType } from '../../../model/login' + +describe('Footer component', () => { + const mockHandleConnectionType = jest.fn() + const mockHandleSignIn = jest.fn() + + const defaultProps: Props = { + handleConnectionType: mockHandleConnectionType, + handleSighIn: mockHandleSignIn, + type: LoginConnectionType.BasicAuthOrToken + } + + test('renders the default footer when type is not SSO', () => { + const { getByText } = render(