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 (
)
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()
+
+ expect(getByText('Sign in')).toBeInTheDocument()
+ expect(getByText('Continue With SSO')).toBeInTheDocument()
+ })
+
+ test('renders the SSO footer when type is SSO', () => {
+ const { getByText } = render()
+
+ expect(getByText('Sign in with SSO')).toBeInTheDocument()
+ expect(getByText('Use Basic-Auth')).toBeInTheDocument()
+ })
+
+ test('calls handleSighIn when "Sign in" button is clicked', () => {
+ const { getByText } = render()
+ const signInButton = getByText('Sign in')
+
+ fireEvent.click(signInButton)
+
+ expect(mockHandleSignIn).toHaveBeenCalledTimes(1)
+ })
+
+ test('calls handleConnectionType with LoginConnectionType.Default when "Use Basic-Auth" button is clicked', () => {
+ const { getByText } = render()
+ const basicAuthButton = getByText('Use Basic-Auth')
+
+ fireEvent.click(basicAuthButton)
+
+ expect(mockHandleConnectionType).toHaveBeenCalledTimes(1)
+ expect(mockHandleConnectionType).toHaveBeenCalledWith(LoginConnectionType.BasicAuthOrToken)
+ })
+
+ test('calls handleConnectionType with LoginConnectionType.Sso when "Continue With SSO" button is clicked', () => {
+ const { getByText } = render()
+ const ssoButton = getByText('Continue With SSO')
+
+ fireEvent.click(ssoButton)
+
+ expect(mockHandleConnectionType).toHaveBeenCalledTimes(1)
+ expect(mockHandleConnectionType).toHaveBeenCalledWith(LoginConnectionType.Sso)
+ })
+})
diff --git a/src/components/Page/Login/Footer.tsx b/src/components/Page/Login/Footer.tsx
new file mode 100644
index 00000000..1f9c385f
--- /dev/null
+++ b/src/components/Page/Login/Footer.tsx
@@ -0,0 +1,63 @@
+import css from './Form.module.css'
+import { LoginConnectionType } from '../../../model/login'
+import { SsoIcon } from '../../UI/Icons/SsoIcon'
+
+export interface Props {
+ handleConnectionType: (type: LoginConnectionType) => void
+ handleSighIn: () => void
+ type: LoginConnectionType
+}
+
+export function Footer(props: Props): JSX.Element {
+ return isSso(props.type) ? :
+}
+
+export interface HandlersProps {
+ handleConnectionType: (type: LoginConnectionType) => void
+ handleSighIn: () => void
+}
+
+function SsoFooter(props: HandlersProps): JSX.Element {
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
+
+function DefaultFooter(props: HandlersProps): JSX.Element {
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
+
+function isSso(type: LoginConnectionType): boolean {
+ return type === LoginConnectionType.Sso
+}
diff --git a/src/components/Page/Login/Form.module.css b/src/components/Page/Login/Form.module.css
new file mode 100644
index 00000000..4338a13c
--- /dev/null
+++ b/src/components/Page/Login/Form.module.css
@@ -0,0 +1,534 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 20px;
+ margin-top: 10px;
+ margin-bottom: 0px;
+ width: 320px;
+ transition: height 0.5s ease;
+}
+
+.containerAccessToken {
+ height: 255px;
+}
+
+.containerAdvancedUrl {
+ height: 434px;
+}
+
+.containerAdvancedUrlWithPass {
+ height: 484px;
+}
+
+.containerPassword {
+ height: 334px;
+}
+
+.containerSso {
+ height: 255px
+}
+
+.containerAdvancedUrlWitSso {
+ height: 419px;
+}
+
+.advanceUrlArrowOpen {
+ transition: all 1.5s ease;
+ transform: rotate(180deg);
+ margin-left: 5px;
+}
+
+.advanceUrlArrowClose {
+ transition: all 0.9s ease;
+}
+
+.inputHeader {
+ text-transform: uppercase;
+ letter-spacing: 0.02em;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.switchBtn {
+ color: rgb(0, 168, 252);
+ cursor: pointer;
+ font-size: x-small;
+ background: #0000;
+ border: none;
+}
+
+.ssoBtn {
+ color: rgb(0, 168, 252);
+ cursor: pointer;
+ font-size: x-small;
+ display: flex;
+ flex-flow: column;
+ margin-top: 10px;
+ align-items: center;
+ background: #0000;
+ border: none;
+}
+
+.btnContainer {
+ width: 315px;
+ justify-content: center;
+ display: flex;
+}
+
+.passBox {
+ display: flex;
+ justify-content: space-between;
+}
+
+.input {
+ width: 240px;
+ height: 19px;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ padding: 6px 11px;
+ gap: 10px;
+ background: #3c3c3c;
+ color: rgb(242, 243, 245);
+ border: none;
+ border: 1px solid transparent;
+ animation: slideIn 0.5s forwards;
+ /* Animation duration and name */
+ opacity: 0;
+ /* Initially hide the input */
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateY(-100%);
+ /* Start position offscreen to the left */
+ opacity: 0;
+ /* Initially transparent */
+ }
+
+ to {
+ transform: translateX(0);
+ /* End position, slide in from left */
+ opacity: 1;
+ /* Fully visible */
+ }
+}
+
+.inputError {
+ border: 1px solid #eb2828dd;
+}
+
+.credContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.paragraph {
+ color: rgb(165, 175, 187);
+ font-size: 13px;
+ width: 254px;
+ font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
+
+.redStar {
+ color: red;
+ margin-left: 5px;
+}
+
+.webLoginBtn {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ padding: 6px 11px;
+ min-width: 196px;
+ min-height: 35px;
+ color: #ffffff;
+ background: #7558ea;
+ border-radius: 30px;
+ border: none;
+ position: relative;
+ overflow: hidden;
+ cursor: pointer;
+ transition: all 0.1s ease;
+ margin-top: 10px;
+}
+
+.webLoginBtn:hover {
+ -webkit-animation: hover 1200ms linear 2 alternate;
+ animation: hover 1200ms linear 2 alternate;
+}
+
+.webLoginBtn:active {
+ -webkit-animation: active 1200ms ease 1 alternate;
+ animation: active 1200ms ease 1 alternate;
+ background: #7558ea;
+}
+
+@-webkit-keyframes hover {
+ 0% {
+ -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 1.8% {
+ -webkit-transform: matrix3d(1.016, 0, 0, 0, 0, 1.037, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.016, 0, 0, 0, 0, 1.037, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 3.5% {
+ -webkit-transform: matrix3d(1.033, 0, 0, 0, 0, 1.094, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.033, 0, 0, 0, 0, 1.094, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 4.7% {
+ -webkit-transform: matrix3d(1.045, 0, 0, 0, 0, 1.129, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.045, 0, 0, 0, 0, 1.129, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 5.31% {
+ -webkit-transform: matrix3d(1.051, 0, 0, 0, 0, 1.142, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.051, 0, 0, 0, 0, 1.142, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 7.01% {
+ -webkit-transform: matrix3d(1.068, 0, 0, 0, 0, 1.158, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.068, 0, 0, 0, 0, 1.158, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 8.91% {
+ -webkit-transform: matrix3d(1.084, 0, 0, 0, 0, 1.141, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.084, 0, 0, 0, 0, 1.141, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 9.41% {
+ -webkit-transform: matrix3d(1.088, 0, 0, 0, 0, 1.132, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.088, 0, 0, 0, 0, 1.132, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 10.71% {
+ -webkit-transform: matrix3d(1.097, 0, 0, 0, 0, 1.107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.097, 0, 0, 0, 0, 1.107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 12.61% {
+ -webkit-transform: matrix3d(1.108, 0, 0, 0, 0, 1.077, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.108, 0, 0, 0, 0, 1.077, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 14.11% {
+ -webkit-transform: matrix3d(1.114, 0, 0, 0, 0, 1.067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.114, 0, 0, 0, 0, 1.067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 14.41% {
+ -webkit-transform: matrix3d(1.115, 0, 0, 0, 0, 1.067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.115, 0, 0, 0, 0, 1.067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 16.32% {
+ -webkit-transform: matrix3d(1.119, 0, 0, 0, 0, 1.077, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.119, 0, 0, 0, 0, 1.077, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 18.12% {
+ -webkit-transform: matrix3d(1.121, 0, 0, 0, 0, 1.096, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.121, 0, 0, 0, 0, 1.096, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 18.72% {
+ -webkit-transform: matrix3d(1.121, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.121, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 20.02% {
+ -webkit-transform: matrix3d(1.121, 0, 0, 0, 0, 1.113, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.121, 0, 0, 0, 0, 1.113, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 21.82% {
+ -webkit-transform: matrix3d(1.119, 0, 0, 0, 0, 1.119, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.119, 0, 0, 0, 0, 1.119, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 24.32% {
+ -webkit-transform: matrix3d(1.115, 0, 0, 0, 0, 1.11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.115, 0, 0, 0, 0, 1.11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 25.53% {
+ -webkit-transform: matrix3d(1.113, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.113, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 29.23% {
+ -webkit-transform: matrix3d(1.106, 0, 0, 0, 0, 1.089, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.106, 0, 0, 0, 0, 1.089, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 29.93% {
+ -webkit-transform: matrix3d(1.105, 0, 0, 0, 0, 1.09, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.105, 0, 0, 0, 0, 1.09, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 35.54% {
+ -webkit-transform: matrix3d(1.098, 0, 0, 0, 0, 1.105, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.098, 0, 0, 0, 0, 1.105, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 36.64% {
+ -webkit-transform: matrix3d(1.097, 0, 0, 0, 0, 1.106, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.097, 0, 0, 0, 0, 1.106, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 41.04% {
+ -webkit-transform: matrix3d(1.096, 0, 0, 0, 0, 1.099, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.096, 0, 0, 0, 0, 1.099, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 44.04% {
+ -webkit-transform: matrix3d(1.096, 0, 0, 0, 0, 1.097, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.096, 0, 0, 0, 0, 1.097, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 51.45% {
+ -webkit-transform: matrix3d(1.099, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.099, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 52.15% {
+ -webkit-transform: matrix3d(1.099, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.099, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 58.86% {
+ -webkit-transform: matrix3d(1.101, 0, 0, 0, 0, 1.099, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.101, 0, 0, 0, 0, 1.099, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 63.26% {
+ -webkit-transform: matrix3d(1.101, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.101, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 66.27% {
+ -webkit-transform: matrix3d(1.101, 0, 0, 0, 0, 1.101, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.101, 0, 0, 0, 0, 1.101, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 73.77% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 81.18% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 85.49% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 88.59% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 96% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 100% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+}
+
+@keyframes hover {
+ 0% {
+ -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 1.8% {
+ -webkit-transform: matrix3d(1.016, 0, 0, 0, 0, 1.037, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.016, 0, 0, 0, 0, 1.037, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 3.5% {
+ -webkit-transform: matrix3d(1.033, 0, 0, 0, 0, 1.094, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.033, 0, 0, 0, 0, 1.094, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 4.7% {
+ -webkit-transform: matrix3d(1.045, 0, 0, 0, 0, 1.129, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.045, 0, 0, 0, 0, 1.129, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 5.31% {
+ -webkit-transform: matrix3d(1.051, 0, 0, 0, 0, 1.142, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.051, 0, 0, 0, 0, 1.142, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 7.01% {
+ -webkit-transform: matrix3d(1.068, 0, 0, 0, 0, 1.158, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.068, 0, 0, 0, 0, 1.158, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 8.91% {
+ -webkit-transform: matrix3d(1.084, 0, 0, 0, 0, 1.141, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.084, 0, 0, 0, 0, 1.141, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 9.41% {
+ -webkit-transform: matrix3d(1.088, 0, 0, 0, 0, 1.132, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.088, 0, 0, 0, 0, 1.132, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 10.71% {
+ -webkit-transform: matrix3d(1.097, 0, 0, 0, 0, 1.107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.097, 0, 0, 0, 0, 1.107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 12.61% {
+ -webkit-transform: matrix3d(1.108, 0, 0, 0, 0, 1.077, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.108, 0, 0, 0, 0, 1.077, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 14.11% {
+ -webkit-transform: matrix3d(1.114, 0, 0, 0, 0, 1.067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.114, 0, 0, 0, 0, 1.067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 14.41% {
+ -webkit-transform: matrix3d(1.115, 0, 0, 0, 0, 1.067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.115, 0, 0, 0, 0, 1.067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 16.32% {
+ -webkit-transform: matrix3d(1.119, 0, 0, 0, 0, 1.077, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.119, 0, 0, 0, 0, 1.077, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 18.12% {
+ -webkit-transform: matrix3d(1.121, 0, 0, 0, 0, 1.096, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.121, 0, 0, 0, 0, 1.096, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 18.72% {
+ -webkit-transform: matrix3d(1.121, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.121, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 20.02% {
+ -webkit-transform: matrix3d(1.121, 0, 0, 0, 0, 1.113, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.121, 0, 0, 0, 0, 1.113, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 21.82% {
+ -webkit-transform: matrix3d(1.119, 0, 0, 0, 0, 1.119, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.119, 0, 0, 0, 0, 1.119, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 24.32% {
+ -webkit-transform: matrix3d(1.115, 0, 0, 0, 0, 1.11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.115, 0, 0, 0, 0, 1.11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 25.53% {
+ -webkit-transform: matrix3d(1.113, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.113, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 29.23% {
+ -webkit-transform: matrix3d(1.106, 0, 0, 0, 0, 1.089, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.106, 0, 0, 0, 0, 1.089, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 29.93% {
+ -webkit-transform: matrix3d(1.105, 0, 0, 0, 0, 1.09, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.105, 0, 0, 0, 0, 1.09, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 35.54% {
+ -webkit-transform: matrix3d(1.098, 0, 0, 0, 0, 1.105, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.098, 0, 0, 0, 0, 1.105, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 36.64% {
+ -webkit-transform: matrix3d(1.097, 0, 0, 0, 0, 1.106, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.097, 0, 0, 0, 0, 1.106, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 41.04% {
+ -webkit-transform: matrix3d(1.096, 0, 0, 0, 0, 1.099, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.096, 0, 0, 0, 0, 1.099, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 44.04% {
+ -webkit-transform: matrix3d(1.096, 0, 0, 0, 0, 1.097, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.096, 0, 0, 0, 0, 1.097, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 51.45% {
+ -webkit-transform: matrix3d(1.099, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.099, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 52.15% {
+ -webkit-transform: matrix3d(1.099, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.099, 0, 0, 0, 0, 1.102, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 58.86% {
+ -webkit-transform: matrix3d(1.101, 0, 0, 0, 0, 1.099, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.101, 0, 0, 0, 0, 1.099, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 63.26% {
+ -webkit-transform: matrix3d(1.101, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.101, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 66.27% {
+ -webkit-transform: matrix3d(1.101, 0, 0, 0, 0, 1.101, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.101, 0, 0, 0, 0, 1.101, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 73.77% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 81.18% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 85.49% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 88.59% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 96% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+
+ 100% {
+ -webkit-transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ transform: matrix3d(1.1, 0, 0, 0, 0, 1.1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+ }
+}
\ No newline at end of file
diff --git a/src/components/Page/Login/Form.test.tsx b/src/components/Page/Login/Form.test.tsx
new file mode 100644
index 00000000..2da7fa20
--- /dev/null
+++ b/src/components/Page/Login/Form.test.tsx
@@ -0,0 +1,149 @@
+import { render, fireEvent } from '@testing-library/react'
+import { Form } from './Form'
+import { eventManagerContext } from '../../../store/eventContext'
+import { TestEventManager } from '../../../utils/testUtils'
+
+describe('Form component', () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+
+ test('renders the form with the default access token input', () => {
+ const { getByLabelText, getByText } = render(
+
+
+
+ )
+ const accessTokenInput = getByLabelText('Access Token')
+ const signInButton = getByText('Sign in')
+ expect(accessTokenInput).toBeInTheDocument()
+ expect(signInButton).toBeInTheDocument()
+ })
+
+ test('renders the form with Advanced url button', () => {
+ const { getByText } = render(
+
+
+
+ )
+ const signInButton = getByText('Advanced')
+ expect(signInButton).toBeInTheDocument()
+ })
+
+ test('renders the form with Artifactory & Xray urls on click at Advanced url button', () => {
+ const { getByLabelText, getByText } = render(
+
+
+
+ )
+ fireEvent.click(getByText('Advanced'))
+
+ const artifactoryUrlInput = getByLabelText('Artifactory URL') as HTMLInputElement
+ const xrayUrlInput = getByLabelText('Xray URL') as HTMLInputElement
+ fireEvent.change(artifactoryUrlInput, { target: { value: 'rtUrl' } })
+ fireEvent.change(xrayUrlInput, { target: { value: 'xrayUrl' } })
+ expect(artifactoryUrlInput.value).toBe('rtUrl')
+ expect(xrayUrlInput.value).toBe('xrayUrl')
+ })
+
+ test('calls handleAccessTokenSwitch when "Have Password?" button is clicked', () => {
+ const { getByText } = render(
+
+
+
+ )
+ const switchButton = getByText('Have Password?')
+ expect(switchButton).toBeInTheDocument()
+ })
+
+ test('renders the form with the default password input', () => {
+ const { getByText } = render(
+
+
+
+ )
+ const passwordButton = getByText('Have Password?')
+ fireEvent.click(passwordButton)
+ expect(getByText('Username')).toBeInTheDocument()
+ expect(getByText('Password')).toBeInTheDocument()
+ expect(getByText('Sign in')).toBeInTheDocument()
+ })
+
+ test('renders the form with the access token input after "Have Access Token" have been clicked', () => {
+ const { getByText } = render(
+
+
+
+ )
+ let button = getByText('Have Password?')
+ fireEvent.click(button)
+ expect(getByText('Username')).toBeInTheDocument()
+ expect(getByText('Password')).toBeInTheDocument()
+ button = getByText('Have Access-Token?')
+ fireEvent.click(button)
+ expect(getByText('Access Token')).toBeInTheDocument()
+ })
+
+ test('calls accessTokenHandler when access token inputs change', () => {
+ const { getByLabelText } = render(
+
+
+
+ )
+ const accessTokenInput = getByLabelText('Access Token') as HTMLInputElement
+ fireEvent.change(accessTokenInput, { target: { value: 'accessToken!' } })
+ expect(accessTokenInput.value).toBe('accessToken!')
+ })
+
+ test('calls handleUsername and handlePassword when username and password inputs change', () => {
+ const { getByText, getByLabelText } = render(
+
+
+
+ )
+ const passwordButton = getByText('Have Password?')
+ fireEvent.click(passwordButton)
+ const usernameInput = getByLabelText('Username') as HTMLInputElement
+ const passwordInput = getByLabelText('Password') as HTMLInputElement
+ fireEvent.change(usernameInput, { target: { value: 'testUser' } })
+ fireEvent.change(passwordInput, { target: { value: 'testPassword' } })
+ expect(usernameInput.value).toBe('testUser')
+ expect(passwordInput.value).toBe('testPassword')
+ })
+
+ test('no calls Login when "Sign in" button is clicked with not valid login data', () => {
+ const { getByText } = render(
+
+
+
+ )
+ const signInButton = getByText('Sign in')
+ fireEvent.click(signInButton)
+ expect(mockLogin).toHaveBeenCalledTimes(0)
+ })
+
+ test('calls Login when "Sign in" button is clicked with valid login data', () => {
+ const { getByText, getByLabelText } = render(
+
+
+
+ )
+ const accessTokenInput = getByLabelText('Access Token') as HTMLInputElement
+ fireEvent.change(accessTokenInput, { target: { value: 'accessToken!' } })
+ const urlInput = getByLabelText('Platform URL') as HTMLInputElement
+ fireEvent.change(urlInput, { target: { value: 'url!' } })
+ const signInButton = getByText('Sign in')
+ fireEvent.click(signInButton)
+ expect(mockLogin).toHaveBeenCalledTimes(1)
+ })
+
+ test('renders the form with the SSO component when connection type is SSO', () => {
+ const { getByText } = render(
+
+
+
+ )
+ const ssoButton = getByText('Continue With SSO')
+ fireEvent.click(ssoButton)
+ expect(getByText('Requires Artifactory version 7.57 or higher')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/Page/Login/Form.tsx b/src/components/Page/Login/Form.tsx
new file mode 100644
index 00000000..6ada640d
--- /dev/null
+++ b/src/components/Page/Login/Form.tsx
@@ -0,0 +1,130 @@
+import { eventManagerContext } from '../../../store/eventContext'
+import css from './Form.module.css'
+import { ChangeEvent, useState, useContext } from 'react'
+import { ISendLoginEventData, LoginConnectionType } from '../../../model/login'
+import { Password } from './Password'
+import { AccessToken } from './AccessToken'
+import { Sso } from './Sso'
+import { Footer } from './Footer'
+import { Url } from './Url'
+
+export function Form(): JSX.Element {
+ const ctx = useContext(eventManagerContext)
+ const [showPassword, setShowPassword] = useState(false)
+ const [advancedUrl, setAdvancedUrl] = useState(false)
+ const [loginData, setLoginData] = useState({
+ url: '',
+ loginConnectionType: LoginConnectionType.BasicAuthOrToken,
+ username: '',
+ password: '',
+ accessToken: ''
+ })
+ const [inputError, setInputError] = useState(false)
+
+ const inputChangeHandler =
+ (field: keyof ISendLoginEventData) =>
+ (event: ChangeEvent): void => {
+ setLoginData(prevState => ({
+ ...prevState,
+ [field]: event.target.value
+ }))
+ setInputError(false)
+ }
+
+ const urlHandler = inputChangeHandler('url')
+ const artifactoryUrlHandler = inputChangeHandler('artifactoryUrl')
+ const xrayUrlHandler = inputChangeHandler('xrayUrl')
+ const usernameHandler = inputChangeHandler('username')
+ const passwordHandler = inputChangeHandler('password')
+ const accessTokenHandler = inputChangeHandler('accessToken')
+
+ const connectionTypeHandler = (loginConnectionType: LoginConnectionType): void => {
+ setLoginData((prev: ISendLoginEventData) => ({
+ ...prev,
+ loginConnectionType: loginConnectionType
+ }))
+ }
+
+ const handleSighIn = (): void => {
+ if (!isLoginDataValid(loginData)) {
+ setInputError(true)
+ } else {
+ ctx.Login(loginData)
+ }
+ }
+
+ const onAdvancedUrl = (): void => {
+ setLoginData(prev => ({ ...prev, xrayUrl: '', artifactoryUrl: '' }))
+ setAdvancedUrl(prev => !prev)
+ }
+
+ const switchToPassword = (): void => {
+ setShowPassword(true)
+ setLoginData(prev => ({ ...prev, accessToken: '' }))
+ }
+
+ const switchToAccessToken = (): void => {
+ setShowPassword(false)
+ setLoginData(prev => ({ ...prev, password: '' }))
+ }
+
+ let form = (
+
+ )
+ let containerCss = css.containerPassword
+
+ switch (loginData.loginConnectionType) {
+ case LoginConnectionType.BasicAuthOrToken:
+ if (!showPassword) {
+ form = (
+
+ )
+ containerCss = advancedUrl ? css.containerAdvancedUrl : css.containerAccessToken
+ } else {
+ containerCss = advancedUrl ? css.containerAdvancedUrlWithPass : css.containerPassword
+ }
+
+ break
+ case LoginConnectionType.Sso:
+ form =
+ containerCss = advancedUrl ? css.containerAdvancedUrlWitSso : css.containerSso
+ }
+
+ function isLoginDataValid(loginData: ISendLoginEventData): boolean {
+ const { url, accessToken, username, password } = loginData
+ return (
+ url?.trim() !== '' &&
+ (accessToken?.trim() !== '' ||
+ (username?.trim() !== '' && password?.trim() !== '') ||
+ loginData.loginConnectionType === LoginConnectionType.Sso)
+ )
+ }
+
+ return (
+
+
+ {form}
+
+
+ )
+}
diff --git a/src/components/Page/Login/Header.module.css b/src/components/Page/Login/Header.module.css
new file mode 100644
index 00000000..b65d2f45
--- /dev/null
+++ b/src/components/Page/Login/Header.module.css
@@ -0,0 +1,9 @@
+.welcome {
+ color: rgb(242, 243, 245);
+ font-size: 24px;
+ font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
+
+.text {
+ color: rgba(255, 255, 255, 0.8);
+}
diff --git a/src/components/Page/Login/Header.test.tsx b/src/components/Page/Login/Header.test.tsx
new file mode 100644
index 00000000..9983d3c9
--- /dev/null
+++ b/src/components/Page/Login/Header.test.tsx
@@ -0,0 +1,18 @@
+import { render } from '@testing-library/react'
+import { Header } from './Header'
+
+describe('Header component', () => {
+ it('renders the header with the JFrogIcon and welcome text', () => {
+ const { getByText, getByTestId } = render()
+ const jfrogIcon = getByTestId('jfrog-icon')
+ const welcomeText = getByText('Welcome to JFrog')
+ expect(jfrogIcon).toBeInTheDocument()
+ expect(welcomeText).toBeInTheDocument()
+ })
+
+ it('renders the header with the text "We\'re excited to see you!"', () => {
+ const { getByText } = render()
+ const excitedText = getByText("We're excited to see you!")
+ expect(excitedText).toBeInTheDocument()
+ })
+})
diff --git a/src/components/Page/Login/Header.tsx b/src/components/Page/Login/Header.tsx
new file mode 100644
index 00000000..ef41b146
--- /dev/null
+++ b/src/components/Page/Login/Header.tsx
@@ -0,0 +1,12 @@
+import { JfrogIcon } from '../../UI/Icons/JfrogIcon'
+import css from './Header.module.css'
+
+export function Header(): JSX.Element {
+ return (
+ <>
+
+ Welcome to JFrog
+ We're excited to see you!
+ >
+ )
+}
diff --git a/src/components/Page/Login/Login.module.css b/src/components/Page/Login/Login.module.css
new file mode 100644
index 00000000..44159c99
--- /dev/null
+++ b/src/components/Page/Login/Login.module.css
@@ -0,0 +1,31 @@
+.container {
+ overflow: hidden;
+}
+
+.header {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ gap: 10px;
+ min-height: 150px;
+ min-width: 250px;
+}
+
+.form {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ min-height: 200px;
+}
+
+.lineText {
+ position: absolute;
+ left: 48%;
+ top: -8px;
+ text-transform: uppercase;
+ letter-spacing: 0.02em;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.8);
+}
diff --git a/src/components/Page/Login/Login.test.tsx b/src/components/Page/Login/Login.test.tsx
new file mode 100644
index 00000000..c7df1ddd
--- /dev/null
+++ b/src/components/Page/Login/Login.test.tsx
@@ -0,0 +1,49 @@
+import { render, screen } from '@testing-library/react'
+import { Login } from './Login'
+import { ILoginPage, PageType } from '../../../model/webviewPages'
+import { LoginConnectionType, LoginProgressStatus } from '../../../model/login'
+
+let overlayElement: HTMLElement | null
+
+describe('Login component', () => {
+ beforeEach(() => {
+ overlayElement = document.createElement('div')
+ overlayElement.setAttribute('id', 'overlay')
+ document.body.appendChild(overlayElement)
+ })
+
+ afterEach(() => {
+ overlayElement?.remove()
+ overlayElement = null
+ jest.restoreAllMocks()
+ })
+
+ const mockData: ILoginPage = {
+ pageType: PageType.Login,
+ url: 'https://example.com',
+ status: LoginProgressStatus.Initial,
+ connectionType: LoginConnectionType.BasicAuthOrToken
+ }
+
+ test('renders Login component', () => {
+ render()
+ screen.getByTestId('jfrog-icon')
+ })
+
+ test('does not display LoginModal when status is initial', () => {
+ render()
+
+ const loginModalElement = screen.queryByTestId('Verifying...')
+ expect(loginModalElement).not.toBeInTheDocument()
+ })
+
+ test('displays LoginModal when status is not initial', () => {
+ const updatedMockData: ILoginPage = {
+ ...mockData,
+ status: LoginProgressStatus.Verifying
+ }
+ render()
+ const loginModalElement = screen.getByText('Verifying...')
+ expect(loginModalElement).toBeInTheDocument()
+ })
+})
diff --git a/src/components/Page/Login/Login.tsx b/src/components/Page/Login/Login.tsx
new file mode 100644
index 00000000..22006908
--- /dev/null
+++ b/src/components/Page/Login/Login.tsx
@@ -0,0 +1,40 @@
+import { LoginProgressStatus } from '../../../model/login'
+import { ILoginPage } from '../../../model/webviewPages'
+import Wrapper from '../../UI/Wrapper/Wrapper'
+import { Form } from './Form'
+import { Header } from './Header'
+import css from './Login.module.css'
+import { useState, useEffect } from 'react'
+import { LoginModal } from '../../UI/Modal/pages/LoginModal'
+
+export interface Props {
+ data: ILoginPage
+}
+
+export function Login(props: Props): JSX.Element {
+ const [modal, setModal] = useState(props.data.status !== LoginProgressStatus.Initial)
+
+ useEffect(() => {
+ setModal(props.data.status !== LoginProgressStatus.Initial)
+ }, [props.data.status])
+
+ const closeModal = (): void => {
+ setModal(false)
+ }
+
+ return (
+
+ {modal &&
}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/Page/Login/Password.test.tsx b/src/components/Page/Login/Password.test.tsx
new file mode 100644
index 00000000..73acaaf7
--- /dev/null
+++ b/src/components/Page/Login/Password.test.tsx
@@ -0,0 +1,53 @@
+import { render, screen, fireEvent } from '@testing-library/react'
+import { Password, Props } from './Password'
+
+describe('Password component', () => {
+ const mockProps: Props = {
+ handleUsername: jest.fn(),
+ handlePassword: jest.fn(),
+ handleAccessTokenSwitch: jest.fn(),
+ inputError: false
+ }
+
+ beforeEach(() => {
+ render()
+ })
+
+ test('renders the Password component', () => {
+ expect(screen.getByLabelText('Username')).toBeInTheDocument()
+ expect(screen.getByLabelText('Password')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: 'Have Access-Token?' })).toBeInTheDocument()
+ })
+
+ test('calls the handleUsername function when username input changes', () => {
+ const usernameInput = screen.getByLabelText('Username')
+ const event = {
+ target: { value: 'testUsername' }
+ } as React.ChangeEvent
+ fireEvent.change(usernameInput, event)
+ expect(mockProps.handleUsername).toHaveBeenCalledWith(
+ expect.objectContaining({
+ target: expect.objectContaining({ value: 'testUsername' })
+ })
+ )
+ })
+
+ test('calls the handlePassword function when password input changes', () => {
+ const passwordInput = screen.getByLabelText('Password')
+ const event = {
+ target: { value: 'testPassword' }
+ } as React.ChangeEvent
+ fireEvent.change(passwordInput, event)
+ expect(mockProps.handlePassword).toHaveBeenCalledWith(
+ expect.objectContaining({
+ target: expect.objectContaining({ value: 'testPassword' })
+ })
+ )
+ })
+
+ test('calls the handleAccessTokenSwitch function when access token button is clicked', () => {
+ const accessTokenButton = screen.getByRole('button', { name: 'Have Access-Token?' })
+ fireEvent.click(accessTokenButton)
+ expect(mockProps.handleAccessTokenSwitch).toHaveBeenCalled()
+ })
+})
diff --git a/src/components/Page/Login/Password.tsx b/src/components/Page/Login/Password.tsx
new file mode 100644
index 00000000..9051540f
--- /dev/null
+++ b/src/components/Page/Login/Password.tsx
@@ -0,0 +1,46 @@
+import css from './Form.module.css'
+import { ChangeEvent } from 'react'
+
+export interface Props {
+ handleUsername: (event: ChangeEvent) => void
+ handlePassword: (event: ChangeEvent) => void
+ handleAccessTokenSwitch: () => void
+ inputError: boolean
+}
+
+export function Password(props: Props): JSX.Element {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/components/Page/Login/Sso.test.tsx b/src/components/Page/Login/Sso.test.tsx
new file mode 100644
index 00000000..42351cf6
--- /dev/null
+++ b/src/components/Page/Login/Sso.test.tsx
@@ -0,0 +1,26 @@
+import { render, screen } from '@testing-library/react'
+import { Sso } from './Sso'
+
+describe('Sso', () => {
+ beforeEach(() => {
+ render()
+ })
+
+ test('renders the SSO component', () => {
+ const ssoElement = screen.getByText(
+ 'To proceed with authentication, you will be redirected to the SSO login page.'
+ )
+ expect(ssoElement).toBeInTheDocument()
+ })
+
+ test('renders the required Artifactory version paragraph', () => {
+ const versionElement = screen.getByText('Requires Artifactory version 7.57 or higher')
+ expect(versionElement).toBeInTheDocument()
+ })
+
+ test('renders the red star in the required Artifactory version paragraph', () => {
+ const starElement = screen.getByText('*')
+ expect(starElement).toBeInTheDocument()
+ expect(starElement).toHaveClass('redStar')
+ })
+})
diff --git a/src/components/Page/Login/Sso.tsx b/src/components/Page/Login/Sso.tsx
new file mode 100644
index 00000000..b2c5b863
--- /dev/null
+++ b/src/components/Page/Login/Sso.tsx
@@ -0,0 +1,14 @@
+import css from './Form.module.css'
+
+export function Sso(): JSX.Element {
+ return (
+
+
+ To proceed with authentication, you will be redirected to the SSO login page.
+
+
+ Requires Artifactory version 7.57 or higher*
+
+
+ )
+}
diff --git a/src/components/Page/Login/Url.test.tsx b/src/components/Page/Login/Url.test.tsx
new file mode 100644
index 00000000..d058c4b3
--- /dev/null
+++ b/src/components/Page/Login/Url.test.tsx
@@ -0,0 +1,77 @@
+import { render, fireEvent } from '@testing-library/react'
+import { Props, Url } from './Url'
+
+describe('Url login component', () => {
+ const mockHandleUrl = jest.fn()
+ const mockHandleArtifactoryUrl = jest.fn()
+ const mockHandleXrayUrl = jest.fn()
+ const mockHandleAdvancedUrl = jest.fn()
+ const props: Props = {
+ inputError: false,
+ showAdvancedUrl: false,
+ handleUrl: mockHandleUrl,
+ handleArtifactoryUrl: mockHandleArtifactoryUrl,
+ handleXrayUrl: mockHandleXrayUrl,
+ handleAdvancedUrl: mockHandleAdvancedUrl
+ }
+
+ test('renders the platform URL input', () => {
+ const { getByLabelText } = render()
+ expect(getByLabelText('Platform URL')).toBeInTheDocument()
+ })
+
+ test('calls the handleUrl function on platform URL input change', () => {
+ const { getByLabelText } = render()
+ const inputElement = getByLabelText('Platform URL') as HTMLInputElement
+ fireEvent.change(inputElement, { target: { value: 'https://example.com' } })
+ expect(mockHandleUrl).toHaveBeenCalledTimes(1)
+ expect(mockHandleUrl).toHaveBeenCalledWith(expect.any(Object))
+ })
+
+ test('renders the "Advanced" button', () => {
+ const { getByRole } = render()
+ expect(getByRole('button', { name: 'Advanced' })).toBeInTheDocument()
+ })
+
+ test('calls the handleAdvancedUrl function on "Advanced" button click', () => {
+ const { getByRole } = render()
+ fireEvent.click(getByRole('button', { name: 'Advanced' }))
+ expect(mockHandleAdvancedUrl).toHaveBeenCalledTimes(1)
+ })
+
+ test('renders the Artifactory URL input when showAdvancedUrl is true', () => {
+ const customProps: Props = { ...props, showAdvancedUrl: true }
+ const { getByLabelText } = render()
+ const artifactoryInputElement = getByLabelText('Artifactory URL') as HTMLInputElement
+ expect(artifactoryInputElement).toBeInTheDocument()
+ expect(artifactoryInputElement.type).toBe('text')
+ })
+
+ test('calls the handleArtifactoryUrl function on Artifactory URL input change', () => {
+ const customProps: Props = { ...props, showAdvancedUrl: true }
+ const { getByLabelText } = render()
+ const artifactoryInputElement = getByLabelText('Artifactory URL') as HTMLInputElement
+ fireEvent.change(artifactoryInputElement, {
+ target: { value: 'https://artifactory.example.com' }
+ })
+ expect(mockHandleArtifactoryUrl).toHaveBeenCalledTimes(1)
+ expect(mockHandleArtifactoryUrl).toHaveBeenCalledWith(expect.any(Object))
+ })
+
+ test('renders the Xray URL input when showAdvancedUrl is true', () => {
+ const customProps: Props = { ...props, showAdvancedUrl: true }
+ const { getByLabelText } = render()
+ const xrayInputElement = getByLabelText('Xray URL') as HTMLInputElement
+ expect(xrayInputElement).toBeInTheDocument()
+ expect(xrayInputElement.type).toBe('text')
+ })
+
+ test('calls the handleXrayUrl function on Xray URL input change', () => {
+ const customProps: Props = { ...props, showAdvancedUrl: true }
+ const { getByLabelText } = render()
+ const xrayInputElement = getByLabelText('Xray URL') as HTMLInputElement
+ fireEvent.change(xrayInputElement, { target: { value: 'https://xray.example.com' } })
+ expect(mockHandleXrayUrl).toHaveBeenCalledTimes(1)
+ expect(mockHandleXrayUrl).toHaveBeenCalledWith(expect.any(Object))
+ })
+})
diff --git a/src/components/Page/Login/Url.tsx b/src/components/Page/Login/Url.tsx
new file mode 100644
index 00000000..575137f3
--- /dev/null
+++ b/src/components/Page/Login/Url.tsx
@@ -0,0 +1,68 @@
+import css from './Form.module.css'
+import { ChangeEvent } from 'react'
+import { BlueArrowIcon } from '../../UI/Icons/BlueArrowIcon'
+
+export interface Props {
+ inputError: boolean
+ showAdvancedUrl: boolean
+ handleAdvancedUrl: () => void
+ handleUrl: (event: ChangeEvent) => void
+ handleArtifactoryUrl: (event: ChangeEvent) => void
+ handleXrayUrl: (event: ChangeEvent) => void
+}
+
+export function Url(props: Props): JSX.Element {
+ return (
+ <>
+
+
+
+
+
+
+
+ {props.showAdvancedUrl && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+ >
+ )
+}
diff --git a/src/components/UI/Icons/BlueArrowIcon.tsx b/src/components/UI/Icons/BlueArrowIcon.tsx
new file mode 100644
index 00000000..804be40b
--- /dev/null
+++ b/src/components/UI/Icons/BlueArrowIcon.tsx
@@ -0,0 +1,35 @@
+export interface Props {
+ className: string
+}
+
+export function BlueArrowIcon(props: Props): JSX.Element {
+ return (
+
+ )
+}
diff --git a/src/components/UI/Icons/InfoIcon.tsx b/src/components/UI/Icons/InfoIcon.tsx
new file mode 100644
index 00000000..e4763925
--- /dev/null
+++ b/src/components/UI/Icons/InfoIcon.tsx
@@ -0,0 +1,30 @@
+export function InfoIcon(): JSX.Element {
+ return (
+
+ )
+}
diff --git a/src/components/UI/Icons/JfrogIcon.tsx b/src/components/UI/Icons/JfrogIcon.tsx
new file mode 100644
index 00000000..86c558d1
--- /dev/null
+++ b/src/components/UI/Icons/JfrogIcon.tsx
@@ -0,0 +1,17 @@
+export function JfrogIcon(): JSX.Element {
+ return (
+
+ )
+}
diff --git a/src/components/UI/Icons/SsoIcon.tsx b/src/components/UI/Icons/SsoIcon.tsx
new file mode 100644
index 00000000..c061cdab
--- /dev/null
+++ b/src/components/UI/Icons/SsoIcon.tsx
@@ -0,0 +1,18 @@
+export function SsoIcon(): JSX.Element {
+ return (
+
+ )
+}
diff --git a/src/components/UI/Icons/XMark.tsx b/src/components/UI/Icons/XMark.tsx
new file mode 100644
index 00000000..c33a8af1
--- /dev/null
+++ b/src/components/UI/Icons/XMark.tsx
@@ -0,0 +1,17 @@
+export function XMark(): JSX.Element {
+ return (
+
+ )
+}
diff --git a/src/components/UI/Modal/Modal.module.css b/src/components/UI/Modal/Modal.module.css
new file mode 100644
index 00000000..67bcf2c9
--- /dev/null
+++ b/src/components/UI/Modal/Modal.module.css
@@ -0,0 +1,44 @@
+.modal {
+ position: fixed;
+ z-index: 200;
+ border: 10px solid #000000;
+ background-color: rgb(49, 51, 56);
+ border-radius: 30px;
+ text-align: center;
+ box-sizing: border-box;
+ top: 30%;
+ left: 10%;
+ width: 80%;
+ height: 35%;
+ transition: all 0.3s ease-out;
+ animation: openModal 0.4s ease-out forwards;
+ height: 300px;
+}
+
+@keyframes openModal {
+ 0% {
+ opacity: 0;
+ transform: translateY(-100%);
+ }
+
+ 50% {
+ opacity: 1;
+ transform: translateY(30%);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0%);
+ }
+}
+
+.backdrop {
+ position: fixed;
+ display: block;
+ z-index: 100;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+}
diff --git a/src/components/UI/Modal/Modal.test.tsx b/src/components/UI/Modal/Modal.test.tsx
new file mode 100644
index 00000000..98b084aa
--- /dev/null
+++ b/src/components/UI/Modal/Modal.test.tsx
@@ -0,0 +1,44 @@
+import { render, waitFor, fireEvent } from '@testing-library/react'
+import { Modal } from './Modal'
+
+describe('Modal component', () => {
+ const mockChildren = Modal content
+ const mockOnClose = jest.fn()
+ let overlayElement: HTMLElement | null
+
+ beforeEach(() => {
+ overlayElement = document.createElement('div')
+ overlayElement.setAttribute('id', 'overlay')
+ document.body.appendChild(overlayElement)
+ })
+
+ afterEach(() => {
+ overlayElement?.remove()
+ overlayElement = null
+ jest.restoreAllMocks()
+ })
+
+ test('renders the backdrop and modal overlay', async () => {
+ const { getByText } = render({mockChildren})
+ await waitFor(() => {
+ expect(getByText('Modal content')).toBeInTheDocument()
+ expect(document.querySelector('.backdrop')).toBeInTheDocument()
+ expect(document.querySelector('.modal')).toBeInTheDocument()
+ })
+ })
+
+ test('calls onClose function when backdrop is clicked', async () => {
+ render({mockChildren})
+
+ await waitFor(() => {
+ const backdropElement = document.querySelector('.backdrop')
+ expect(backdropElement).toBeInTheDocument()
+
+ if (backdropElement) {
+ fireEvent.click(backdropElement)
+ }
+
+ expect(mockOnClose).toHaveBeenCalledTimes(1)
+ })
+ })
+})
diff --git a/src/components/UI/Modal/Modal.tsx b/src/components/UI/Modal/Modal.tsx
new file mode 100644
index 00000000..0cc34b84
--- /dev/null
+++ b/src/components/UI/Modal/Modal.tsx
@@ -0,0 +1,42 @@
+import css from './Modal.module.css'
+import ReactDOM from 'react-dom'
+
+export interface PropsBackdrop {
+ onClose: () => void
+}
+
+function Backdrop(props: PropsBackdrop): JSX.Element {
+ return
+}
+
+interface modalProps {
+ children: React.ReactNode
+}
+
+function ModalOverlay(props: modalProps): JSX.Element {
+ return (
+
+ )
+}
+
+export interface Props {
+ children: React.ReactNode
+ onClose: () => void
+}
+
+export function Modal(props: Props): JSX.Element {
+ const overlayElement: HTMLElement | null = document.getElementById('overlay')
+
+ if (!overlayElement) {
+ throw new Error('The element #overlay was not found')
+ }
+
+ return (
+ <>
+ {ReactDOM.createPortal(, overlayElement)}
+ {ReactDOM.createPortal({props.children}, overlayElement)}
+ >
+ )
+}
diff --git a/src/components/UI/Modal/pages/LoginModal.module.css b/src/components/UI/Modal/pages/LoginModal.module.css
new file mode 100644
index 00000000..4231d7f6
--- /dev/null
+++ b/src/components/UI/Modal/pages/LoginModal.module.css
@@ -0,0 +1,59 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ color: rgb(242, 243, 245);
+ font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue';
+ line-height: 40px;
+ padding: 8px;
+ margin-top: 30px;
+}
+
+.welcome {
+ color: rgb(242, 243, 245);
+ font-size: 15px;
+ font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
+
+.text {
+ color: rgb(165, 175, 187);
+ font-size: 11px;
+ line-height: 1.5;
+}
+
+.textBold {
+ font-weight: 700;
+}
+
+.closeBtn {
+ display: flex;
+ position: absolute;
+ top: 16px;
+ right: 12px;
+ height: 26px;
+ padding: 4px;
+ opacity: 0.5;
+ color: rgba(255, 255, 255, 0.8);
+ cursor: pointer;
+ border-radius: 3px;
+ box-sizing: content-box;
+ background: transparent;
+ border: none;
+}
+
+.autoConnectBtn {
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ padding: 6px 11px;
+ min-width: 90px;
+ min-height: 35px;
+ color: #ffffff;
+ background: rgb(35, 39, 42);
+ border-radius: 30px;
+ border: none;
+ position: relative;
+ cursor: pointer;
+ transition: all 0.1s ease;
+ margin-top: 30px;
+ font-size: small;
+}
diff --git a/src/components/UI/Modal/pages/LoginModal.test.tsx b/src/components/UI/Modal/pages/LoginModal.test.tsx
new file mode 100644
index 00000000..050fac9e
--- /dev/null
+++ b/src/components/UI/Modal/pages/LoginModal.test.tsx
@@ -0,0 +1,356 @@
+import { render, fireEvent, waitFor } from '@testing-library/react'
+import { LoginModal } from './LoginModal'
+import { ILoginPage, LoginProgressStatus, LoginConnectionType, PageType } from '../../../../model'
+import { eventManagerContext } from '../../../../store/eventContext'
+import { TestEventManager } from '../../../../utils/testUtils'
+import { WebviewSendEventType } from '../../../../api'
+import { SendLoginEvent } from '../../../../api/sendEvent/login'
+
+describe('LoginModal component', () => {
+ const mockOnClose = jest.fn()
+
+ let overlayElement: HTMLElement | null
+
+ beforeEach(() => {
+ overlayElement = document.createElement('div')
+ overlayElement.setAttribute('id', 'overlay')
+ document.body.appendChild(overlayElement)
+ })
+
+ afterEach(() => {
+ overlayElement?.remove()
+ overlayElement = null
+
+ jest.restoreAllMocks()
+ jest.clearAllMocks()
+ })
+
+ describe('JFrog CLI', () => {
+ const mockLoginData: ILoginPage = {
+ status: LoginProgressStatus.Verifying,
+ connectionType: LoginConnectionType.Cli,
+ url: 'example.com',
+ pageType: PageType.Login
+ }
+
+ test('renders the login modal correctly', async () => {
+ const { getByText } = render()
+ await waitFor(() => {
+ expect(
+ getByText('You have a predefined JFrog CLI. Would you like to sign-in to')
+ ).toBeInTheDocument()
+ expect(getByText('example.com')).toBeInTheDocument()
+ expect(getByText('Verifying...')).toBeInTheDocument()
+ expect(document.querySelector('.closeBtn')).toBeInTheDocument()
+ expect(document.querySelector('.text')).toBeInTheDocument()
+ expect(document.querySelector('.welcome')).toBeInTheDocument()
+ expect(document.querySelector('.autoConnectBtn')).not.toBeInTheDocument()
+ })
+ })
+
+ test('calls onClose function when the close button is clicked', async () => {
+ render()
+ await waitFor(() => {
+ const closeButton = document.querySelector('.closeBtn')
+ expect(closeButton).toBeInTheDocument()
+
+ if (closeButton) {
+ fireEvent.click(closeButton)
+ }
+
+ expect(mockOnClose).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ test('calls eventManager Login function when the auto connect button is clicked', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.AutoConnect
+ }
+ render(
+
+
+
+ )
+
+ await waitFor(() => {
+ const autoConnectButton = document.querySelector('.autoConnectBtn')
+ expect(autoConnectButton).toBeInTheDocument()
+
+ if (autoConnectButton) {
+ fireEvent.click(autoConnectButton)
+ }
+
+ expect(mockLogin).toHaveBeenCalledWith({
+ type: WebviewSendEventType.Login,
+ data: { loginConnectionType: LoginConnectionType.Cli }
+ } as SendLoginEvent)
+ })
+ })
+ })
+ describe('Env Vars', () => {
+ const mockLoginData: ILoginPage = {
+ status: LoginProgressStatus.Verifying,
+ connectionType: LoginConnectionType.EnvVars,
+ url: 'example.com',
+ pageType: PageType.Login
+ }
+
+ test('renders the login modal correctly', async () => {
+ const { getByText } = render()
+ await waitFor(() => {
+ expect(
+ getByText('You have a predefined Environment Variable. Would you like to sign-in to')
+ ).toBeInTheDocument()
+ expect(getByText('example.com')).toBeInTheDocument()
+ expect(getByText('Verifying...')).toBeInTheDocument()
+ expect(document.querySelector('.closeBtn')).toBeInTheDocument()
+ expect(document.querySelector('.text')).toBeInTheDocument()
+ expect(document.querySelector('.welcome')).toBeInTheDocument()
+ expect(document.querySelector('.autoConnectBtn')).not.toBeInTheDocument()
+ })
+ })
+
+ test('calls eventManager Login function when the auto connect button is clicked', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.AutoConnect
+ }
+ render(
+
+
+
+ )
+
+ await waitFor(() => {
+ const autoConnectButton = document.querySelector('.autoConnectBtn')
+ expect(autoConnectButton).toBeInTheDocument()
+
+ if (autoConnectButton) {
+ fireEvent.click(autoConnectButton)
+ }
+
+ expect(mockLogin).toHaveBeenCalledWith({
+ type: WebviewSendEventType.Login,
+ data: { loginConnectionType: LoginConnectionType.EnvVars }
+ } as SendLoginEvent)
+ })
+ })
+ })
+
+ describe('BasicAuthOrToken', () => {
+ const mockLoginData: ILoginPage = {
+ status: LoginProgressStatus.Verifying,
+ connectionType: LoginConnectionType.BasicAuthOrToken,
+ url: 'example.com',
+ pageType: PageType.Login
+ }
+
+ test('renders the login modal correctly', async () => {
+ const { getByText, queryByText } = render(
+
+ )
+ await waitFor(() => {
+ expect(
+ queryByText('You have a predefined Environment Variable. Would you like to sign-in to')
+ ).not.toBeInTheDocument()
+ expect(queryByText('example.com')).not.toBeInTheDocument()
+ expect(getByText('Verifying...')).toBeInTheDocument()
+ expect(document.querySelector('.closeBtn')).toBeInTheDocument()
+ expect(document.querySelector('.text')).toBeInTheDocument()
+ expect(document.querySelector('.welcome')).toBeInTheDocument()
+ expect(document.querySelector('.autoConnectBtn')).not.toBeInTheDocument()
+ })
+ })
+
+ test('calls eventManager Login function when the auto connect button is clicked', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.AutoConnect
+ }
+ render(
+
+
+
+ )
+
+ await waitFor(() => {
+ const autoConnectButton = document.querySelector('.autoConnectBtn')
+ expect(autoConnectButton).toBeInTheDocument()
+
+ if (autoConnectButton) {
+ fireEvent.click(autoConnectButton)
+ }
+
+ expect(mockLogin).toHaveBeenCalledWith({
+ type: WebviewSendEventType.Login,
+ data: { loginConnectionType: LoginConnectionType.BasicAuthOrToken }
+ } as SendLoginEvent)
+ })
+ })
+ })
+ describe('Sso', () => {
+ const mockLoginData: ILoginPage = {
+ status: LoginProgressStatus.Verifying,
+ connectionType: LoginConnectionType.Sso,
+ url: 'example.com',
+ pageType: PageType.Login
+ }
+
+ test('renders the login modal correctly', async () => {
+ const { getByText, queryByText } = render(
+
+ )
+ await waitFor(() => {
+ expect(
+ queryByText('You have a predefined Environment Variable. Would you like to sign-in to')
+ ).not.toBeInTheDocument()
+ expect(getByText('Almost there!')).toBeInTheDocument()
+ expect(getByText('Waiting for you to sign in...')).toBeInTheDocument()
+ expect(document.querySelector('.closeBtn')).toBeInTheDocument()
+ expect(document.querySelector('.text')).toBeInTheDocument()
+ expect(document.querySelector('.welcome')).toBeInTheDocument()
+ expect(document.querySelector('.autoConnectBtn')).not.toBeInTheDocument()
+ })
+ })
+
+ test('calls eventManager Login function when the auto connect button is clicked', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.AutoConnect
+ }
+ render(
+
+
+
+ )
+
+ await waitFor(() => {
+ const autoConnectButton = document.querySelector('.autoConnectBtn')
+ expect(autoConnectButton).toBeInTheDocument()
+
+ if (autoConnectButton) {
+ fireEvent.click(autoConnectButton)
+ }
+
+ expect(mockLogin).toHaveBeenCalledWith({
+ type: WebviewSendEventType.Login,
+ data: { loginConnectionType: LoginConnectionType.Sso }
+ } as SendLoginEvent)
+ })
+ })
+
+ test('should render fail to sign in', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.Failed
+ }
+ const { getByText } = render(
+
+
+
+ )
+
+ await waitFor(() => {
+ expect(getByText('Connection could not be established.')).toBeInTheDocument()
+ })
+ })
+ test('should render FailedBadCredentials', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.FailedBadCredentials
+ }
+ const { getByText } = render(
+
+
+
+ )
+
+ await waitFor(() => {
+ expect(getByText('Invalid credentials.')).toBeInTheDocument()
+ })
+ })
+ test('should render FailedTimeout', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.FailedTimeout
+ }
+ const { getByText } = render(
+
+
+
+ )
+
+ await waitFor(() => {
+ expect(getByText('A timeout occurred. Please try again.')).toBeInTheDocument()
+ })
+ })
+ test('should render FailedServerNotFound', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.FailedServerNotFound
+ }
+ const { getByText } = render(
+
+
+
+ )
+
+ await waitFor(() => {
+ expect(getByText('Server not found.')).toBeInTheDocument()
+ })
+ })
+ test('should render FailedServerNotSupported', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.FailedServerNotSupported
+ }
+ const { getByText } = render(
+
+
+
+ )
+
+ await waitFor(() => {
+ expect(getByText('The server is not compatible with SSO login.')).toBeInTheDocument()
+ })
+ })
+ test('should render FailedServerNotFound', async () => {
+ const mockLogin = jest.fn()
+ const mockEventManager = new TestEventManager(mockLogin, mockLogin)
+ const mockLoginDataWithAutoConnect: ILoginPage = {
+ ...mockLoginData,
+ status: LoginProgressStatus.Success
+ }
+ const { getByText } = render(
+
+
+
+ )
+
+ await waitFor(() => {
+ expect(
+ getByText('Your credentials will be securely stored on the machine for future use.')
+ ).toBeInTheDocument()
+ })
+ })
+ })
+})
diff --git a/src/components/UI/Modal/pages/LoginModal.tsx b/src/components/UI/Modal/pages/LoginModal.tsx
new file mode 100644
index 00000000..5032459b
--- /dev/null
+++ b/src/components/UI/Modal/pages/LoginModal.tsx
@@ -0,0 +1,145 @@
+import { EventManager } from '../../../../api/eventManager'
+import {
+ ILoginPage,
+ LoginProgressStatus,
+ LoginConnectionType,
+ ISendLoginEventData
+} from '../../../../model'
+import { eventManagerContext } from '../../../../store/eventContext'
+import { Spinner, State } from '../../Spinner/Spinner'
+import { Modal } from '../Modal'
+import css from './LoginModal.module.css'
+import { useContext } from 'react'
+import { XMark } from './../../Icons/XMark'
+
+export interface Props {
+ onClose: () => void
+ loginData: ILoginPage
+}
+
+export function LoginModal(props: Props): JSX.Element {
+ const ctx: EventManager = useContext(eventManagerContext)
+ return (
+
+
+
+
+
+
+
+
+
+ {createTitle(props.loginData)}
+ {createBody(props.loginData)}
+ {props.loginData.status === LoginProgressStatus.AutoConnect && (
+
+ )}
+
+
+
+ )
+}
+
+function createSpinnerState(s: LoginProgressStatus): State {
+ switch (s) {
+ case LoginProgressStatus.Success:
+ return State.Success
+ case LoginProgressStatus.AutoConnect:
+ return State.AutoConnect
+ case LoginProgressStatus.Failed:
+ case LoginProgressStatus.FailedTimeout:
+ case LoginProgressStatus.FailedServerNotFound:
+ case LoginProgressStatus.FailedBadCredentials:
+ return State.Fail
+ default:
+ return State.Loading
+ }
+}
+
+function createBody(data: ILoginPage): JSX.Element {
+ let pageBody = <> >
+
+ switch (data.connectionType) {
+ case LoginConnectionType.Cli:
+ pageBody = (
+
+ You have a predefined JFrog CLI. Would you like to sign-in to
+ {data.url}?
+
+ )
+ break
+ case LoginConnectionType.EnvVars:
+ pageBody = (
+
+ You have a predefined Environment Variable. Would you like to sign-in to
+ {data.url}?
+
+ )
+ break
+ case LoginConnectionType.Sso:
+ pageBody = Waiting for you to sign in...
+ break
+ }
+
+ switch (data.status) {
+ case LoginProgressStatus.Failed:
+ pageBody = Connection could not be established.
+ break
+ case LoginProgressStatus.FailedBadCredentials:
+ pageBody = Invalid credentials.
+ break
+ case LoginProgressStatus.FailedTimeout:
+ pageBody = A timeout occurred. Please try again.
+ break
+ case LoginProgressStatus.FailedServerNotFound:
+ pageBody = Server not found.
+ break
+ case LoginProgressStatus.FailedServerNotSupported:
+ pageBody = The server is not compatible with SSO login.
+ break
+ case LoginProgressStatus.Success:
+ pageBody = Your credentials will be securely stored on the machine for future use.
+ break
+ }
+
+ return {pageBody}
+}
+
+function createTitle(data: ILoginPage): JSX.Element {
+ let title = <> >
+
+ switch (data.status) {
+ case LoginProgressStatus.Failed:
+ case LoginProgressStatus.FailedBadCredentials:
+ case LoginProgressStatus.FailedTimeout:
+ case LoginProgressStatus.FailedServerNotFound:
+ title = Sign in failed
+ break
+ case LoginProgressStatus.Success:
+ title = You're in!
+ break
+ case LoginProgressStatus.AutoConnect:
+ title = JFrog Server Found
+ break
+ case LoginProgressStatus.Verifying:
+ title =
+ data.connectionType === LoginConnectionType.Sso ? (
+ Almost there!
+ ) : (
+ Verifying...
+ )
+ }
+
+ return {title}
+}
diff --git a/src/components/UI/Spinner/Spinner.module.css b/src/components/UI/Spinner/Spinner.module.css
new file mode 100644
index 00000000..84b6b313
--- /dev/null
+++ b/src/components/UI/Spinner/Spinner.module.css
@@ -0,0 +1,120 @@
+/* Variables */
+:root {
+ --scale: 36px;
+ --speed: 2s;
+ --angle: 25;
+ --color-element: hsl(123, 50%, 50%);
+}
+
+@keyframes loader-spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.loader {
+ display: inline-block;
+ border: calc(var(--scale) * 0.1) solid var(--color-element);
+ overflow: hidden;
+ border-radius: 30px;
+ width: var(--scale);
+ height: var(--scale);
+ animation: loader-spin var(--speed) linear infinite reverse;
+ filter: url(#goo);
+ box-shadow: 0 0 0 1px var(--color-element) inset;
+}
+
+.loader:before {
+ content: '';
+ position: absolute;
+ animation: loader-spin var(--speed) cubic-bezier(0.59, 0.25, 0.4, 0.69) infinite;
+ background: var(--color-element);
+ transform-origin: top center;
+ border-radius: 50%;
+ width: 150%;
+ height: 150%;
+ top: 50%;
+ left: -12.5%;
+}
+
+.checkmark {
+ stroke: var(--color-element);
+ stroke-width: calc(var(--scale) * 0.1);
+ display: inline-block;
+ border: calc(var(--scale) * 0.1) solid var(--color-element);
+ overflow: hidden;
+ border-radius: 30px;
+ width: var(--scale);
+ height: var(--scale);
+ box-shadow: 0 0 0 1px var(--color-element) inset;
+}
+
+.checkmark path {
+ transform: translate(-1%, -2%) scale(1.5);
+ animation: checkmark-draw var(--speed) ease-in-out forwards;
+ stroke-dasharray: 100;
+ stroke-dashoffset: 100;
+}
+
+@keyframes checkmark-draw {
+ 0% {
+ stroke-dashoffset: 100;
+ }
+
+ 100% {
+ stroke-dashoffset: 0;
+ }
+}
+
+/* Failure animation */
+@keyframes failure-animation {
+ 0% {
+ transform: scale(0);
+ opacity: 0;
+ }
+
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+.xmark {
+ display: inline-block;
+ overflow: hidden;
+ width: var(--scale);
+ height: var(--scale);
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ border: calc(var(--scale) * 0.1) solid red;
+ border-radius: 50%;
+ animation: failure-animation 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
+}
+
+.xmark::before,
+.xmark::after {
+ content: '';
+ position: absolute;
+ background-color: red;
+}
+
+.xmark::before,
+.xmark::after {
+ width: calc(var(--scale) * 0.7);
+ height: calc(var(--scale) * 0.08);
+ top: calc(var(--scale) * 0.48);
+ left: calc(var(--scale) * 0.16);
+}
+
+.xmark::before {
+ transform: rotate(45deg);
+}
+
+.xmark::after {
+ transform: rotate(-45deg);
+}
diff --git a/src/components/UI/Spinner/Spinner.tsx b/src/components/UI/Spinner/Spinner.tsx
new file mode 100644
index 00000000..e23a1db7
--- /dev/null
+++ b/src/components/UI/Spinner/Spinner.tsx
@@ -0,0 +1,57 @@
+import { InfoIcon } from '../Icons/InfoIcon'
+import css from './Spinner.module.css'
+
+export enum State {
+ Loading = 0,
+ Fail = 1,
+ Success = 2,
+ AutoConnect = 3
+}
+
+export interface Props {
+ state: State
+}
+
+export function Spinner(props: Props): JSX.Element {
+ let className = css.loader
+
+ if (props.state === State.Success) {
+ className = css.checkmark
+ }
+
+ if (props.state === State.Fail) {
+ className = css.xmark
+ }
+
+ let body = <> >
+
+ if (props.state === State.AutoConnect) {
+ body =
+ } else {
+ body = (
+
+
+
+ )
+ }
+
+ return <>{body}>
+}
diff --git a/src/components/UI/TreeViewer/Filter.tsx b/src/components/UI/TreeViewer/Filter.tsx
index 661bb074..16d9d62d 100644
--- a/src/components/UI/TreeViewer/Filter.tsx
+++ b/src/components/UI/TreeViewer/Filter.tsx
@@ -13,7 +13,6 @@ export default function Filter(props: Props): JSX.Element {
{
+ test('should have correct values for LoginConnectionType', () => {
+ expect(LoginConnectionType.BasicAuthOrToken).toBe('BASIC_AUTH_OR_TOKEN')
+ expect(LoginConnectionType.Sso).toBe('SSO')
+ expect(LoginConnectionType.Cli).toBe('CLI')
+ expect(LoginConnectionType.EnvVars).toBe('ENV_VARS')
+ })
+
+ test('should have correct values for LoginProgressStatus', () => {
+ expect(LoginProgressStatus.Initial).toBe('INITIAL')
+ expect(LoginProgressStatus.AutoConnect).toBe('AUTO_CONNECT')
+ expect(LoginProgressStatus.AutoConnectAccepted).toBe('AUTO_CONNECT_ACCEPTED')
+ expect(LoginProgressStatus.Verifying).toBe('VERIFYING')
+ expect(LoginProgressStatus.Success).toBe('SUCCESS')
+ expect(LoginProgressStatus.Failed).toBe('FAILED')
+ expect(LoginProgressStatus.FailedTimeout).toBe('FAILED_TIMEOUT')
+ expect(LoginProgressStatus.FailedBadCredentials).toBe('FAILED_BAD_CREDENTIALS')
+ expect(LoginProgressStatus.FailedServerNotFound).toBe('FAILED_SERVER_NOT_FOUND')
+ expect(LoginProgressStatus.FailedServerNotSupported).toBe('FAILED_SERVER_NOT_SUPPORTED')
+ expect(LoginProgressStatus.FailedSaveCredentials).toBe('FAILED_SAVE_CREDENTIALS')
+ })
+})
+
+describe('ISendLoginEventData', () => {
+ test('should have the required properties', () => {
+ const eventData: ISendLoginEventData = {
+ loginConnectionType: LoginConnectionType.BasicAuthOrToken
+ }
+
+ expect(eventData).toHaveProperty('loginConnectionType')
+ })
+
+ test('should allow optional properties', () => {
+ const eventData: ISendLoginEventData = {
+ loginConnectionType: LoginConnectionType.BasicAuthOrToken,
+ url: 'https://example.com',
+ username: 'username',
+ password: 'password'
+ }
+
+ expect(eventData).toHaveProperty('url', 'https://example.com')
+ expect(eventData).toHaveProperty('username', 'username')
+ expect(eventData).toHaveProperty('password', 'password')
+ })
+})
diff --git a/src/model/login.ts b/src/model/login.ts
new file mode 100644
index 00000000..68b83a76
--- /dev/null
+++ b/src/model/login.ts
@@ -0,0 +1,30 @@
+export enum LoginConnectionType {
+ BasicAuthOrToken = 'BASIC_AUTH_OR_TOKEN',
+ Sso = 'SSO',
+ Cli = 'CLI',
+ EnvVars = 'ENV_VARS'
+}
+
+export enum LoginProgressStatus {
+ Initial = 'INITIAL',
+ AutoConnect = 'AUTO_CONNECT',
+ AutoConnectAccepted = 'AUTO_CONNECT_ACCEPTED',
+ Verifying = 'VERIFYING',
+ Success = 'SUCCESS',
+ Failed = 'FAILED',
+ FailedTimeout = 'FAILED_TIMEOUT',
+ FailedBadCredentials = 'FAILED_BAD_CREDENTIALS',
+ FailedServerNotFound = 'FAILED_SERVER_NOT_FOUND',
+ FailedServerNotSupported = 'FAILED_SERVER_NOT_SUPPORTED',
+ FailedSaveCredentials = 'FAILED_SAVE_CREDENTIALS'
+}
+
+export interface ISendLoginEventData {
+ url?: string
+ artifactoryUrl?: string
+ xrayUrl?: string
+ username?: string
+ password?: string
+ accessToken?: string
+ loginConnectionType: LoginConnectionType
+}
diff --git a/src/model/secret.ts b/src/model/secret.ts
new file mode 100644
index 00000000..4f73608a
--- /dev/null
+++ b/src/model/secret.ts
@@ -0,0 +1,6 @@
+export interface ISecretFindings {
+ snippet?: string
+ meaning?: string
+ happen?: string
+ do?: string
+}
diff --git a/src/model/webviewPages.test.ts b/src/model/webviewPages.test.ts
index 16d83290..a9d1f284 100644
--- a/src/model/webviewPages.test.ts
+++ b/src/model/webviewPages.test.ts
@@ -1,5 +1,13 @@
+import { LoginConnectionType, LoginProgressStatus } from './login'
import { ISeverity } from './severity'
-import { IDependencyPage, IEosPage, IIaCPage, ISecretsPage, PageType } from './webviewPages'
+import {
+ IDependencyPage,
+ IEosPage,
+ IIaCPage,
+ ILoginPage,
+ ISecretsPage,
+ PageType
+} from './webviewPages'
describe('Model - WebviewPage', () => {
test('should have the correct properties for IDependencyPage', () => {
@@ -194,4 +202,18 @@ describe('Model - WebviewPage', () => {
expect(iacPage.severity).toBe(ISeverity.Low)
expect(iacPage.abbreviation).toBe('DEF')
})
+
+ test('should correctly type LoginPage', () => {
+ const loginPage: ILoginPage = {
+ pageType: PageType.Login,
+ url: 'https://example.com',
+ status: LoginProgressStatus.Success,
+ connectionType: LoginConnectionType.Cli
+ }
+
+ expect(loginPage.pageType).toBe(PageType.Login)
+ expect(loginPage.url).toBe('https://example.com')
+ expect(loginPage.status).toBe(LoginProgressStatus.Success)
+ expect(loginPage.connectionType).toBe(LoginConnectionType.Cli)
+ })
})
diff --git a/src/model/webviewPages.ts b/src/model/webviewPages.ts
index 6ba78b41..310a12c9 100644
--- a/src/model/webviewPages.ts
+++ b/src/model/webviewPages.ts
@@ -5,6 +5,9 @@ import { IReference } from './reference'
import { IExtendedInformation } from './extendedInformation'
import { ISeverity } from './severity'
import { IAnalysisStep } from './analysisStep'
+import { ISecretFindings } from './secret'
+import { IIacFindings } from './iac'
+import { LoginProgressStatus, LoginConnectionType } from './login'
export enum PageType {
Empty = 'EMPTY',
@@ -12,7 +15,8 @@ export enum PageType {
Eos = 'EOS',
CveApplicability = 'CVE_APPLICABILITY',
IaC = 'IaC',
- Secrets = 'SECRETS'
+ Secrets = 'SECRETS',
+ Login = 'LOGIN'
}
export interface IDependencyPage {
@@ -44,13 +48,6 @@ export interface ISecretsPage {
finding?: ISecretFindings
}
-export interface ISecretFindings {
- snippet?: string
- meaning?: string
- happen?: string
- do?: string
-}
-
export interface IEosPage {
pageType: PageType.Eos
header: string
@@ -72,11 +69,11 @@ export interface IIaCPage {
finding?: IIacFindings
}
-export interface IIacFindings {
- snippet?: string
- meaning?: string
- happen?: string
- do?: string
+export interface ILoginPage {
+ pageType: PageType.Login
+ url: string
+ status: LoginProgressStatus
+ connectionType: LoginConnectionType
}
-export type WebviewPage = IDependencyPage | IEosPage | IIaCPage | ISecretsPage
+export type WebviewPage = IDependencyPage | IEosPage | IIaCPage | ISecretsPage | ILoginPage
diff --git a/src/types/index.ts b/src/types/index.ts
index b50124ec..c6c04082 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,19 +1,4 @@
-export { IImpactGraph } from '../model/impactGraph'
-export { ILicense } from '../model/license'
-export { IExtendedInformation, ISeverityReasons } from '../model/extendedInformation'
-export { IReference } from '../model/reference'
-export { ICve, IApplicableDetails, IEvidence } from '../model/cve'
-export { IAnalysisStep } from '../model/analysisStep'
-export { ISeverity } from '../model/severity'
-export { IdeEvent, IdeEventType } from '../api/ideEvent'
-export { WebviewEvent, webviewEventType, SetEmitterEvent, ShowPageEvent } from '../api/webviewEvent'
-export {
- PageType,
- WebviewPage,
- ISecretsPage,
- ISecretFindings,
- IIaCPage,
- IIacFindings,
- IEosPage,
- IDependencyPage
-} from '../model/webviewPages'
+export * from '../api'
+export * from '../api/receiveEvent'
+export * from '../api/sendEvent'
+export * from '../model'
diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts
index 0270a40c..f1c58beb 100644
--- a/src/utils/testUtils.ts
+++ b/src/utils/testUtils.ts
@@ -1,6 +1,8 @@
import { Matcher, SelectorMatcherOptions } from '@testing-library/react'
-import { webviewEventType } from '../api/webviewEvent'
import { act } from 'react-dom/test-utils'
+import { WebviewReceiveEventType } from '../api'
+import { EventManager } from '../api/eventManager'
+import { WebviewPage } from '../model'
export const getByTextAcrossMultipleElements = (
getByText: (text: Matcher, options?: SelectorMatcherOptions | undefined) => HTMLElement,
@@ -18,7 +20,14 @@ export const sendWebviewPage = async (pageData: IPageData): Promise>, f: () => void) {
+ super(setPageState)
+ this.sendFunc = f
+ }
+}