mirror of https://github.com/artifacthub/hub.git
Add support to embed Artifact Hub items (#1249)
Closes #1228 Signed-off-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
This commit is contained in:
parent
93c054beb5
commit
c3bb4d1654
|
|
@ -9,6 +9,7 @@ node_modules/
|
|||
web/build
|
||||
web/public/docs
|
||||
web/coverage
|
||||
widget/build
|
||||
charts/artifact-hub/charts
|
||||
Chart.lock
|
||||
docs/www/content/topics/*
|
||||
|
|
|
|||
|
|
@ -46,13 +46,13 @@
|
|||
.navbar:not(.homeNavbar) {
|
||||
position: sticky !important;
|
||||
top: 0;
|
||||
z-index: 1071;
|
||||
z-index: 1040;
|
||||
}
|
||||
|
||||
:global(.noScroll-modal) .navbar:not(.homeNavbar),
|
||||
/* :global(.noScroll-modal) .navbar:not(.homeNavbar),
|
||||
:global(.noScroll-sidebar) .navbar:not(.homeNavbar) {
|
||||
z-index: 1;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
.iconWrapper {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--color-1-500);
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 10rem;
|
||||
top: 120% !important;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
right: 0.3rem !important;
|
||||
}
|
||||
|
||||
.dropdownItem:not(.isDisabled):hover {
|
||||
background-color: var(--color-1-10);
|
||||
}
|
||||
|
||||
.dropdownItem + .dropdownItem {
|
||||
border-top: 1px solid var(--border-md);
|
||||
}
|
||||
|
||||
.dropdownItem:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767.98px) {
|
||||
.dropdown {
|
||||
width: 18rem;
|
||||
right: -5rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
right: 6.25rem !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import classnames from 'classnames';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { BiCode } from 'react-icons/bi';
|
||||
import { HiDotsVertical } from 'react-icons/hi';
|
||||
|
||||
import useOutsideClick from '../../hooks/useOutsideClick';
|
||||
import { SearchFiltersURL } from '../../types';
|
||||
import styles from './MoreActionsButton.module.css';
|
||||
import WidgetModal from './WidgetModal';
|
||||
|
||||
interface Props {
|
||||
packageId: string;
|
||||
packageName: string;
|
||||
packageDescription: string;
|
||||
visibleWidget: boolean;
|
||||
searchUrlReferer?: SearchFiltersURL;
|
||||
fromStarredPage?: boolean;
|
||||
}
|
||||
|
||||
const MoreActionsButton = (props: Props) => {
|
||||
const [openStatus, setOpenStatus] = useState(false);
|
||||
const [visibleWidget, setVisibleWidget] = useState<boolean>(props.visibleWidget);
|
||||
const [currentPkgId, setCurrentPkgId] = useState<string>(props.packageId);
|
||||
|
||||
const ref = useRef(null);
|
||||
useOutsideClick([ref], openStatus, () => setOpenStatus(false));
|
||||
|
||||
useEffect(() => {
|
||||
if (props.packageId !== currentPkgId && openStatus) {
|
||||
setVisibleWidget(false);
|
||||
setCurrentPkgId(props.packageId);
|
||||
}
|
||||
}, [props.packageId]); /* eslint-disable-line react-hooks/exhaustive-deps */
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-none d-md-block position-relative ml-2">
|
||||
<button
|
||||
data-testid="moreActionsBtn"
|
||||
className="btn p-0 position-relative"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setOpenStatus(true);
|
||||
}}
|
||||
>
|
||||
<div className="d-flex flex-row align-items-center justify-content-center">
|
||||
<div
|
||||
className={`rounded-circle d-flex align-items-center justify-content-center text-primary iconSubsWrapper ${styles.iconWrapper}`}
|
||||
>
|
||||
<HiDotsVertical />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
ref={ref}
|
||||
data-testid="subsBtnDropdown"
|
||||
className={classnames('dropdown-menu dropdown-menu-right p-0', styles.dropdown, { show: openStatus })}
|
||||
>
|
||||
<div className={`arrow ${styles.arrow}`} />
|
||||
|
||||
<div className={`${styles.dropdownItem} dropdownItem`}>
|
||||
<button
|
||||
className={`${styles.dropdownItem} dropdownItem btn btn-link text-dark w-100`}
|
||||
onClick={() => {
|
||||
setVisibleWidget(true);
|
||||
setOpenStatus(false);
|
||||
}}
|
||||
>
|
||||
<div className="d-flex flex-row align-items-center">
|
||||
<BiCode className="mr-2" />
|
||||
<div>Embed artifact</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WidgetModal {...props} visibleWidget={visibleWidget} setOpenStatus={setVisibleWidget} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoreActionsButton;
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
}
|
||||
|
||||
.titleWrapper {
|
||||
margin-right: 200px;
|
||||
margin-right: 230px;
|
||||
}
|
||||
|
||||
.optsWrapper {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ const StarButton = (props: Props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={`d-flex flex-row align-items-center position-relative ${styles.wrapper}`}>
|
||||
<div className={`ml-auto d-flex flex-row align-items-center position-relative ${styles.wrapper}`}>
|
||||
<button
|
||||
data-testid="toggleStarBtn"
|
||||
className={classnames('btn btn-sm btn-primary px-1 px-md-3', styles.starBtn, {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
}
|
||||
|
||||
.arrow {
|
||||
right: 1.25rem !important;
|
||||
right: 0.3rem !important;
|
||||
}
|
||||
|
||||
.dropdownItem:not(.isDisabled):hover {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import classnames from 'classnames';
|
|||
import isNull from 'lodash/isNull';
|
||||
import isUndefined from 'lodash/isUndefined';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { FaCaretDown, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
|
||||
import { FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
|
||||
import { MdNotificationsActive, MdNotificationsOff } from 'react-icons/md';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ const SubscriptionsButton = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="d-none d-md-block position-relative ml-3">
|
||||
<div className="d-none d-md-block position-relative ml-2">
|
||||
<button
|
||||
data-testid="subscriptionsBtn"
|
||||
className="btn p-0 position-relative"
|
||||
|
|
@ -144,26 +144,21 @@ const SubscriptionsButton = (props: Props) => {
|
|||
setOpenStatus(true);
|
||||
}}
|
||||
>
|
||||
<div className="d-flex flex-row align-items-center justify-content-center">
|
||||
<div
|
||||
className={`rounded-circle d-flex align-items-center justify-content-center text-primary iconSubsWrapper ${styles.iconWrapper}`}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className={styles.loading}>
|
||||
<div className={`spinner-border text-primary ${styles.spinner}`} role="status">
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
<div
|
||||
className={`rounded-circle d-flex align-items-center justify-content-center text-primary iconSubsWrapper ${styles.iconWrapper}`}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className={styles.loading}>
|
||||
<div className={`spinner-border text-primary ${styles.spinner}`} role="status">
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
{!isUndefined(activeSubscriptions) && activeSubscriptions.length > 0 ? (
|
||||
<MdNotificationsActive className="rounded-circle" />
|
||||
) : (
|
||||
<MdNotificationsOff className="rounded-circle text-muted" />
|
||||
)}
|
||||
</div>
|
||||
<small className="ml-1 text-primary">
|
||||
<FaCaretDown />
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
{!isUndefined(activeSubscriptions) && activeSubscriptions.length > 0 ? (
|
||||
<MdNotificationsActive className="rounded-circle" />
|
||||
) : (
|
||||
<MdNotificationsOff className="rounded-circle text-muted" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
.iconWrapper {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--color-1-500);
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.customControlRightLabel::before {
|
||||
right: -2.25rem;
|
||||
left: auto !important;
|
||||
}
|
||||
|
||||
.customControlRightLabel::after {
|
||||
right: -1.38rem;
|
||||
left: auto !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { FiMoon, FiSun } from 'react-icons/fi';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { SearchFiltersURL } from '../../types';
|
||||
import Modal from '../common/Modal';
|
||||
import CommandBlock from './installation/CommandBlock';
|
||||
import styles from './WidgetModal.module.css';
|
||||
|
||||
interface Props {
|
||||
packageId: string;
|
||||
packageName: string;
|
||||
packageDescription: string;
|
||||
visibleWidget: boolean;
|
||||
searchUrlReferer?: SearchFiltersURL;
|
||||
fromStarredPage?: boolean;
|
||||
setOpenStatus: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
interface WidgetTheme {
|
||||
name: string;
|
||||
icon: JSX.Element;
|
||||
}
|
||||
|
||||
const DEFAULT_THEME = 'light';
|
||||
const THEMES: WidgetTheme[] = [
|
||||
{
|
||||
name: DEFAULT_THEME,
|
||||
icon: <FiSun />,
|
||||
},
|
||||
{ name: 'dark', icon: <FiMoon /> },
|
||||
];
|
||||
|
||||
const WidgetModal = (props: Props) => {
|
||||
const history = useHistory();
|
||||
const [theme, setTheme] = useState<string>(DEFAULT_THEME);
|
||||
const [header, setHeader] = useState<boolean>(true);
|
||||
const [responsive, setRepsonsive] = useState<boolean>(false);
|
||||
|
||||
const compoundWidgetSource = (): string => {
|
||||
const url = `${window.location.origin}${window.location.pathname}`;
|
||||
const code = `<div class="artifacthub-widget" data-url="${url}" data-theme="${theme}" data-header="${
|
||||
!header ? 'false' : 'true'
|
||||
}" data-responsive="${responsive ? 'true' : 'false'}"><blockquote><p lang="en" dir="ltr"><b>${
|
||||
props.packageName
|
||||
}</b>${
|
||||
props.packageDescription ? `: ${props.packageDescription}` : ''
|
||||
}</p>— Open in <a href="${url}">Artifact Hub</a></blockquote></div><script async src="${
|
||||
window.location.origin
|
||||
}/artifacthub-widget.js"></script>`;
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
const [widgetSource, setWidgetSource] = useState<string>(compoundWidgetSource());
|
||||
|
||||
const onCloseModal = () => {
|
||||
props.setOpenStatus(false);
|
||||
history.replace({
|
||||
search: '',
|
||||
state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setWidgetSource(compoundWidgetSource());
|
||||
}, [theme, header, responsive, props.packageId]); /* eslint-disable-line react-hooks/exhaustive-deps */
|
||||
|
||||
useEffect(() => {
|
||||
if (props.visibleWidget) {
|
||||
history.replace({
|
||||
search: '?modal=widget',
|
||||
state: { searchUrlReferer: props.searchUrlReferer, fromStarredPage: props.fromStarredPage },
|
||||
});
|
||||
}
|
||||
}, [props.visibleWidget]); /* eslint-disable-line react-hooks/exhaustive-deps */
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.visibleWidget && (
|
||||
<Modal
|
||||
modalDialogClassName={styles.modalDialog}
|
||||
header={<div className={`h3 m-2 flex-grow-1 ${styles.title}`}>Widget</div>}
|
||||
onClose={onCloseModal}
|
||||
open={props.visibleWidget}
|
||||
>
|
||||
<div className="w-100 position-relative">
|
||||
<label className={`font-weight-bold ${styles.label}`} htmlFor="theme">
|
||||
Theme
|
||||
</label>
|
||||
<div className="d-flex flex-row mb-3">
|
||||
{THEMES.map((themeOpt: WidgetTheme) => {
|
||||
return (
|
||||
<div className="custom-control custom-radio mr-4" key={`radio_theme_${themeOpt.name}`}>
|
||||
<input
|
||||
className="custom-control-input"
|
||||
type="radio"
|
||||
name="theme"
|
||||
id={themeOpt.name}
|
||||
value={themeOpt.name}
|
||||
checked={theme === themeOpt.name}
|
||||
required
|
||||
readOnly
|
||||
/>
|
||||
<label
|
||||
className="text-capitalize custom-control-label"
|
||||
htmlFor={themeOpt.name}
|
||||
onClick={() => {
|
||||
setTheme(themeOpt.name);
|
||||
}}
|
||||
>
|
||||
<div className="d-flex flex-row align-items-center">
|
||||
{themeOpt.icon}
|
||||
<span className="ml-1">{themeOpt.name}</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 mb-3">
|
||||
<div className="custom-control custom-switch pl-0">
|
||||
<input
|
||||
id="header"
|
||||
type="checkbox"
|
||||
className="custom-control-input"
|
||||
value="true"
|
||||
onChange={() => setHeader(!header)}
|
||||
checked={header}
|
||||
/>
|
||||
<label
|
||||
htmlFor="header"
|
||||
className={`custom-control-label font-weight-bold ${styles.label} ${styles.customControlRightLabel}`}
|
||||
>
|
||||
Header
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<small className="form-text text-muted mt-2">
|
||||
Displays Artifact Hub header at the top of the widget.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 mb-3">
|
||||
<div className="custom-control custom-switch pl-0">
|
||||
<input
|
||||
id="responsive"
|
||||
type="checkbox"
|
||||
className="custom-control-input"
|
||||
value="true"
|
||||
onChange={() => setRepsonsive(!responsive)}
|
||||
checked={responsive}
|
||||
/>
|
||||
<label
|
||||
htmlFor="responsive"
|
||||
className={`custom-control-label font-weight-bold ${styles.label} ${styles.customControlRightLabel}`}
|
||||
>
|
||||
Responsive
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<small className="form-text text-muted mt-2">
|
||||
The widget will try to use the width available on the parent container (between 350px and 650px).
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<label className={`font-weight-bold ${styles.label}`}>Code</label>
|
||||
|
||||
<CommandBlock command={widgetSource} language="javascript" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WidgetModal;
|
||||
|
|
@ -50,6 +50,7 @@ import CustomResourceDefinition from './CustomResourceDefinition';
|
|||
import Details from './Details';
|
||||
import InstallationModal from './installation/Modal';
|
||||
import ModalHeader from './ModalHeader';
|
||||
import MoreActionsButton from './MoreActionsButton';
|
||||
import styles from './PackageView.module.css';
|
||||
import ReadmeWrapper from './readme';
|
||||
import RecommendedPackages from './RecommendedPackages';
|
||||
|
|
@ -610,6 +611,14 @@ const PackageView = (props: Props) => {
|
|||
</span>
|
||||
<StarButton packageId={detail.packageId} />
|
||||
<SubscriptionsButton packageId={detail.packageId} />
|
||||
<MoreActionsButton
|
||||
packageId={detail.packageId}
|
||||
packageName={detail.displayName || detail.name}
|
||||
packageDescription={detail.description}
|
||||
visibleWidget={!isUndefined(props.visibleModal) && props.visibleModal === 'widget'}
|
||||
searchUrlReferer={props.searchUrlReferer}
|
||||
fromStarredPage={props.fromStarredPage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row align-items-baseline d-md-none">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": ["react-app", "plugin:prettier/recommended"],
|
||||
"plugins": ["simple-import-sort"],
|
||||
"rules": {
|
||||
"simple-import-sort/sort": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts?(x)"],
|
||||
"rules": {
|
||||
"sort-imports": "off",
|
||||
"import/order": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
module.exports = {
|
||||
babel: {
|
||||
presets: [],
|
||||
plugins: [
|
||||
["babel-plugin-styled-components", { "displayName": true }]
|
||||
],
|
||||
loaderOptions: {},
|
||||
loaderOptions: (babelLoaderOptions) => { return babelLoaderOptions; }
|
||||
},
|
||||
webpack: {
|
||||
configure: {
|
||||
output: {
|
||||
filename: 'static/js/artifacthub-widget.js',
|
||||
},
|
||||
optimization: {
|
||||
runtimeChunk: false,
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
vendors: false,
|
||||
// vendor chunk
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "widget",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/react": "^16.9.49",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/styled-components": "^5.1.9",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.27.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"styled-components": "^5.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^5.9.0",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.0.2",
|
||||
"@testing-library/user-event": "^12.1.4",
|
||||
"@types/jest": "^26.0.13",
|
||||
"@types/node": "^14.6.4",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-simple-import-sort": "^5.0.2",
|
||||
"prettier": "^2.1.1",
|
||||
"react-scripts": "^3.4.3",
|
||||
"rewire": "^5.0.0",
|
||||
"typescript": "^3.9.7",
|
||||
"webpack-cli": "^4.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "REACT_APP_HUB_BASE_URL=http://localhost:8000 react-scripts start",
|
||||
"build": "craco build",
|
||||
"build:cra": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --ext .js,.jsx,.ts,.tsx src --color",
|
||||
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx src --fix",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"format:diff": "prettier --list-different \"src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"isready": "yarn format && yarn lint && yarn test --watchAll=false --passWithNoTests --verbose && yarn build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Artifact Hub widget</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div class="artifacthub-widget" data-url="http://localhost:8000/packages/helm/hub/artifact-hub"></div>
|
||||
<div class="artifacthub-widget" data-url="http://localhost:8000/packages/tekton-task/test/send-to-telegram" data-theme="dark" data-responsive="true"></div>
|
||||
<div class="artifacthub-widget" data-url="https://staging.artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack" data-theme="light"></div>
|
||||
<div class="artifacthub-widget" data-url="https://staging.artifacthub.io/packages/helm-plugin/diff/diff" data-theme="dark"></div>
|
||||
<div class="artifacthub-widget" data-url="https://staging.artifacthub.io/packages/krew/krew-index/cilium" data-theme="dark"></div>
|
||||
<div class="artifacthub-widget" data-url="https://staging.artifacthub.io/packages/olm/community-operators/elastic-cloud-eck" data-theme="dark"></div>
|
||||
<div class="artifacthub-widget" data-url="https://staging.artifacthub.io/packages/falco/security-hub/ssh-connections" data-theme="dark"></div>
|
||||
<div class="artifacthub-widget" data-url="https://staging.artifacthub.io/packages/keda-scaler/keda-scalers-test/apache-kafka" data-theme="dark"></div>
|
||||
<div class="artifacthub-widget" data-url="https://staging.artifacthub.io/packages/opa/deprek8ion/deprek8ion" data-theme="dark"></div>
|
||||
<div class="artifacthub-widget" data-url="https://staging.artifacthub.io/packages/tbaction/tinkerbell-community/archive2disk" data-theme="dark"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import camelCase from 'lodash/camelCase';
|
||||
import isArray from 'lodash/isArray';
|
||||
import isObject from 'lodash/isObject';
|
||||
|
||||
import { Package } from '../types';
|
||||
|
||||
class API {
|
||||
private API_BASE_URL = `/api/v1`;
|
||||
|
||||
private toCamelCase(r: any): any {
|
||||
if (isArray(r)) {
|
||||
return r.map((v) => this.toCamelCase(v));
|
||||
} else if (isObject(r)) {
|
||||
return Object.keys(r).reduce(
|
||||
(result, key) => ({
|
||||
...result,
|
||||
[camelCase(key)]: this.toCamelCase((r as any)[key]),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
private async handleContent(res: any) {
|
||||
if (!res.ok) {
|
||||
throw new Error('error');
|
||||
} else {
|
||||
switch (res.headers.get('Content-Type')) {
|
||||
case 'text/plain; charset=utf-8':
|
||||
const text = await res.text();
|
||||
return text;
|
||||
case 'application/json':
|
||||
const json = await res.json();
|
||||
return this.toCamelCase(json);
|
||||
default:
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async apiFetch(url: string) {
|
||||
return fetch(url)
|
||||
.then((res) => this.handleContent(res))
|
||||
.catch((error) => Promise.reject(error));
|
||||
}
|
||||
|
||||
public getPackageInfo(baseUrl: string, pathname: string): Promise<Package> {
|
||||
return this.apiFetch(`${baseUrl}${this.API_BASE_URL}${pathname}/summary`);
|
||||
}
|
||||
}
|
||||
|
||||
const APIMethods = new API();
|
||||
export default APIMethods;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import App from './layout/App';
|
||||
|
||||
const WidgetDivs = document.querySelectorAll('.artifacthub-widget');
|
||||
WidgetDivs.forEach((div: Element) => {
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App element={div} />
|
||||
</React.StrictMode>,
|
||||
div
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import { isNull, isUndefined } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled, { css, keyframes } from 'styled-components';
|
||||
|
||||
import APIMethods from '../api';
|
||||
import { Package } from '../types';
|
||||
import Card from './card';
|
||||
import SVGIcons from './common/SVGIcons';
|
||||
|
||||
interface Props {
|
||||
element: Element;
|
||||
}
|
||||
|
||||
const DEFAULT_THEME = 'light';
|
||||
const AVAILABLE_THEMES = [DEFAULT_THEME, 'dark'];
|
||||
|
||||
const Wrapper = styled('div')`
|
||||
margin: 0.75rem;
|
||||
`;
|
||||
|
||||
const CardWrapper = styled('div')`
|
||||
${(props) =>
|
||||
props.theme === 'dark'
|
||||
? css`
|
||||
--color-1-500: #131216;
|
||||
--color-font: #a3a3a6;
|
||||
--white: #222529;
|
||||
--color-1-5: rgba(15, 14, 17, 0.95);
|
||||
--color-1-10: rgba(15, 14, 17, 0.9);
|
||||
--color-1-20: rgba(15, 14, 17, 0.8);
|
||||
--color-2-10: rgba(15, 14, 17, 0.1);
|
||||
--color-black-25: rgba(255, 255, 255, 0.25);
|
||||
--light-gray: #e9ecef;
|
||||
--success: #131216;
|
||||
--icon-color: #a3a3a6;
|
||||
`
|
||||
: css`
|
||||
--color-1-500: #659dbd;
|
||||
--color-font: #38383f;
|
||||
--white: #fff;
|
||||
--color-1-5: rgba(28, 44, 53, 0.05);
|
||||
--color-1-10: rgba(28, 44, 53, 0.1);
|
||||
--color-1-20: rgba(28, 44, 53, 0.2);
|
||||
--color-2-10: rgba(176, 206, 224, 0.1);
|
||||
--light-gray: #e9ecef;
|
||||
--success: #28a745;
|
||||
--icon-color: #fff;
|
||||
--color-black-25: rgba(0, 0, 0, 0.25);
|
||||
`}
|
||||
|
||||
font-family: 'Lato', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 1rem;
|
||||
color: var(--color-font);
|
||||
background-color: var(--white);
|
||||
background-clip: padding-box;
|
||||
border: 3px solid var(--color-1-500);
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
width: 350px;
|
||||
line-height: 1.15rem;
|
||||
|
||||
&.responsive {
|
||||
min-width: 350px;
|
||||
width: 100%;
|
||||
max-width: 650px;
|
||||
}
|
||||
|
||||
& *, & *:after, & *:before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Link = styled('a')`
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
`;
|
||||
|
||||
const CardHeader = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-1-500);
|
||||
font-size: 1.25rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
height: 40px;
|
||||
`;
|
||||
|
||||
const CardBody = styled('div')`
|
||||
padding: 0.75rem;
|
||||
color: var(--color-font);
|
||||
`;
|
||||
|
||||
const SpinnerWrapper = styled('div')`
|
||||
padding: 2rem 3rem;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const spinnerBorder = keyframes`
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Spinner = styled('div')`
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: text-bottom;
|
||||
border: 0.25em solid var(--color-1-500);
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: ${spinnerBorder} 0.75s linear infinite;
|
||||
animation: ${spinnerBorder} 0.75s linear infinite;
|
||||
`;
|
||||
|
||||
const Brand = styled(SVGIcons)`
|
||||
${(props) =>
|
||||
props.theme === 'dark' &&
|
||||
css`
|
||||
opacity: 0.75;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default function App(props: Props) {
|
||||
const { url, theme, header, responsive } = (props.element as HTMLElement).dataset;
|
||||
const currentTheme = theme && AVAILABLE_THEMES.includes(theme) ? theme : DEFAULT_THEME;
|
||||
const [urlParams, setUrlParams] = useState<URL | undefined>();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [packageItem, setPackageItem] = useState<Package | undefined | null>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchPackage() {
|
||||
if (url && urlParams) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setPackageItem(await APIMethods.getPackageInfo(urlParams.origin, urlParams.pathname));
|
||||
setIsLoading(false);
|
||||
} catch {
|
||||
setIsLoading(false);
|
||||
setPackageItem(null);
|
||||
}
|
||||
} else {
|
||||
setPackageItem(null);
|
||||
}
|
||||
}
|
||||
fetchPackage();
|
||||
}, [urlParams]); /* eslint-disable-line react-hooks/exhaustive-deps */
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
setUrlParams(new URL(url));
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
if (isNull(packageItem)) return null;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<CardWrapper
|
||||
theme={currentTheme}
|
||||
className={!isUndefined(responsive) && responsive === 'true' ? 'responsive' : ''}
|
||||
>
|
||||
<Link href={url} rel="noopener noreferrer" target="_blank">
|
||||
{(isUndefined(header) || header !== 'false') && (
|
||||
<CardHeader>
|
||||
<Brand name="logo" theme={currentTheme} />
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
<CardBody>
|
||||
{isLoading || isUndefined(packageItem) ? (
|
||||
<SpinnerWrapper>
|
||||
<Spinner />
|
||||
</SpinnerWrapper>
|
||||
) : (
|
||||
<Card
|
||||
packageItem={packageItem}
|
||||
theme={theme || DEFAULT_THEME}
|
||||
baseUrl={urlParams ? urlParams.origin : undefined}
|
||||
/>
|
||||
)}
|
||||
</CardBody>
|
||||
</Link>
|
||||
</CardWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
import { isUndefined } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Package } from '../../types';
|
||||
import Image from '../common/Image';
|
||||
import Label from '../common/Label';
|
||||
import RepositoryIconLabel from '../common/RepositoryIconLabel';
|
||||
import SVGIcons from '../common/SVGIcons';
|
||||
|
||||
interface Props {
|
||||
packageItem: Package;
|
||||
theme: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
const prettifyNumber = (num: number, digits?: number): string | number => {
|
||||
if (num < 1000) {
|
||||
return num;
|
||||
}
|
||||
|
||||
const si = [
|
||||
{ value: 1, symbol: '' },
|
||||
{ value: 1e3, symbol: 'k' },
|
||||
{ value: 1e6, symbol: 'M' },
|
||||
{ value: 1e9, symbol: 'G' },
|
||||
{ value: 1e12, symbol: 'T' },
|
||||
{ value: 1e15, symbol: 'P' },
|
||||
{ value: 1e18, symbol: 'E' },
|
||||
];
|
||||
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
|
||||
let i;
|
||||
for (i = si.length - 1; i > 0; i--) {
|
||||
if (num >= si[i].value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (num / si[i].value).toFixed(digits || 2).replace(rx, '$1') + si[i].symbol;
|
||||
};
|
||||
|
||||
const ImageWrapper = styled('div')`
|
||||
position: relative;
|
||||
min-width: 60px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-color: var(--white);
|
||||
border: 2px solid var(--color-1-10);
|
||||
box-shadow: 0px 0px 5px 0px var(--color-1-20);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Content = styled('div')`
|
||||
width: calc(100% - 60px);
|
||||
padding-left: 0.5rem;
|
||||
`;
|
||||
|
||||
const HeaderWrapper = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const TitleWrapper = styled('div')`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
|
||||
const TextTruncate = styled('div')`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const Title = styled(TextTruncate)`
|
||||
font-size: 1.15rem;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-right: 0.5rem;
|
||||
`;
|
||||
|
||||
const IconStarsWrapper = styled('div')`
|
||||
font-size: 75%;
|
||||
`;
|
||||
const StarsNumber = styled('div')`
|
||||
margin-left: 0.25rem;
|
||||
`;
|
||||
|
||||
const BadgesWrapper = styled('div')`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
|
||||
& > * {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Description = styled('div')`
|
||||
overflow: hidden;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 5;
|
||||
-webkit-box-orient: vertical;
|
||||
`;
|
||||
|
||||
const Legend = styled('span')`
|
||||
font-size: 80%;
|
||||
opacity: 0.8;
|
||||
text-transform: uppercase;
|
||||
margin-right: 0.25rem;
|
||||
`;
|
||||
|
||||
const ExtraInfo = styled('div')`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const Version = styled(TextTruncate)`
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
`;
|
||||
|
||||
const PublishedBy = styled(TextTruncate)`
|
||||
font-size: 0.9rem;
|
||||
`;
|
||||
|
||||
const Date = styled('small')`
|
||||
font-size: 70%;
|
||||
opacity: 0.75;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const Badge = styled('div')`
|
||||
font-size: 75%;
|
||||
text-transform: none;
|
||||
height: 19px;
|
||||
background-color: var(--color-1-5);
|
||||
border: 1px solid var(--color-1-10);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1em 0.4em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
border-radius: 50rem;
|
||||
`;
|
||||
|
||||
const Card = (props: Props) => {
|
||||
if (isUndefined(props.baseUrl)) return null;
|
||||
return (
|
||||
<>
|
||||
<HeaderWrapper>
|
||||
<ImageWrapper>
|
||||
{props.baseUrl && (
|
||||
<Image
|
||||
baseUrl={props.baseUrl}
|
||||
imageId={props.packageItem.logoImageId}
|
||||
alt={`Logo ${props.packageItem.displayName || props.packageItem.name}`}
|
||||
kind={props.packageItem.repository.kind}
|
||||
/>
|
||||
)}
|
||||
</ImageWrapper>
|
||||
<Content>
|
||||
<TitleWrapper>
|
||||
<Title>{props.packageItem.displayName || props.packageItem.name}</Title>
|
||||
<Badge>
|
||||
<IconStarsWrapper>
|
||||
<SVGIcons name="stars" />
|
||||
</IconStarsWrapper>
|
||||
<StarsNumber>{prettifyNumber(props.packageItem.stars || 0)}</StarsNumber>
|
||||
</Badge>
|
||||
</TitleWrapper>
|
||||
<div>
|
||||
<Version>
|
||||
<Legend>Version:</Legend>
|
||||
{props.packageItem.version}
|
||||
</Version>
|
||||
</div>
|
||||
</Content>
|
||||
</HeaderWrapper>
|
||||
|
||||
<ExtraInfo>
|
||||
<RepositoryIconLabel kind={props.packageItem.repository.kind} baseUrl={props.baseUrl} theme={props.theme} />
|
||||
|
||||
<Date>Updated {moment(props.packageItem.ts * 1000).fromNow()}</Date>
|
||||
</ExtraInfo>
|
||||
|
||||
{props.packageItem.description && <Description>{props.packageItem.description}</Description>}
|
||||
|
||||
<PublishedBy>
|
||||
<Legend>Published by:</Legend>
|
||||
{props.packageItem.repository.userAlias ||
|
||||
props.packageItem.repository.organizationDisplayName ||
|
||||
props.packageItem.repository.organizationName}
|
||||
</PublishedBy>
|
||||
|
||||
{(props.packageItem.official ||
|
||||
props.packageItem.repository.official ||
|
||||
props.packageItem.repository.verifiedPublisher) && (
|
||||
<BadgesWrapper>
|
||||
{(props.packageItem.official || props.packageItem.repository.official) && (
|
||||
<Label text="Official" type="success" icon={<SVGIcons name="official" />} />
|
||||
)}
|
||||
|
||||
{props.packageItem.repository.verifiedPublisher && (
|
||||
<Label text="Verified Publisher" icon={<SVGIcons name="verified" />} />
|
||||
)}
|
||||
</BadgesWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Card;
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { isNull, isUndefined } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { RepositoryKind } from '../../types';
|
||||
|
||||
interface Props {
|
||||
baseUrl: string;
|
||||
imageId?: string | null;
|
||||
alt: string;
|
||||
placeholderIcon?: JSX.Element;
|
||||
kind?: RepositoryKind;
|
||||
}
|
||||
|
||||
const PLACEHOLDER_SRC = '/static/media/package_placeholder.svg';
|
||||
|
||||
const StyledImage = styled('img')`
|
||||
max-width: calc(100% - 8px);
|
||||
max-height: calc(100% - 8px);
|
||||
`;
|
||||
|
||||
const Image = (props: Props) => {
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const getSrc = () => {
|
||||
return `${props.baseUrl}/image/${props.imageId}`;
|
||||
};
|
||||
|
||||
const getPlaceholder = (): string => {
|
||||
if (isUndefined(props.kind)) {
|
||||
return PLACEHOLDER_SRC;
|
||||
} else {
|
||||
switch (props.kind) {
|
||||
case RepositoryKind.Helm:
|
||||
case RepositoryKind.HelmPlugin:
|
||||
return '/static/media/placeholder_pkg_helm.png';
|
||||
case RepositoryKind.OLM:
|
||||
return '/static/media/placeholder_pkg_olm.png';
|
||||
case RepositoryKind.OPA:
|
||||
return '/static/media/placeholder_pkg_opa.png';
|
||||
case RepositoryKind.Falco:
|
||||
return '/static/media/placeholder_pkg_falco.png';
|
||||
case RepositoryKind.TBAction:
|
||||
return '/static/media/placeholder_pkg_tbaction.png';
|
||||
case RepositoryKind.Krew:
|
||||
return '/static/media/placeholder_pkg_krew.png';
|
||||
case RepositoryKind.TektonTask:
|
||||
return '/static/media/placeholder_pkg_tekton-task.png';
|
||||
case RepositoryKind.KedaScaler:
|
||||
return '/static/media/placeholder_pkg_keda-scaler.png';
|
||||
default:
|
||||
return PLACEHOLDER_SRC;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{error || isNull(props.imageId) || isUndefined(props.imageId) ? (
|
||||
<>
|
||||
{isUndefined(props.placeholderIcon) ? (
|
||||
<StyledImage alt={props.alt} src={`${props.baseUrl}${getPlaceholder()}`} />
|
||||
) : (
|
||||
<>{props.placeholderIcon}</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<StyledImage
|
||||
alt={props.alt}
|
||||
srcSet={`${getSrc()}@1x 1x, ${getSrc()}@2x 2x, ${getSrc()}@3x 3x, ${getSrc()}@4x 4x`}
|
||||
src={getSrc()}
|
||||
onError={() => setError(true)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Image;
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const LabelWrapper = styled('div')`
|
||||
font-size: 0.72rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const LabelText = styled('div')`
|
||||
background-color: var(--color-1-5);
|
||||
line-height: 18px;
|
||||
padding: 0 5px 0 10px;
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--color-1-10);
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
const IconWrapper = styled('div')`
|
||||
position: relative;
|
||||
border: 1px solid var(--color-1-10);
|
||||
border-radius: 3px;
|
||||
height: 20px;
|
||||
line-height: 21px;
|
||||
color: var(--icon-color);
|
||||
border-right: none;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
box-shadow: inset -1px 0 var(--color-black-25);
|
||||
margin-right: -3px;
|
||||
padding-left: 3px;
|
||||
width: 20px;
|
||||
background-color: var(--color-1-500);
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: calc(50% - 3px);
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset -1px 0 var(--color-black-25);
|
||||
background-color: var(--color-1-500);
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: var(--success);
|
||||
|
||||
&:before {
|
||||
background-color: var(--success);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
icon: JSX.Element;
|
||||
text: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
const Label = (props: Props) => (
|
||||
<LabelWrapper>
|
||||
<IconWrapper className={props.type}>{props.icon}</IconWrapper>
|
||||
<LabelText>{props.text}</LabelText>
|
||||
</LabelWrapper>
|
||||
);
|
||||
|
||||
export default Label;
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
|
||||
import { RepositoryKind } from '../../types';
|
||||
import SVGIcons from './SVGIcons';
|
||||
|
||||
interface Props {
|
||||
baseUrl: string;
|
||||
kind: RepositoryKind;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IconsList {
|
||||
[key: number]: JSX.Element;
|
||||
}
|
||||
|
||||
const ICONS: IconsList = {
|
||||
[RepositoryKind.Helm]: <SVGIcons name="helm" />,
|
||||
[RepositoryKind.HelmPlugin]: <SVGIcons name="helm-plugin" />,
|
||||
[RepositoryKind.OPA]: <SVGIcons name="opa" />,
|
||||
[RepositoryKind.OLM]: <SVGIcons name="olm" />,
|
||||
[RepositoryKind.Falco]: <SVGIcons name="falco" />,
|
||||
[RepositoryKind.TektonTask]: <SVGIcons name="tekton" />,
|
||||
[RepositoryKind.TBAction]: <SVGIcons name="tinkerbell" />,
|
||||
[RepositoryKind.Krew]: <SVGIcons name="krew" />,
|
||||
[RepositoryKind.KedaScaler]: <SVGIcons name="keda" />,
|
||||
};
|
||||
|
||||
const RepositoryIcon = (props: Props) => <div className={props.className}>{ICONS[props.kind]}</div>;
|
||||
|
||||
export default RepositoryIcon;
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import { isUndefined } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { RepositoryKind } from '../../types';
|
||||
import RepositoryIcon from './RepositoryIcon';
|
||||
|
||||
interface Props {
|
||||
baseUrl: string;
|
||||
kind: RepositoryKind;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface RepoKindDef {
|
||||
kind: RepositoryKind;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const REPOSITORY_KINDS: RepoKindDef[] = [
|
||||
{
|
||||
kind: RepositoryKind.Helm,
|
||||
name: 'Helm chart',
|
||||
},
|
||||
{
|
||||
kind: RepositoryKind.HelmPlugin,
|
||||
name: 'Helm plugin',
|
||||
},
|
||||
{
|
||||
kind: RepositoryKind.Falco,
|
||||
name: 'Falco rules',
|
||||
},
|
||||
{
|
||||
kind: RepositoryKind.OPA,
|
||||
name: 'OPA policies',
|
||||
},
|
||||
{
|
||||
kind: RepositoryKind.OLM,
|
||||
name: 'OLM operator',
|
||||
},
|
||||
{
|
||||
kind: RepositoryKind.TBAction,
|
||||
name: 'Tinkerbell action',
|
||||
},
|
||||
{
|
||||
kind: RepositoryKind.Krew,
|
||||
name: 'Krew kubectl plugin',
|
||||
},
|
||||
{
|
||||
kind: RepositoryKind.TektonTask,
|
||||
name: 'Tekton task',
|
||||
},
|
||||
{
|
||||
kind: RepositoryKind.KedaScaler,
|
||||
name: 'KEDA scaler',
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = styled('span')`
|
||||
background-color: var(--color-1-5);
|
||||
border: 1px solid var(--color-1-10);
|
||||
border-radius: 50rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1em 0.4em;
|
||||
font-size: 75%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const RepoName = styled('div')`
|
||||
margin-left: 0.25rem;
|
||||
`;
|
||||
|
||||
const Icon = styled(RepositoryIcon)`
|
||||
height: 15px;
|
||||
|
||||
& svg:not(:root) {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.grayedOut {
|
||||
filter: grayscale(1) brightness(1.5);
|
||||
}
|
||||
`;
|
||||
|
||||
const RepositoryIconLabel = (props: Props) => {
|
||||
const repo = REPOSITORY_KINDS.find((repoKind: RepoKindDef) => repoKind.kind === props.kind);
|
||||
|
||||
if (isUndefined(repo)) return null;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<div>
|
||||
<Icon
|
||||
kind={props.kind}
|
||||
baseUrl={props.baseUrl}
|
||||
theme={props.theme}
|
||||
className={props.theme === 'dark' ? 'grayedOut' : ''}
|
||||
/>
|
||||
</div>
|
||||
<RepoName>{repo.name}</RepoName>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryIconLabel;
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,33 @@
|
|||
.badge {
|
||||
font-size: 75%;
|
||||
text-transform: none;
|
||||
height: 19px;
|
||||
background-color: var(--color-1-5);
|
||||
border: 1px solid var(--color-1-10);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1em 0.4em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
border-radius: 50rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
top: -1px;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.icon img {
|
||||
max-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.textTruncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="react-scripts" />
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
export interface Package {
|
||||
packageId: string;
|
||||
name: string;
|
||||
displayName?: string;
|
||||
normalizedName: string;
|
||||
description: string;
|
||||
logoImageId?: string;
|
||||
repository: Repository;
|
||||
version: string;
|
||||
stars?: number | null;
|
||||
ts: number;
|
||||
official?: boolean;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
kind: RepositoryKind;
|
||||
verifiedPublisher?: boolean;
|
||||
official?: boolean;
|
||||
organizationName?: string;
|
||||
organizationDisplayName?: string;
|
||||
userAlias?: string;
|
||||
}
|
||||
|
||||
export enum RepositoryKind {
|
||||
Helm = 0,
|
||||
Falco,
|
||||
OPA,
|
||||
OLM,
|
||||
TBAction,
|
||||
Krew,
|
||||
HelmPlugin,
|
||||
TektonTask,
|
||||
KedaScaler,
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue