Compare commits
3 Commits
commit-05d
...
commit-808
Author | SHA1 | Date | |
---|---|---|---|
8086f6880d | |||
a55248774c | |||
936590d822 |
12
.github/workflows/end-to-end-test.yml
vendored
12
.github/workflows/end-to-end-test.yml
vendored
@ -111,7 +111,19 @@ jobs:
|
||||
regctl registry set --tls disabled $REGISTRY_HOST:$REGISTRY_PORT
|
||||
make test-data REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT
|
||||
|
||||
- name: Install playwright dependencies
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
make playwright-browsers
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
make integration-tests REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT
|
||||
|
||||
- name: Upload playwright report
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -129,3 +129,8 @@ dist
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
data.md
|
||||
|
9
Makefile
9
Makefile
@ -30,8 +30,13 @@ test-data:
|
||||
--registry $(REGISTRY_HOST):$(REGISTRY_PORT) \
|
||||
--data-dir tests/data \
|
||||
--config-file tests/data/config.yaml \
|
||||
--metadata-file tests/data/image_metadata.json
|
||||
--metadata-file tests/data/image_metadata.json \
|
||||
-d
|
||||
|
||||
.PHONY: playwright-browsers
|
||||
playwright-browsers:
|
||||
npx playwright install --with-deps
|
||||
|
||||
.PHONY: integration-tests
|
||||
integration-tests: # Triggering the tests TBD
|
||||
cat tests/data/image_metadata.json | jq
|
||||
UI_HOST=$(REGISTRY_HOST):$(REGISTRY_PORT) API_HOST=$(REGISTRY_HOST):$(REGISTRY_PORT) npm run test:ui
|
||||
|
17502
package-lock.json
generated
17502
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -5,7 +5,6 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.9.3",
|
||||
"@emotion/styled": "^11.9.3",
|
||||
"@mui-treasury/styles": "^1.13.1",
|
||||
"@mui/icons-material": "^5.2.5",
|
||||
"@mui/lab": "^5.0.0-alpha.89",
|
||||
"@mui/material": "^5.8.6",
|
||||
@ -18,7 +17,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.5.2",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
"nth-check": "^2.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
@ -26,18 +24,24 @@
|
||||
"web-vitals": "^2.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.1",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"prettier": "^2.7.1",
|
||||
"react-scripts": "^5.0.1"
|
||||
"react-scripts": "^5.0.1",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --detectOpenHandles",
|
||||
"test:coverage": "react-scripts test --detectOpenHandles --coverage",
|
||||
"test:ui": "playwright test",
|
||||
"test:ui-headed": "playwright test --headed --trace on",
|
||||
"test:ui-debug": "playwright test --trace on",
|
||||
"test:release": "npm run test && npm run test:ui",
|
||||
"lint": "eslint -c .eslintrc.json --ext .js,.jsx .",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc",
|
||||
|
113
playwright.config.js
Normal file
113
playwright.config.js
Normal file
@ -0,0 +1,113 @@
|
||||
// @ts-check
|
||||
const { devices } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
* @type {import('@playwright/test').PlaywrightTestConfig}
|
||||
*/
|
||||
const config = {
|
||||
testDir: './tests',
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 50 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 15000
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: 2,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: 2,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
ignoreHTTPSErrors: true
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
module.exports = config;
|
@ -76,4 +76,10 @@
|
||||
.hide-on-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
.hide-on-small {
|
||||
display:none
|
||||
}
|
||||
}
|
25
src/App.js
25
src/App.js
@ -1,22 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import HomePage from './pages/HomePage.jsx';
|
||||
import LoginPage from './pages/LoginPage.jsx';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
|
||||
import { isAuthenticated } from 'utilities/authUtilities';
|
||||
|
||||
import HomePage from './pages/HomePage';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import { AuthWrapper } from 'utilities/AuthWrapper';
|
||||
import RepoPage from 'pages/RepoPage';
|
||||
import TagPage from 'pages/TagPage';
|
||||
import ExplorePage from 'pages/ExplorePage';
|
||||
|
||||
import './App.css';
|
||||
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AuthWrapper } from 'utilities/AuthWrapper.jsx';
|
||||
import RepoPage from 'pages/RepoPage.jsx';
|
||||
import TagPage from 'pages/TagPage';
|
||||
import ExplorePage from 'pages/ExplorePage.jsx';
|
||||
|
||||
function App() {
|
||||
const isToken = () => {
|
||||
const localStorageToken = localStorage.getItem('token');
|
||||
return localStorageToken ? true : false;
|
||||
};
|
||||
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(isToken());
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(isAuthenticated());
|
||||
|
||||
return (
|
||||
<div className="App" data-testid="app-container">
|
||||
|
@ -1,8 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import App from './App';
|
||||
import MockThemeProvider from './__mocks__/MockThemeProvider';
|
||||
|
||||
it('renders the app component', () => {
|
||||
render(<App />);
|
||||
render(
|
||||
<MockThemeProvider>
|
||||
<App />
|
||||
</MockThemeProvider>
|
||||
);
|
||||
expect(screen.getByTestId('app-container')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -3,8 +3,8 @@ import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
const theme = createTheme();
|
||||
|
||||
function MockThemeProvier({ children }) {
|
||||
function MockThemeProvider({ children }) {
|
||||
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||
}
|
||||
|
||||
export default MockThemeProvier;
|
||||
export default MockThemeProvider;
|
||||
|
@ -6,7 +6,7 @@ import React from 'react';
|
||||
import { createSearchParams, MemoryRouter } from 'react-router-dom';
|
||||
import filterConstants from 'utilities/filterConstants.js';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria.js';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// router mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@ -18,11 +18,11 @@ jest.mock('react-router-dom', () => ({
|
||||
const StateExploreWrapper = (props) => {
|
||||
const queryString = props.search || '';
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<MemoryRouter initialEntries={[`/explore?${queryString.toString()}`]}>
|
||||
<Explore />
|
||||
</MemoryRouter>
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
const mockImageList = {
|
||||
@ -236,6 +236,10 @@ beforeEach(() => {
|
||||
disconnect: () => null
|
||||
});
|
||||
window.IntersectionObserver = mockIntersectionObserver;
|
||||
Object.defineProperty(window.document, 'cookie', {
|
||||
writable: true,
|
||||
value: 'user=test'
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -2,19 +2,19 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import FilterCard from 'components/Shared/FilterCard';
|
||||
import React, { useState } from 'react';
|
||||
import filterConstants from 'utilities/filterConstants';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const StateFilterCardWrapper = () => {
|
||||
const [filters, setFilters] = useState([]);
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<FilterCard
|
||||
title="Operating System"
|
||||
filters={filterConstants.osFilters}
|
||||
updateFilters={setFilters}
|
||||
filterValue={filters}
|
||||
/>
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -4,7 +4,7 @@ import Home from 'components/Home/Home';
|
||||
import React from 'react';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@ -15,9 +15,9 @@ jest.mock('react-router-dom', () => ({
|
||||
|
||||
const HomeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<Home />
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -2,19 +2,22 @@ import { render, screen } from '@testing-library/react';
|
||||
import LoginPage from 'pages/LoginPage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
it('renders the signin presentation component and signin components if auth enabled', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<LoginPage isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<LoginPage isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
expect(screen.getByTestId('login-container')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('presentation-container')).toBeInTheDocument();
|
||||
|
@ -4,6 +4,12 @@ import SignIn from 'components/Login/SignIn';
|
||||
import { api } from '../../api';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const mockMgmtResponse = {
|
||||
distSpecVersion: '1.1.0-dev',
|
||||
binaryType: '-apikey-lint-metrics-mgmt-scrub-search-sync-ui-userprefs',
|
||||
http: { auth: { htpasswd: {} } }
|
||||
};
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
@ -35,7 +41,10 @@ describe('Signin component automatic navigation', () => {
|
||||
describe('Sign in form', () => {
|
||||
beforeEach(() => {
|
||||
// mock auth check request
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} });
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 401,
|
||||
data: mockMgmtResponse
|
||||
});
|
||||
});
|
||||
|
||||
it('should change username and password values on user input', async () => {
|
||||
@ -77,7 +86,7 @@ describe('Sign in form', () => {
|
||||
it('should should display login error if login not successful', async () => {
|
||||
render(<SignIn isAuthEnabled={true} setIsAuthEnabled={() => {}} isLoggedIn={false} setIsLoggedIn={() => {}} />);
|
||||
const submitButton = await screen.findByText('Continue');
|
||||
jest.spyOn(api, 'get').mockRejectedValue();
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 401, data: {} });
|
||||
fireEvent.click(submitButton);
|
||||
const errorDisplay = await screen.findByText(/Authentication Failed/i);
|
||||
await waitFor(() => {
|
||||
|
@ -3,14 +3,14 @@ import RepoDetails from 'components/Repo/RepoDetails';
|
||||
import React from 'react';
|
||||
import { api } from 'api';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const RepoDetailsThemeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<RepoDetails />
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -234,6 +234,13 @@ const mockRepoDetailsHigh = {
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window.document, 'cookie', {
|
||||
writable: true,
|
||||
value: 'user=test'
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
|
@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
|
||||
import RepoPage from 'pages/RepoPage';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
@ -27,18 +27,13 @@ afterEach(() => {
|
||||
|
||||
it('renders the repository page component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<MockThemeProvier>
|
||||
<RepoPage />
|
||||
</MockThemeProvier>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<RepoPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
expect(screen.getByTestId('repo-container')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -2,13 +2,13 @@ import { fireEvent, waitFor, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Tags from 'components/Repo/Tabs/Tags';
|
||||
import React from 'react';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const TagsThemeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<Tags tags={mockedTagsData} />
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import RepoCard from 'components/Shared/RepoCard';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
// usenavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@ -28,7 +28,7 @@ const mockImage = {
|
||||
const RepoCardWrapper = (props) => {
|
||||
const { image } = props;
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<RepoCard
|
||||
name={image.name}
|
||||
version={image.latestVersion}
|
||||
@ -38,7 +38,7 @@ const RepoCardWrapper = (props) => {
|
||||
lastUpdated={image.lastUpdated}
|
||||
platforms={image.platforms}
|
||||
/>
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { api } from 'api';
|
||||
import DependsOn from 'components/Tag/Tabs/DependsOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const mockDependenciesList = {
|
||||
data: {
|
||||
@ -53,13 +53,13 @@ const mockDependenciesList = {
|
||||
|
||||
const RouterDependsWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<DependsOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { api } from 'api';
|
||||
import IsDependentOn from 'components/Tag/Tabs/IsDependentOn';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
|
||||
const mockDependentsList = {
|
||||
data: {
|
||||
@ -53,13 +53,13 @@ const mockDependentsList = {
|
||||
|
||||
const RouterDependsWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<IsDependentOn name="alpine:latest" />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,18 +3,18 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import TagDetails from 'components/Tag/TagDetails';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||
|
||||
const TagDetailsThemeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="*" element={<TagDetails />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
import MockThemeProvider from '__mocks__/MockThemeProvider';
|
||||
import { api } from 'api';
|
||||
import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails';
|
||||
import React from 'react';
|
||||
@ -8,11 +8,11 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
const StateVulnerabilitiesWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<MockThemeProvider>
|
||||
<MemoryRouter>
|
||||
<VulnerabilitiesDetails name="mongo" />
|
||||
</MemoryRouter>
|
||||
</MockThemeProvier>
|
||||
</MockThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
35
src/api.js
35
src/api.js
@ -1,14 +1,25 @@
|
||||
import axios from 'axios';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||
import { logoutUser } from 'utilities/authUtilities';
|
||||
import { host } from 'host';
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
if (config.url.includes(endpoints.authConfig)) {
|
||||
config.withCredentials = false;
|
||||
} else {
|
||||
config.headers['X-ZOT-API-CLIENT'] = 'zot-ui';
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
if (error.response.status === 401) {
|
||||
localStorage.clear();
|
||||
if (error?.response?.status === 401) {
|
||||
logoutUser();
|
||||
window.location.replace('/login');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@ -19,24 +30,15 @@ const api = {
|
||||
getAxiosInstance: () => axios,
|
||||
|
||||
getRequestCfg: () => {
|
||||
const authConfig = JSON.parse(localStorage.getItem('authConfig'));
|
||||
const genericHeaders = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
const token = localStorage.getItem('token');
|
||||
if (token && token !== '-') {
|
||||
const authHeaders = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${token}`
|
||||
};
|
||||
return {
|
||||
headers: authHeaders
|
||||
};
|
||||
}
|
||||
|
||||
// withCredentials option must be enabled on cross-origin
|
||||
return {
|
||||
headers: genericHeaders
|
||||
headers: genericHeaders,
|
||||
withCredentials: host() !== window?.location?.origin && authConfig !== null
|
||||
};
|
||||
},
|
||||
|
||||
@ -74,7 +76,10 @@ const api = {
|
||||
};
|
||||
|
||||
const endpoints = {
|
||||
status: `/v2/`,
|
||||
authConfig: `/v2/_zot/ext/mgmt`,
|
||||
openidAuth: `/auth/login`,
|
||||
logout: `/auth/logout`,
|
||||
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
||||
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
|
3
src/assets/GhIcon.svg
Normal file
3
src/assets/GhIcon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.0001 0.296387C5.3735 0.296387 0 5.66889 0 12.2965C0 17.5984 3.43839 22.0966 8.2064 23.6833C8.80613 23.7944 9.0263 23.423 9.0263 23.1061C9.0263 22.8199 9.01518 21.8746 9.01001 20.8719C5.67157 21.5978 4.96712 19.456 4.96712 19.456C4.42125 18.069 3.63473 17.7002 3.63473 17.7002C2.54596 16.9554 3.7168 16.9707 3.7168 16.9707C4.92181 17.0554 5.55632 18.2073 5.55632 18.2073C6.6266 20.0419 8.36359 19.5115 9.04836 19.2049C9.15607 18.4293 9.46706 17.8999 9.81024 17.6002C7.14486 17.2968 4.34295 16.2678 4.34295 11.6697C4.34295 10.3596 4.81172 9.28911 5.57937 8.44874C5.45477 8.14649 5.04402 6.92597 5.69562 5.27305C5.69562 5.27305 6.70331 4.95053 8.9965 6.5031C9.95372 6.23722 10.9803 6.10388 12.0001 6.09931C13.0199 6.10388 14.0473 6.23722 15.0063 6.5031C17.2967 4.95053 18.303 5.27305 18.303 5.27305C18.9562 6.92597 18.5452 8.14649 18.4206 8.44874C19.1901 9.28911 19.6557 10.3596 19.6557 11.6697C19.6557 16.2788 16.8484 17.2936 14.1762 17.5907C14.6067 17.9631 14.9902 18.6934 14.9902 19.8129C14.9902 21.4186 14.9763 22.7108 14.9763 23.1061C14.9763 23.4254 15.1923 23.7996 15.8006 23.6818C20.566 22.0932 24 17.5967 24 12.2965C24 5.66889 18.6273 0.296387 12.0001 0.296387ZM4.49443 17.3908C4.468 17.4504 4.3742 17.4683 4.28876 17.4273C4.20172 17.3882 4.15283 17.3069 4.18105 17.2471C4.20688 17.1857 4.30088 17.1686 4.38772 17.2098C4.47495 17.2489 4.52463 17.331 4.49443 17.3908ZM5.0847 17.9175C5.02747 17.9705 4.91559 17.9459 4.83968 17.862C4.76119 17.7784 4.74648 17.6665 4.80451 17.6126C4.86353 17.5596 4.97203 17.5844 5.05072 17.6681C5.12921 17.7527 5.14451 17.8638 5.0847 17.9175ZM5.48965 18.5914C5.41612 18.6424 5.2959 18.5945 5.22158 18.4878C5.14805 18.3811 5.14805 18.2531 5.22317 18.2019C5.29769 18.1506 5.41612 18.1967 5.49144 18.3026C5.56476 18.4111 5.56476 18.5391 5.48965 18.5914ZM6.1745 19.3718C6.10873 19.4443 5.96863 19.4249 5.86609 19.3259C5.76117 19.2291 5.73196 19.0918 5.79793 19.0193C5.8645 18.9466 6.00539 18.967 6.10873 19.0652C6.21285 19.1618 6.24465 19.3001 6.1745 19.3718ZM7.05961 19.6353C7.0306 19.7293 6.89567 19.772 6.75975 19.7321C6.62402 19.6909 6.5352 19.5808 6.56262 19.4858C6.59084 19.3913 6.72636 19.3467 6.86328 19.3895C6.9988 19.4304 7.08783 19.5397 7.05961 19.6353ZM8.0669 19.747C8.07028 19.846 7.95502 19.9281 7.81235 19.9299C7.66887 19.933 7.55282 19.853 7.55123 19.7556C7.55123 19.6556 7.6639 19.5744 7.80738 19.572C7.95006 19.5692 8.0669 19.6487 8.0669 19.747ZM9.05645 19.7091C9.07354 19.8057 8.97438 19.9048 8.8327 19.9313C8.6934 19.9567 8.56443 19.8971 8.54674 19.8013C8.52945 19.7024 8.6304 19.6032 8.7695 19.5776C8.91139 19.5529 9.03837 19.6109 9.05645 19.7091Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.6 MiB |
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
BIN
src/assets/zot-white-horizontal.png
Normal file
BIN
src/assets/zot-white-horizontal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
BIN
src/assets/zot-white-icon.png
Normal file
BIN
src/assets/zot-white-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.7 KiB |
27
src/assets/zotLogoWhiteHorizontal.svg
Normal file
27
src/assets/zotLogoWhiteHorizontal.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 155.8 73.7" style="enable-background:new 0 0 155.8 73.7;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2_1_">
|
||||
</g>
|
||||
<path class="st0" d="M58,45.8c6,1.1,10.4,2.4,10.4,5.6c0,6.1-11.9,9-23.1,9s-23.1-3.2-23.1-9c0-5.2,11.9-6.6,23.1-6.6h0.1
|
||||
c0,0-3.1,1.1-6,2.1c-2.5,0.9-7.9,1.9-9.2,2.3c-4.4,1.1-5.3,2.4-5.3,2.6c0,0.3,1,1.5,5.3,2.6c4,1,9.3,1.6,15,1.6s11-0.6,15-1.6
|
||||
c4.4-1.1,5.3-2.4,5.3-2.6c0-0.3-1-1.5-5.3-2.6c-1.4-0.4-3-0.7-4.8-0.9L58,45.8z"/>
|
||||
<path class="st0" d="M45.4,31.2c-11.1,0-23.1-2.7-23.1-7.7c0-5.4,11.9-8.1,23.1-8.1s23.1,2.8,23.1,8.1
|
||||
C68.5,28.5,56.5,31.2,45.4,31.2z M45.4,19.5c-5.7,0-11,0.5-15,1.5c-4.4,1.1-5.3,2.2-5.3,2.5c0,0.2,1,1.4,5.3,2.5c4,1,9.3,1.5,15,1.5
|
||||
s11-0.5,15-1.5c4.4-1.1,5.3-2.2,5.3-2.5c0-0.2-1-1.4-5.3-2.5C56.4,20.1,51.1,19.5,45.4,19.5z"/>
|
||||
<path class="st0" d="M65,36.1C49.3,54.5,39.5,56.6,37.3,56.6c-0.1,0-11.9-2.6-11.9-2.6L25,51.7C34.4,50.7,51.6,48,65,36.1z"/>
|
||||
<path class="st0" d="M48.3,43.7c3.3-1.5,11.8-10.2,10.9-15.4l9.3-4.8C68.5,35.9,51,42.2,48.3,43.7z"/>
|
||||
<g>
|
||||
<path class="st0" d="M97.2,33.4L84.1,48.1h13.1v3.5H79.3v-3.9L92.4,33H79.9v-3.4h17.2V33.4z"/>
|
||||
<path class="st0" d="M100.1,40.6c0-4,1.2-7.1,3.6-9.1c1.9-1.7,4.3-2.5,7.3-2.5c3.4,0,6.1,1.1,8,3.3c1.9,2,2.8,4.8,2.8,8.4
|
||||
c0,4-1.2,7-3.5,9.1c-1.9,1.7-4.4,2.5-7.4,2.5c-3.4,0-6.1-1.1-8.1-3.4C101.1,46.8,100.1,44,100.1,40.6z M104.4,40.6
|
||||
c0,2.8,0.7,5,2.2,6.5c1.2,1.2,2.7,1.9,4.5,1.9c2.1,0,3.8-0.8,5-2.4c1.1-1.5,1.6-3.4,1.6-5.9c0-2.9-0.7-5.1-2.1-6.5
|
||||
c-1.2-1.2-2.7-1.8-4.5-1.8c-2.1,0-3.8,0.8-5,2.4C104.9,36.1,104.4,38.1,104.4,40.6z"/>
|
||||
<path class="st0" d="M136.3,48.3v3.5c-1,0.3-2,0.4-3,0.4c-2,0-3.6-0.6-4.6-1.9c-0.9-1-1.3-2.4-1.3-4.2V33h-3.3v-3.5h3.3l0.3-5.3
|
||||
h3.8v5.3h4.7V33h-4.7v13.2c0,1.6,0.8,2.5,2.3,2.5C134.6,48.6,135.4,48.5,136.3,48.3z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
@ -1,17 +1,18 @@
|
||||
// react global
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
// components
|
||||
import { AppBar, Toolbar, Grid } from '@mui/material';
|
||||
import { isAuthenticated, isAuthenticationEnabled, logoutUser } from '../../utilities/authUtilities';
|
||||
|
||||
// components
|
||||
import { AppBar, Toolbar, Grid, Button } from '@mui/material';
|
||||
import SearchSuggestion from './SearchSuggestion';
|
||||
import UserAccountMenu from './UserAccountMenu';
|
||||
// styling
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import logo from '../../assets/zotLogoWhite.svg';
|
||||
import logoxs from '../../assets/zotLogoWhiteSmall.svg';
|
||||
import githubLogo from '../../assets/Git.png';
|
||||
import { useState, useEffect } from 'react';
|
||||
import SearchSuggestion from './SearchSuggestion';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
barOpen: {
|
||||
@ -130,6 +131,10 @@ function Header({ setSearchCurrentValue = () => {} }) {
|
||||
const classes = useStyles();
|
||||
const path = useLocation().pathname;
|
||||
|
||||
const handleSignInClick = () => {
|
||||
logoutUser();
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '5rem' }}>
|
||||
<Toolbar className={classes.header}>
|
||||
@ -168,6 +173,18 @@ function Header({ setSearchCurrentValue = () => {} }) {
|
||||
<img alt="github repository" src={githubLogo} className={classes.logo} />
|
||||
</a>
|
||||
</Grid>
|
||||
{isAuthenticated() && isAuthenticationEnabled() && (
|
||||
<Grid item>
|
||||
<UserAccountMenu />
|
||||
</Grid>
|
||||
)}
|
||||
{!isAuthenticated() && isAuthenticationEnabled() && (
|
||||
<Grid item>
|
||||
<Button className={classes.signInBtn} onClick={handleSignInClick}>
|
||||
Sign in
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Toolbar>
|
||||
|
46
src/components/Header/UserAccountMenu.jsx
Normal file
46
src/components/Header/UserAccountMenu.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Menu, MenuItem, IconButton, Avatar, Divider } from '@mui/material';
|
||||
|
||||
import { getLoggedInUser, logoutUser } from '../../utilities/authUtilities';
|
||||
|
||||
function UserAccountMenu() {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const openMenu = Boolean(anchorEl);
|
||||
|
||||
const handleUserClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleUserClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={handleUserClick}
|
||||
size="small"
|
||||
aria-controls={open ? 'account-menu' : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
>
|
||||
<Avatar sx={{ width: 32, height: 32 }} />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={openMenu}
|
||||
onClose={handleUserClose}
|
||||
onClick={handleUserClose}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
>
|
||||
<MenuItem onClick={handleUserClose}>{getLoggedInUser()}</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem onClick={logoutUser}>Log out</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserAccountMenu;
|
@ -1,33 +1,35 @@
|
||||
// react global
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { host } from '../../host';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../api';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { host } from '../../host';
|
||||
import { isEmpty, isObject } from 'lodash';
|
||||
|
||||
// components
|
||||
import { Card, CardContent, CssBaseline } from '@mui/material';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Box from '@mui/material/Box';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import TermsOfService from './TermsOfService';
|
||||
import Loading from '../Shared/Loading';
|
||||
|
||||
import { GoogleLoginButton, GithubLoginButton, DexLoginButton } from './ThirdPartyLoginComponents';
|
||||
|
||||
// styling
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import { Card, CardContent } from '@mui/material';
|
||||
import Loading from '../Shared/Loading';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
cardContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto',
|
||||
padding: '0.625rem',
|
||||
position: 'relative'
|
||||
},
|
||||
loginCard: {
|
||||
@ -35,45 +37,82 @@ const useStyles = makeStyles(() => ({
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: '20%',
|
||||
width: '60%',
|
||||
height: '60%',
|
||||
background: '#FFFFFF',
|
||||
gap: '0.625em',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.5rem',
|
||||
borderRadius: '0.75rem',
|
||||
minWidth: '30rem'
|
||||
},
|
||||
loginCardContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: '0.1875rem black',
|
||||
maxWidth: '73%',
|
||||
height: '90%'
|
||||
width: '100%',
|
||||
padding: '3rem'
|
||||
},
|
||||
text: {
|
||||
color: '#14191F',
|
||||
width: '100%',
|
||||
fontSize: '1.5rem'
|
||||
fontSize: '1.5rem',
|
||||
lineHeight: '2.25rem',
|
||||
letterSpacing: '-0.01rem',
|
||||
marginBottom: '0.25rem'
|
||||
},
|
||||
subtext: {
|
||||
color: '#52637A',
|
||||
width: '100%',
|
||||
fontSize: '1rem'
|
||||
fontSize: '1rem',
|
||||
marginBottom: '2.375rem'
|
||||
},
|
||||
textField: {
|
||||
borderRadius: '0.25rem'
|
||||
borderRadius: '0.25rem',
|
||||
marginTop: 0,
|
||||
marginBottom: '1.5rem'
|
||||
},
|
||||
button: {
|
||||
textColor: {
|
||||
color: '#8596AD'
|
||||
},
|
||||
labelColor: {
|
||||
color: '#667C99',
|
||||
'&:focused': {
|
||||
color: '#667C99'
|
||||
}
|
||||
},
|
||||
continueButton: {
|
||||
textTransform: 'none',
|
||||
color: '##FFFFFF',
|
||||
fontSize: '1.4375rem',
|
||||
fontWeight: '500',
|
||||
background: '#F15527',
|
||||
color: '#FFFFFF',
|
||||
fontSize: '1.438rem',
|
||||
fontWeight: '600',
|
||||
height: '3.125rem',
|
||||
borderRadius: '0.25rem',
|
||||
letterSpacing: '0.01rem'
|
||||
letterSpacing: '0.01rem',
|
||||
marginBottom: '1rem',
|
||||
padding: 0,
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '#F15527',
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
continueAsGuestButton: {
|
||||
textTransform: 'none',
|
||||
background: '#FFFFFF',
|
||||
color: '#52637A',
|
||||
fontSize: '1.438rem',
|
||||
fontWeight: '600',
|
||||
height: '3.125rem',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #52637A',
|
||||
letterSpacing: '0.01rem',
|
||||
marginBottom: '1rem',
|
||||
padding: 0,
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFFFFF',
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
gitLogo: {
|
||||
height: '24px',
|
||||
@ -95,6 +134,15 @@ const useStyles = makeStyles(() => ({
|
||||
fontWeight: '400',
|
||||
paddingLeft: '1rem',
|
||||
paddingRight: '1rem'
|
||||
},
|
||||
divider: {
|
||||
color: '#C2CBD6',
|
||||
marginBottom: '2rem',
|
||||
width: '100%'
|
||||
},
|
||||
thirdPartyLoginContainer: {
|
||||
width: '100%',
|
||||
marginBottom: '2rem'
|
||||
}
|
||||
}));
|
||||
|
||||
@ -106,6 +154,8 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
const [requestProcessing, setRequestProcessing] = useState(false);
|
||||
const [requestError, setRequestError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [authMethods, setAuthMethods] = useState({});
|
||||
const [isGuestLoginEnabled, setIsGuestLoginEnabled] = useState(false);
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
const navigate = useNavigate();
|
||||
const classes = useStyles();
|
||||
@ -121,14 +171,28 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
.get(`${host()}${endpoints.authConfig}`, abortController.signal)
|
||||
.then((response) => {
|
||||
if (response.data?.http && isEmpty(response.data?.http?.auth)) {
|
||||
localStorage.setItem('token', '-');
|
||||
localStorage.setItem('authConfig', '{}');
|
||||
setIsLoggedIn(true);
|
||||
navigate('/home');
|
||||
} else if (response.data?.http?.auth) {
|
||||
setAuthMethods(response.data?.http?.auth);
|
||||
localStorage.setItem('authConfig', JSON.stringify(response.data?.http?.auth));
|
||||
setIsLoading(false);
|
||||
wrapperSetLoading(false);
|
||||
api
|
||||
.get(`${host()}${endpoints.status}`)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setIsGuestLoginEnabled(true);
|
||||
}
|
||||
})
|
||||
.catch(() => console.log('could not obtain guest login status'));
|
||||
}
|
||||
setIsLoading(false);
|
||||
wrapperSetLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setIsLoading(false);
|
||||
wrapperSetLoading(false);
|
||||
});
|
||||
@ -138,22 +202,20 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
const handleBasicAuth = () => {
|
||||
setRequestProcessing(true);
|
||||
let cfg = {};
|
||||
const token = btoa(username + ':' + password);
|
||||
cfg = {
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`
|
||||
}
|
||||
},
|
||||
withCredentials: host() !== window?.location?.origin
|
||||
};
|
||||
api
|
||||
.get(`${host()}/v2/`, abortController.signal, cfg)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
const token = btoa(username + ':' + password);
|
||||
localStorage.setItem('token', token);
|
||||
setRequestProcessing(false);
|
||||
setRequestError(false);
|
||||
setIsLoggedIn(true);
|
||||
@ -166,11 +228,34 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
if (Object.keys(authMethods).includes('htpasswd')) {
|
||||
handleBasicAuth();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGuestClick = () => {
|
||||
setRequestProcessing(false);
|
||||
setRequestError(false);
|
||||
setIsLoggedIn(true);
|
||||
navigate('/home');
|
||||
};
|
||||
|
||||
const handleClickExternalLogin = (event, provider) => {
|
||||
event.preventDefault();
|
||||
window.location.replace(
|
||||
`${host()}${endpoints.openidAuth}?callback_ui=${encodeURIComponent(
|
||||
window?.location?.origin
|
||||
)}/home&provider=${provider}`
|
||||
);
|
||||
};
|
||||
|
||||
const handleChange = (event, type) => {
|
||||
event.preventDefault();
|
||||
setRequestError(false);
|
||||
|
||||
const val = event.target.value;
|
||||
const val = event.target?.value;
|
||||
const isEmpty = val === '';
|
||||
|
||||
switch (type) {
|
||||
@ -195,8 +280,24 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
}
|
||||
};
|
||||
|
||||
const renderThirdPartyLoginMethods = () => {
|
||||
let isGoogle = isObject(authMethods.openid?.providers?.google);
|
||||
// let isGitlab = isObject(authMethods.openid?.providers?.gitlab);
|
||||
let isGithub = isObject(authMethods.openid?.providers?.github);
|
||||
let isDex = isObject(authMethods.openid?.providers?.dex);
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing="1rem" className={classes.thirdPartyLoginContainer}>
|
||||
{isGithub && <GithubLoginButton handleClick={handleClickExternalLogin} />}
|
||||
{isGoogle && <GoogleLoginButton handleClick={handleClickExternalLogin} />}
|
||||
{/* {isGitlab && <GitlabLoginButton handleClick={handleClickExternalLogin} />} */}
|
||||
{isDex && <DexLoginButton handleClick={handleClickExternalLogin} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={classes.cardContainer} data-testid="signin-container">
|
||||
<div className={classes.cardContainer} data-testid="signin-container">
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
@ -204,68 +305,70 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
||||
<CardContent className={classes.loginCardContent}>
|
||||
<CssBaseline />
|
||||
<Typography align="left" className={classes.text} component="h1" variant="h4">
|
||||
Sign in
|
||||
Sign In
|
||||
</Typography>
|
||||
<Typography align="left" className={classes.subtext} variant="body1" gutterBottom>
|
||||
Welcome back! Please enter your details.
|
||||
</Typography>
|
||||
|
||||
<Box component="form" onSubmit={null} noValidate autoComplete="off" sx={{ mt: 1 }}>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="username"
|
||||
label="Username"
|
||||
name="username"
|
||||
className={classes.textField}
|
||||
onInput={(e) => handleChange(e, 'username')}
|
||||
error={usernameError != null}
|
||||
helperText={usernameError}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Enter password"
|
||||
type="password"
|
||||
id="password"
|
||||
className={classes.textField}
|
||||
onInput={(e) => handleChange(e, 'password')}
|
||||
error={passwordError != null}
|
||||
helperText={passwordError}
|
||||
/>
|
||||
{requestProcessing && <CircularProgress style={{ marginTop: 20 }} color="secondary" />}
|
||||
{requestError && (
|
||||
<Alert style={{ marginTop: 20 }} severity="error">
|
||||
Authentication Failed. Please try again.
|
||||
</Alert>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
{renderThirdPartyLoginMethods()}
|
||||
{Object.keys(authMethods).length > 1 && <Divider className={classes.divider}>or</Divider>}
|
||||
{Object.keys(authMethods).includes('htpasswd') && (
|
||||
<Box component="form" onSubmit={null} noValidate autoComplete="off">
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
variant="contained"
|
||||
className={classes.button}
|
||||
sx={{
|
||||
mt: 3,
|
||||
mb: 1,
|
||||
background: '#1479FF',
|
||||
'&:hover': {
|
||||
backgroundColor: '#1565C0'
|
||||
}
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{' '}
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
<TermsOfService sx={{ mt: 2, mb: 4 }} />
|
||||
id="username"
|
||||
label="Username"
|
||||
name="username"
|
||||
className={classes.textField}
|
||||
inputProps={{ className: classes.textColor }}
|
||||
InputLabelProps={{ className: classes.labelColor }}
|
||||
onInput={(e) => handleChange(e, 'username')}
|
||||
error={usernameError != null}
|
||||
helperText={usernameError}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Enter password"
|
||||
type="password"
|
||||
id="password"
|
||||
className={classes.textField}
|
||||
inputProps={{ className: classes.textColor }}
|
||||
InputLabelProps={{ className: classes.labelColor }}
|
||||
onInput={(e) => handleChange(e, 'password')}
|
||||
error={passwordError != null}
|
||||
helperText={passwordError}
|
||||
/>
|
||||
{requestProcessing && <CircularProgress style={{ marginTop: 20 }} color="secondary" />}
|
||||
{requestError && (
|
||||
<Alert style={{ marginTop: 20 }} severity="error">
|
||||
Authentication Failed. Please try again.
|
||||
</Alert>
|
||||
)}
|
||||
<div>
|
||||
<Button fullWidth variant="contained" className={classes.continueButton} onClick={handleClick}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
{isGuestLoginEnabled && (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
className={classes.continueAsGuestButton}
|
||||
onClick={handleGuestClick}
|
||||
>
|
||||
Continue as guest
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,55 +1,52 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import logoWhite from '../../assets/Zot-white.svg';
|
||||
import loginDrawing from '../../assets/codeReviewSignIn.png';
|
||||
import backgroundImage from '../../assets/backgroundSignIn.png';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
import logoWhite from '../../assets/zotLogoWhiteHorizontal.svg';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
backgroundImage: `url(${backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
minHeight: '100%',
|
||||
alignItems: 'center'
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
contentContainer: {
|
||||
width: '51%',
|
||||
height: '22%'
|
||||
},
|
||||
logoContainer: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
logo: {
|
||||
maxHeight: 96,
|
||||
maxWidth: 320,
|
||||
marginTop: '17%'
|
||||
},
|
||||
loginDrawing: {
|
||||
maxHeight: 298,
|
||||
maxWidth: 464,
|
||||
marginTop: '4%'
|
||||
width: '64%'
|
||||
},
|
||||
mainText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 700,
|
||||
maxWidth: '45%',
|
||||
marginTop: '4%',
|
||||
fontSize: '2.5rem'
|
||||
},
|
||||
captionText: {
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
maxWidth: '48%',
|
||||
marginTop: '2%',
|
||||
fontSize: '1.1875rem'
|
||||
color: '#F6F7F9',
|
||||
fontWeight: '700',
|
||||
width: '100%',
|
||||
fontSize: '2.5rem',
|
||||
lineHeight: '3rem'
|
||||
}
|
||||
}));
|
||||
|
||||
export default function SigninPresentation() {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Stack spacing={0} className={classes.container} data-testid="presentation-container">
|
||||
<img src={logoWhite} alt="zot logo" className={classes.logo}></img>
|
||||
<Typography variant="h2" className={classes.mainText}>
|
||||
Welcome to our repository
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.captionText}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Amet, dis pellentesque posuere nulla tortor ac eu arcu
|
||||
nunc.
|
||||
</Typography>
|
||||
<img src={loginDrawing} alt="drawing" className={classes.loginDrawing}></img>
|
||||
</Stack>
|
||||
<div className={classes.container}>
|
||||
<Stack spacing={'3rem'} className={classes.contentContainer} data-testid="presentation-container">
|
||||
<div className={classes.logoContainer}>
|
||||
<img src={logoWhite} alt="zot logo" className={classes.logo}></img>
|
||||
</div>
|
||||
<Typography variant="h2" className={classes.mainText}>
|
||||
OCI-native container image registry, simplified
|
||||
</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -26,8 +26,8 @@ export default function TermsOfService(props) {
|
||||
return (
|
||||
<Stack spacing={0}>
|
||||
<Typography variant="caption" className={classes.subtext} align="justify" {...props} pb={6}>
|
||||
By creating an account, you agree to the Terms of Service. For more information about our privacy practices, see
|
||||
the ZOT's Privacy Policy.
|
||||
By using zot UI, you agree to the Terms of Service. For more information about our privacy practices, see
|
||||
zot's Privacy Policy.
|
||||
</Typography>
|
||||
<Typography variant="caption" className={classes.text} align="center" {...props}>
|
||||
Privacy Policy | Terms of Service
|
||||
|
93
src/components/Login/ThirdPartyLoginComponents.jsx
Normal file
93
src/components/Login/ThirdPartyLoginComponents.jsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
|
||||
import Button from '@mui/material/Button';
|
||||
import SvgIcon from '@mui/material/SvgIcon';
|
||||
import githubLogo from '../../assets/GhIcon.svg';
|
||||
|
||||
// styling
|
||||
import { makeStyles } from '@mui/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
githubButton: {
|
||||
textTransform: 'none',
|
||||
background: '#161614',
|
||||
color: '#FFFFFF',
|
||||
borderRadius: '0.25rem',
|
||||
padding: 0,
|
||||
height: '3.125rem',
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '#161614',
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
googleButton: {
|
||||
textTransform: 'none',
|
||||
background: '#FFFFFF',
|
||||
color: '#52637A',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #52637A',
|
||||
padding: 0,
|
||||
height: '3.125rem',
|
||||
boxShadow: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFFFFF',
|
||||
boxShadow: 'none'
|
||||
}
|
||||
},
|
||||
buttonsText: {
|
||||
lineHeight: '2.125rem',
|
||||
height: '2.125rem',
|
||||
fontSize: '1.438rem',
|
||||
fontWeight: '600',
|
||||
letterSpacing: '0.01rem'
|
||||
}
|
||||
}));
|
||||
|
||||
function GithubLoginButton({ handleClick }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
className={classes.githubButton}
|
||||
endIcon={<SvgIcon fontSize="medium">{githubLogo}</SvgIcon>}
|
||||
onClick={(e) => handleClick(e, 'github')}
|
||||
>
|
||||
<span className={classes.buttonsText}>Continue with Github</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function GoogleLoginButton({ handleClick }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Button fullWidth variant="contained" className={classes.googleButton} onClick={(e) => handleClick(e, 'google')}>
|
||||
<span className={classes.buttonsText}>Continue with Google</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function GitlabLoginButton({ handleClick }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Button fullWidth variant="contained" className={classes.button} onClick={(e) => handleClick(e, 'gitlab')}>
|
||||
Sign in with Gitlab
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function DexLoginButton({ handleClick }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Button fullWidth variant="contained" className={classes.button} onClick={(e) => handleClick(e, 'dex')}>
|
||||
Sign in with Dex
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { GithubLoginButton, GoogleLoginButton, GitlabLoginButton, DexLoginButton };
|
@ -15,6 +15,11 @@ const useStyles = makeStyles(() => ({
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#F6F7F9'
|
||||
},
|
||||
signinContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
loadingHidden: {
|
||||
display: 'none'
|
||||
}
|
||||
@ -27,10 +32,16 @@ function LoginPage({ isLoggedIn, setIsLoggedIn }) {
|
||||
return (
|
||||
<Grid container spacing={0} className={classes.container} data-testid="login-container">
|
||||
{isLoading && <Loading />}
|
||||
<Grid item xs={6} className={isLoading ? classes.loadingHidden : ''}>
|
||||
<Grid item xs={1} md={6} className={`${isLoading ? classes.loadingHidden : ''} hide-on-small`}>
|
||||
<SigninPresentation />
|
||||
</Grid>
|
||||
<Grid item xs={6} className={isLoading ? classes.loadingHidden : ''}>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
sm={12}
|
||||
md={6}
|
||||
className={`${classes.signinContainer} ${isLoading ? classes.loadingHidden : ''}`}
|
||||
>
|
||||
<SignIn isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} wrapperSetLoading={setIsLoading} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@ -1,5 +1,50 @@
|
||||
const isAuthenticated = () => {
|
||||
return localStorage.getItem('token') !== '-';
|
||||
import { isNil } from 'lodash';
|
||||
import { host } from '../host';
|
||||
import { api, endpoints } from '../api';
|
||||
|
||||
const getCookie = (name) => document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`))?.at(2);
|
||||
|
||||
const deleteCookie = (name, path, domain) => {
|
||||
if (getCookie(name)) {
|
||||
document.cookie =
|
||||
name +
|
||||
'=' +
|
||||
(path ? ';path=' + path : '') +
|
||||
(domain ? ';domain=' + domain : '') +
|
||||
';expires=Thu, 01 Jan 1970 00:00:01 GMT';
|
||||
}
|
||||
};
|
||||
|
||||
export { isAuthenticated };
|
||||
const logoutUser = () => {
|
||||
localStorage.clear();
|
||||
api
|
||||
.post(`${host()}${endpoints.logout}`)
|
||||
.then(() => {
|
||||
deleteCookie('user');
|
||||
window.location.replace('/login');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
const isAuthenticated = () => {
|
||||
const loggedIn = getCookie('user');
|
||||
if (loggedIn) return true;
|
||||
const authState = JSON.parse(localStorage.getItem('authConfig'));
|
||||
if (isNil(authState)) return false;
|
||||
if (Object.keys(authState).length === 0) return true;
|
||||
};
|
||||
|
||||
const isAuthenticationEnabled = () => {
|
||||
const authMethods = JSON.parse(localStorage.getItem('authConfig')) || {};
|
||||
return Object.keys(authMethods).length > 0;
|
||||
};
|
||||
|
||||
const getLoggedInUser = () => {
|
||||
const userCookie = getCookie('user');
|
||||
if (!userCookie) return null;
|
||||
return userCookie;
|
||||
};
|
||||
|
||||
export { isAuthenticated, isAuthenticationEnabled, getLoggedInUser, logoutUser };
|
||||
|
@ -1,4 +1,14 @@
|
||||
images:
|
||||
- name: ubuntu
|
||||
tags:
|
||||
- "18.04"
|
||||
- "bionic-20230301"
|
||||
- "bionic"
|
||||
- "22.04"
|
||||
- "jammy-20230301"
|
||||
- "jammy"
|
||||
- "latest"
|
||||
multiarch: ""
|
||||
- name: alpine
|
||||
tags:
|
||||
- "3.17"
|
||||
@ -20,16 +30,6 @@ images:
|
||||
- "3.14"
|
||||
- "3.14.9"
|
||||
multiarch: "all"
|
||||
- name: ubuntu
|
||||
tags:
|
||||
- "18.04"
|
||||
- "bionic-20230301"
|
||||
- "bionic"
|
||||
- "22.04"
|
||||
- "jammy-20230301"
|
||||
- "jammy"
|
||||
- "latest"
|
||||
multiarch: ""
|
||||
- name: debian
|
||||
tags:
|
||||
- "bullseye-slim"
|
||||
|
83
tests/explore.spec.js
Normal file
83
tests/explore.spec.js
Normal file
@ -0,0 +1,83 @@
|
||||
// @ts-check
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { scroll } from './utils/scroll';
|
||||
import { getRepoCardNameForLocator, getRepoListOrderedAlpha } from './utils/test-data-parser';
|
||||
import { hosts, endpoints, sortCriteria } from './values/test-constants';
|
||||
|
||||
test.describe('explore page test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('token', '-');
|
||||
});
|
||||
});
|
||||
|
||||
test('explore data', async ({ page }) => {
|
||||
const expectedRequest = `${hosts.api}${endpoints.globalSearch('', sortCriteria.relevance, 1)}`;
|
||||
const exploreDataRequest = page.waitForRequest(
|
||||
(request) => request.url() === expectedRequest && request.method() === 'GET'
|
||||
);
|
||||
await page.goto(`${hosts.ui}/explore?search=`);
|
||||
const expectDataResponse = await exploreDataRequest;
|
||||
expect(expectDataResponse).toBeTruthy();
|
||||
|
||||
// if no search query provided and no filters selected, data should be alphabetical when sorted by relevance
|
||||
const alphaOrderedData = getRepoListOrderedAlpha();
|
||||
|
||||
const exploreFirst = page.getByRole('button', {
|
||||
name: getRepoCardNameForLocator(alphaOrderedData[0])
|
||||
});
|
||||
|
||||
const exploreSecond = page.getByRole('button', {
|
||||
name: getRepoCardNameForLocator(alphaOrderedData[1])
|
||||
});
|
||||
|
||||
await expect(exploreFirst).toBeVisible({ timeout: 250000 });
|
||||
await expect(exploreSecond).toBeVisible({ timeout: 250000 });
|
||||
|
||||
const exploreNextPageRequest = page.waitForRequest(
|
||||
(request) =>
|
||||
request.url() === `${hosts.api}${endpoints.globalSearch('', sortCriteria.relevance, 2)}` &&
|
||||
request.method() === 'GET'
|
||||
);
|
||||
await page.evaluate(scroll, { direction: 'down', speed: 'fast' });
|
||||
const exploreNextPageResponse = await exploreNextPageRequest;
|
||||
expect(exploreNextPageResponse).toBeTruthy();
|
||||
|
||||
const postScrollExploreElementOne = page.getByRole('button', {
|
||||
name: getRepoCardNameForLocator(alphaOrderedData[alphaOrderedData.length - 1])
|
||||
});
|
||||
const postScrollExploreElementTwo = page.getByRole('button', {
|
||||
name: getRepoCardNameForLocator(alphaOrderedData[alphaOrderedData.length - 2])
|
||||
});
|
||||
|
||||
await expect(postScrollExploreElementOne).toBeVisible({ timeout: 250000 });
|
||||
await expect(postScrollExploreElementTwo).toBeVisible({ timeout: 250000 });
|
||||
});
|
||||
|
||||
test('explore filtering', async ({ page }) => {
|
||||
const alphaOrderedData = getRepoListOrderedAlpha();
|
||||
|
||||
await page.goto(`${hosts.ui}/explore?search=`);
|
||||
const exploreFirst = page.getByRole('button', {
|
||||
name: getRepoCardNameForLocator(alphaOrderedData[0])
|
||||
});
|
||||
|
||||
const exploreSecond = page.getByRole('button', {
|
||||
name: getRepoCardNameForLocator(alphaOrderedData[1])
|
||||
});
|
||||
|
||||
await expect(exploreFirst).toBeVisible({ timeout: 250000 });
|
||||
await expect(exploreSecond).toBeVisible({ timeout: 250000 });
|
||||
|
||||
const linuxFilter = page.getByRole('checkbox', { name: 'linux' });
|
||||
await linuxFilter.check();
|
||||
|
||||
await expect(linuxFilter).toBeChecked();
|
||||
|
||||
await expect(exploreFirst).toBeVisible({ timeout: 250000 });
|
||||
|
||||
await linuxFilter.uncheck();
|
||||
await page.getByRole('checkbox', { name: 'windows' }).check();
|
||||
await expect(exploreFirst).not.toBeVisible({ timeout: 250000 });
|
||||
});
|
||||
});
|
25
tests/home.spec.js
Normal file
25
tests/home.spec.js
Normal file
@ -0,0 +1,25 @@
|
||||
// @ts-check
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { hosts, endpoints, sortCriteria } from './values/test-constants';
|
||||
|
||||
test.describe('homepage test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('token', '-');
|
||||
});
|
||||
});
|
||||
|
||||
test('homepage viewall navigation', async ({ page }) => {
|
||||
await page.goto(`${hosts.ui}/home`);
|
||||
const popularRequest = page.waitForRequest(
|
||||
(request) =>
|
||||
request.url() === `${hosts.api}${endpoints.globalSearch('', sortCriteria.downloads)}` &&
|
||||
request.method() === 'GET'
|
||||
);
|
||||
const viewAllButton = page.getByText('View all').first();
|
||||
await viewAllButton.click();
|
||||
const popularResponse = await popularRequest;
|
||||
expect(popularResponse).toBeTruthy();
|
||||
await expect(page).toHaveURL(`${hosts.ui}/explore?sortby=${sortCriteria.downloads}`);
|
||||
});
|
||||
});
|
38
tests/navbar.spec.js
Normal file
38
tests/navbar.spec.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { hosts, endpoints, sortCriteria } from './values/test-constants';
|
||||
import { getRepoListOrderedAlpha } from './utils/test-data-parser';
|
||||
|
||||
test.describe('navbar test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('token', '-');
|
||||
});
|
||||
});
|
||||
|
||||
test('nav search', async ({ page }) => {
|
||||
const alphaOrderedData = getRepoListOrderedAlpha();
|
||||
await page.goto(`${hosts.ui}/home`);
|
||||
// search results
|
||||
const searchRequest = page.waitForRequest(
|
||||
(request) =>
|
||||
request.url() ===
|
||||
`${hosts.api}${endpoints.globalSearch(
|
||||
alphaOrderedData[0].repo.substring(0, 3),
|
||||
sortCriteria.relevance,
|
||||
1,
|
||||
9
|
||||
)}` && request.method() === 'GET'
|
||||
);
|
||||
await page.getByPlaceholder('Search for content...').click();
|
||||
await page.getByPlaceholder('Search for content...').fill(alphaOrderedData[0].repo.substring(0, 3));
|
||||
const searchResponse = await searchRequest;
|
||||
expect(searchResponse).toBeTruthy();
|
||||
const searchSuggestion = await page.getByRole('option', { name: alphaOrderedData[0].repo });
|
||||
await expect(searchSuggestion).toBeVisible({ timeout: 100000 });
|
||||
|
||||
// clicking a search result
|
||||
|
||||
await searchSuggestion.click();
|
||||
await expect(page).toHaveURL(/.*\/image.*/);
|
||||
});
|
||||
});
|
48
tests/repo.spec.js
Normal file
48
tests/repo.spec.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { hosts, endpoints } from './values/test-constants';
|
||||
import { getMultiTagRepo } from './utils/test-data-parser';
|
||||
import { head } from 'lodash';
|
||||
|
||||
const testRepo = getMultiTagRepo();
|
||||
|
||||
test.describe('Repository page test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('token', '-');
|
||||
});
|
||||
|
||||
await page.goto(`${hosts.ui}/image/${testRepo.repo}`);
|
||||
});
|
||||
|
||||
test('Repository page data', async ({ page }) => {
|
||||
// check metadata
|
||||
const firstTag = head(testRepo.tags);
|
||||
await expect(page.getByText(firstTag.description).first()).toBeVisible({ timeout: 100000 });
|
||||
await expect(page.getByText(firstTag.source).first()).toBeVisible({ timeout: 100000 });
|
||||
|
||||
// check tags and tags search
|
||||
for (let tag of testRepo.tags) {
|
||||
await expect(page.getByText(tag.tag, { exact: true })).toBeVisible({ timeout: 100000 });
|
||||
}
|
||||
await page.getByText('Show more').first().click();
|
||||
await expect(page.getByText('linux/amd64')).toBeVisible({ timeout: 100000 });
|
||||
await page.getByPlaceholder('Search tags...').click();
|
||||
await page.getByPlaceholder('Search tags...').fill(testRepo.tags[0].tag);
|
||||
await expect(page.getByText(testRepo.tags[0].tag, { exact: true })).toBeVisible({ timeout: 100000 });
|
||||
await expect(page.getByText(testRepo.tags[1].tag, { exact: true })).not.toBeVisible({ timeout: 100000 });
|
||||
});
|
||||
|
||||
test('Repository page navigation', async ({ page }) => {
|
||||
await expect(page.getByText(testRepo.tags[0].tag, { exact: true })).toBeVisible({ timeout: 100000 });
|
||||
const tagPageRequest = page.waitForRequest(
|
||||
(request) =>
|
||||
request.url() === `${hosts.api}${endpoints.image(`${testRepo.repo}:${testRepo.tags[0].tag}`)}` &&
|
||||
request.method() === 'GET'
|
||||
);
|
||||
await page.getByText(testRepo.tags[0].tag, { exact: true }).click();
|
||||
await expect(tagPageRequest).toBeDefined();
|
||||
const tagPageResponse = await tagPageRequest;
|
||||
expect(tagPageResponse).toBeTruthy();
|
||||
await expect(page).toHaveURL(/.*\/image\/.+\/tag\/.*/);
|
||||
});
|
||||
});
|
@ -185,7 +185,7 @@ if [ $? -eq 0 ]; then
|
||||
echo "Image ${local_image_ref_skopeo} found locally"
|
||||
else
|
||||
echo "Image ${local_image_ref_skopeo} will be copied"
|
||||
skopeo --insecure-policy copy --format=oci ${multiarch_arg} ${remote_src_image_ref} ${local_image_ref_skopeo}
|
||||
skopeo --insecure-policy --override-os="linux" --override-arch="amd64" copy --override-os="linux" --override-arch="amd64" --format=oci ${multiarch_arg} ${remote_src_image_ref} ${local_image_ref_skopeo}
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@ -206,7 +206,7 @@ if [ ! -z "${username}" ]; then
|
||||
fi
|
||||
|
||||
# Upload image to target registry
|
||||
skopeo copy --dest-tls-verify=false ${multiarch_arg} ${credentials_args} ${local_image_ref_skopeo} docker://${remote_dest_image_ref}
|
||||
skopeo --override-os="linux" --override-arch="amd64" copy --dest-tls-verify=false ${multiarch_arg} ${credentials_args} ${local_image_ref_skopeo} docker://${remote_dest_image_ref}
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
43
tests/tag.spec.js
Normal file
43
tests/tag.spec.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { getTagWithDependencies, getTagWithDependents, getTagWithVulnerabilities } from './utils/test-data-parser';
|
||||
import { hosts, pageSizes } from './values/test-constants';
|
||||
|
||||
test.describe('Tag page test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('token', '-');
|
||||
});
|
||||
});
|
||||
|
||||
test('Tag page with dependents', async ({ page }) => {
|
||||
const tagWithDependents = getTagWithDependents();
|
||||
await page.goto(`${hosts.ui}/image/${tagWithDependents.title}/tag/${tagWithDependents.tag}`);
|
||||
await expect(page.getByRole('tab', { name: 'Layers' })).toBeVisible({ timeout: 100000 });
|
||||
await page.getByRole('tab', { name: 'Layers' }).click();
|
||||
await expect(page.getByTestId('layer-card-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
||||
await page.getByRole('tab', { name: 'Used by' }).click();
|
||||
await expect(page.getByTestId('dependents-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
||||
await expect(page.getByText('Tag').nth(1)).toBeVisible({ timeout: 100000 });
|
||||
await expect(await page.getByText('Tag').count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Tag page with dependencies', async ({ page }) => {
|
||||
const tagWithDependencies = getTagWithDependencies();
|
||||
await page.goto(`${hosts.ui}/image/${tagWithDependencies.title}/tag/${tagWithDependencies.tag}`);
|
||||
await expect(page.getByRole('tab', { name: 'Layers' })).toBeVisible({ timeout: 100000 });
|
||||
await page.getByRole('tab', { name: 'Layers' }).click();
|
||||
await expect(page.getByTestId('layer-card-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
||||
await page.getByRole('tab', { name: 'Uses' }).click();
|
||||
await expect(page.getByTestId('depends-on-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
||||
await expect(page.getByText('Tag')).toHaveCount(1, { timeout: 100000 });
|
||||
});
|
||||
|
||||
test('Tag page with vulnerabilities', async ({ page }) => {
|
||||
const tagWithVulnerabilities = getTagWithVulnerabilities();
|
||||
await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`);
|
||||
await page.getByRole('tab', { name: 'Vulnerabilities' }).click();
|
||||
await expect(page.getByTestId('vulnerability-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
|
||||
await expect(await page.getByText('CVE-').count()).toBeGreaterThan(0);
|
||||
await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
|
||||
});
|
||||
});
|
14
tests/utils/scroll.js
Normal file
14
tests/utils/scroll.js
Normal file
@ -0,0 +1,14 @@
|
||||
export const scroll = async (args) => {
|
||||
const { direction, speed } = args;
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const scrollHeight = () => document.body.scrollHeight;
|
||||
const start = direction === 'down' ? 0 : scrollHeight();
|
||||
const shouldStop = (position) => (direction === 'down' ? position > scrollHeight() : position < 0);
|
||||
const increment = direction === 'down' ? 100 : -100;
|
||||
const delayTime = speed === 'slow' ? 50 : 10;
|
||||
console.error(start, shouldStop(start), increment);
|
||||
for (let i = start; !shouldStop(i); i += increment) {
|
||||
window.scrollTo(0, i);
|
||||
await delay(delayTime);
|
||||
}
|
||||
};
|
163
tests/utils/test-data-parser.js
Normal file
163
tests/utils/test-data-parser.js
Normal file
@ -0,0 +1,163 @@
|
||||
// read raw test data and get expected result for different queries
|
||||
import { isNil } from 'lodash';
|
||||
import * as rawData from '../data/image_metadata.json';
|
||||
|
||||
const rawDataToRepo = ([rawDataRepoKey, rawDataRepoValue]) => {
|
||||
if (rawDataRepoKey === 'default') return;
|
||||
return {
|
||||
repo: rawDataRepoKey,
|
||||
tags: Object.entries(rawDataRepoValue).map(([key, value]) => ({
|
||||
tag: key,
|
||||
title: value['org.opencontainers.image.title'],
|
||||
description: value['org.opencontainers.image.description'],
|
||||
url: value['org.opencontainers.image.url'],
|
||||
source: value['org.opencontainers.image.source'],
|
||||
license: value['org.opencontainers.image.licenses'],
|
||||
vendor: value['org.opencontainers.image.vendor'],
|
||||
documentation: value['org.opencontainers.image.documentation'],
|
||||
manifests: value['manifests'],
|
||||
cves: value['cves']
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
const getManifestDependents = (manifestValue, repoName) => {
|
||||
const dependents = [];
|
||||
Object.entries(rawData)
|
||||
.map((repo) => rawDataToRepo(repo))
|
||||
.forEach((repo) => {
|
||||
// if different repo
|
||||
repo?.tags.forEach((tag) => {
|
||||
if (tag.title !== repoName) {
|
||||
Object.values(tag?.manifests).forEach((value) => {
|
||||
if (value.layers?.length > manifestValue.layers?.length) {
|
||||
if (manifestValue.layers?.every((i) => value.layers?.includes(i))) {
|
||||
dependents.push(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return dependents;
|
||||
};
|
||||
|
||||
const getManifestDependencies = (manifestValue, repoName) => {
|
||||
const dependencies = [];
|
||||
Object.entries(rawData)
|
||||
.map((repo) => rawDataToRepo(repo))
|
||||
.forEach((repo) => {
|
||||
repo?.tags.forEach((tag) => {
|
||||
// if different repo
|
||||
if (tag.title !== repoName) {
|
||||
Object.values(tag?.manifests).forEach((value) => {
|
||||
if (value.layers?.length < manifestValue.layers?.length) {
|
||||
if (value.layers?.every((i) => manifestValue.layers?.includes(i))) {
|
||||
dependencies.push(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return dependencies;
|
||||
};
|
||||
|
||||
const getMultiTagRepo = () => {
|
||||
const multiTagImage = Object.entries(rawData)
|
||||
.find(([, value]) => Object.keys(value).length > 1)
|
||||
.filter((e) => !isNil(e));
|
||||
return rawDataToRepo(multiTagImage);
|
||||
};
|
||||
|
||||
const getTagWithDependents = (minSize = 0) => {
|
||||
const parsedRepoList = Object.entries(rawData)
|
||||
.map((repo) => rawDataToRepo(repo))
|
||||
.filter((e) => !isNil(e));
|
||||
let tagsArray = [];
|
||||
parsedRepoList.forEach((el) => (tagsArray = tagsArray.concat(el?.tags)));
|
||||
for (let tag of tagsArray) {
|
||||
if (!isNil(tag)) {
|
||||
const tagManifests = Object.values(tag?.manifests);
|
||||
const manifestWithDependent = tagManifests.findIndex(
|
||||
(manifest) => getManifestDependents(manifest, tag.title).length > minSize
|
||||
);
|
||||
if (manifestWithDependent !== -1) return tag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getTagWithDependencies = (minSize = 0) => {
|
||||
const parsedRepoList = Object.entries(rawData)
|
||||
.map((repo) => rawDataToRepo(repo))
|
||||
.filter((e) => !isNil(e));
|
||||
let tagsArray = [];
|
||||
parsedRepoList.forEach((el) => (tagsArray = tagsArray.concat(el?.tags)));
|
||||
for (let tag of tagsArray) {
|
||||
if (!isNil(tag)) {
|
||||
const tagManifests = Object.values(tag?.manifests);
|
||||
const manifestWithDependencies = tagManifests.findIndex(
|
||||
(manifest) => getManifestDependencies(manifest, tag.title).length > minSize
|
||||
);
|
||||
if (manifestWithDependencies !== -1) return tag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getTagWithVulnerabilities = () => {
|
||||
const parsedRepoList = Object.entries(rawData)
|
||||
.map((repo) => rawDataToRepo(repo))
|
||||
.filter((e) => !isNil(e));
|
||||
let tagsArray = [];
|
||||
parsedRepoList.forEach((el) => (tagsArray = tagsArray.concat(el?.tags)));
|
||||
const tagWithCves = tagsArray.find((tag) => Object.keys(tag.cves).length > 0);
|
||||
return tagWithCves;
|
||||
};
|
||||
|
||||
const getTagWithMultiarch = () => {
|
||||
const parsedRepoList = Object.entries(rawData)
|
||||
.map((repo) => rawDataToRepo(repo))
|
||||
.filter((e) => !isNil(e));
|
||||
let tagsArray = [];
|
||||
const tagsList = parsedRepoList.forEach((el) => tagsArray.concat(el?.tags));
|
||||
const tagWithMultiarch = tagsList.find((tag) => tag.multiarch === 'all');
|
||||
return tagWithMultiarch;
|
||||
};
|
||||
|
||||
const getRepoListOrderedAlpha = () => {
|
||||
const parsedRepoList = Object.entries(rawData)
|
||||
.map((repo) => rawDataToRepo(repo))
|
||||
.filter((e) => !isNil(e));
|
||||
parsedRepoList.sort((a, b) => a?.repo.localeCompare(b?.repo));
|
||||
return parsedRepoList;
|
||||
};
|
||||
|
||||
// Currently image metadata does not contain last update time for tags
|
||||
// const getLastUpdatedForRepo = (repo) => {
|
||||
// const debug = DateTime.max(...repo.tags.map((tag) => DateTime.fromISO(tag.lastUpdated)));
|
||||
// return debug;
|
||||
// };
|
||||
|
||||
// const getRepoListOrderedRecent = () => {
|
||||
// const parsedRepoList = Object.entries(rawData)
|
||||
// .map((repo) => rawDataToRepo(repo))
|
||||
// .filter((e) => !isNil(e));
|
||||
// parsedRepoList.sort((a, b) => getLastUpdatedForRepo(b).diff(getLastUpdatedForRepo(a)));
|
||||
// return parsedRepoList;
|
||||
// };
|
||||
|
||||
const getRepoCardNameForLocator = (repo) => {
|
||||
return `${repo?.repo} ${repo?.tags[0]?.description?.slice(0, 10)}`;
|
||||
};
|
||||
|
||||
export {
|
||||
getMultiTagRepo,
|
||||
getRepoListOrderedAlpha,
|
||||
getTagWithDependents,
|
||||
getTagWithDependencies,
|
||||
getTagWithVulnerabilities,
|
||||
getTagWithMultiarch,
|
||||
getRepoCardNameForLocator
|
||||
};
|
31
tests/values/test-constants.js
Normal file
31
tests/values/test-constants.js
Normal file
@ -0,0 +1,31 @@
|
||||
const hosts = {
|
||||
ui: process.env.UI_HOST ? `http://${process.env.UI_HOST}` : 'http://localhost:5000',
|
||||
api: process.env.API_HOST ? `http://${process.env.API_HOST}` : 'http://localhost:5000'
|
||||
};
|
||||
|
||||
const sortCriteria = {
|
||||
relevance: 'RELEVANCE',
|
||||
updateTime: 'UPDATE_TIME',
|
||||
alphabetic: 'ALPHABETIC_ASC',
|
||||
alphabeticDesc: 'ALPHABETIC_DSC',
|
||||
downloads: 'DOWNLOADS'
|
||||
};
|
||||
|
||||
const pageSizes = {
|
||||
EXPLORE: 10,
|
||||
HOME: 10
|
||||
};
|
||||
|
||||
const endpoints = {
|
||||
repoList: `/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage:%20{limit:15%20offset:0}){Results%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20%20Licenses%20Logo%20Title%20Source%20IsSigned%20Documentation%20Vendor%20Labels}%20DownloadCount}}}`,
|
||||
detailedRepoInfo: (name) =>
|
||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:%22${name}%22){Images%20{Manifests%20{Digest%20Platform%20{Os%20Arch}%20Size}%20Vulnerabilities%20{MaxSeverity%20Count}%20Tag%20LastUpdated%20Vendor%20}%20Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20NewestImage%20{RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20Manifests%20{Digest}%20Tag%20Title%20Documentation%20DownloadCount%20Source%20Description%20Licenses}}}}`,
|
||||
globalSearch: (searchTerm, sortCriteria, pageNumber = 1, pageSize = 10) =>
|
||||
`/v2/_zot/ext/search?query={GlobalSearch(query:%22${searchTerm}%22,%20requestedPage:%20{limit:${pageSize}%20offset:${
|
||||
10 * (pageNumber - 1)
|
||||
}%20sortBy:%20${sortCriteria}}%20)%20{Page%20{TotalCount%20ItemCount}%20Repos%20{Name%20LastUpdated%20Size%20Platforms%20{%20Os%20Arch%20}%20IsStarred%20IsBookmarked%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20IsSigned%20Licenses%20Vendor%20Labels%20}%20DownloadCount}}}`,
|
||||
image: (name) =>
|
||||
`/v2/_zot/ext/search?query={Image(image:%20%22${name}%22){RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20%20Referrers%20{MediaType%20ArtifactType%20Size%20Digest%20Annotations{Key%20Value}}%20Tag%20Manifests%20{History%20{Layer%20{Size%20Digest}%20HistoryDescription%20{CreatedBy%20EmptyLayer}}%20Digest%20ConfigDigest%20LastUpdated%20Size%20Platform%20{Os%20Arch}}%20Vendor%20Licenses%20}}`
|
||||
};
|
||||
|
||||
export { hosts, endpoints, sortCriteria, pageSizes };
|
Reference in New Issue
Block a user