1
0
mirror of https://github.com/woodpecker-ci/woodpecker.git synced 2026-05-22 08:55:42 +02:00

Improve plugins index (#1200)

Co-authored-by: qwerty287 <80460567+qwerty287@users.noreply.github.com>
This commit is contained in:
Anbraten
2022-09-25 19:04:47 +02:00
committed by GitHub
parent 896746a91a
commit 62d82765fd
20 changed files with 2628 additions and 2478 deletions
+38 -49
View File
@@ -1,64 +1,53 @@
import { LoadContext, Plugin, PluginContentLoadedActions } from '@docusaurus/types';
import { Octokit } from '@octokit/rest';
import { components as OctokitComponents } from '@octokit/openapi-types';
import path from 'path';
import { Content, WoodpeckerPlugin, WoodpeckerPluginHeader } from './types';
import fs from 'fs';
import axios, { AxiosError } from 'axios';
import { Content, WoodpeckerPlugin, WoodpeckerPluginHeader, WoodpeckerPluginIndexEntry } from './types';
import * as markdown from './markdown';
const octokit = new Octokit();
async function getDocs(repoName: string): Promise<string | undefined> {
try {
const docsResult = (
await octokit.repos.getContent({
owner: 'woodpecker-ci',
repo: repoName,
path: '/docs.md',
})
).data as OctokitComponents['schemas']['content-file'];
return Buffer.from(docsResult.content, 'base64').toString('ascii');
} catch (e) {
console.error("Can't fetch docs file for repository", repoName, e);
}
return undefined;
}
async function loadContent(): Promise<Content> {
const repositories = (
await octokit.rest.search.repos({
// search for repos in woodpecker-ci org with the topic: woodpecker-plugin including forks
q: 'org:woodpecker-ci topic:woodpecker-plugin fork:true',
})
).data.items;
const file = path.join(__dirname, '..', 'plugins.json');
console.log(repositories.map((r) => r.name));
const pluginsIndex = JSON.parse(fs.readFileSync(file).toString()) as { plugins: WoodpeckerPluginIndexEntry[] };
const plugins = (
await Promise.all(
repositories.map(async (repo) => {
const docs = await getDocs(repo.name);
if (!docs) {
pluginsIndex.plugins.map(async (i) => {
if (i['// todo']) {
return undefined;
}
const header = markdown.getHeader<WoodpeckerPluginHeader>(docs);
const body = markdown.getContent(docs);
let docsContent: string;
try {
const response = await axios(i.docs);
docsContent = response.data;
} catch (e) {
console.error("Can't fetch docs file", i.docs, (e as AxiosError).message);
return undefined;
}
const plugin: WoodpeckerPlugin = {
name: header?.name || repo.name,
repoName: repo.name,
url: repo.html_url,
icon: header?.icon,
description: header?.description,
docs: body,
const docsHeader = markdown.getHeader<WoodpeckerPluginHeader>(docsContent);
const docsBody = markdown.getContent(docsContent);
if (!docsHeader.name) {
return undefined;
}
return <WoodpeckerPlugin>{
name: docsHeader.name || i.name,
url: docsHeader.url,
icon: docsHeader.icon,
description: docsHeader.description,
docs: docsBody,
tags: docsHeader.tags || [],
author: docsHeader.author,
containerImage: docsHeader.containerImage,
containerImageUrl: docsHeader.containerImageUrl,
verified: i.verified || false,
};
return plugin;
}),
)
).filter((plugin) => plugin);
).filter<WoodpeckerPlugin>((plugin): plugin is WoodpeckerPlugin => plugin !== undefined);
return {
plugins,
@@ -77,11 +66,11 @@ async function contentLoaded({
const pluginsJsonPath = await createData('plugins.json', JSON.stringify(plugins));
await Promise.all(
plugins.map(async (plugin) => {
const pluginJsonPath = await createData(`plugin-${plugin.repoName}.json`, JSON.stringify(plugin));
plugins.map(async (plugin, i) => {
const pluginJsonPath = await createData(`plugin-${i}.json`, JSON.stringify(plugin));
addRoute({
path: `/plugins/${plugin.repoName}`,
path: `/plugins/${plugin.name}`,
component: '@theme/WoodpeckerPlugin',
modules: {
plugin: pluginJsonPath,
@@ -113,7 +102,7 @@ export default function pluginWoodpeckerPluginsIndex(context: LoadContext, optio
return path.join(__dirname, '..', 'src', 'theme');
},
getPathsToWatch() {
return [path.join(__dirname, '..', 'dist', '**', '*.{js,jsx}')];
return [path.join(__dirname, '..', 'dist', '**', '*.{js,jsx,css}')];
},
};
}
@@ -1,4 +1,5 @@
import { marked } from 'marked';
import { parse as YAMLParse } from 'yaml';
const tokens = ['---', '---'];
const regexHeader = new RegExp('^' + tokens[0] + '([\\s|\\S]*?)' + tokens[1]);
@@ -6,27 +7,18 @@ const regexContent = new RegExp('^ *?\\' + tokens[0] + '[^]*?' + tokens[1] + '*'
export function getHeader<T = any>(data: string): T {
const header = getRawHeader(data);
const tmpObj = {};
const lines = header.trim().split('\n');
lines.forEach((line, i) => {
var arr = line.trim().split(':');
tmpObj[arr.shift()] = arr.join(':').trim();
});
return tmpObj as T;
return YAMLParse(header) as T;
}
export function getRawHeader(data: string): string {
const header = regexHeader.exec(data);
if (!header) {
new Error("Can't get the header");
throw new Error("Can't get the header");
}
return header[1];
}
export function getContent(data): string {
export function getContent(data: string): string {
const content = data.replace(regexContent, '').replace(/<!--(.*?)-->/gm, '');
if (!content) {
throw new Error("Can't get the content");
@@ -0,0 +1,45 @@
import React from 'react';
export const IconVerified = (size = 32) => (
<div title="This plugin is verified by the Woodpecker CI maintainers">
<svg width={size} height={size} viewBox="0 0 24 24" style={{ color: '#0369a1', marginLeft: '1rem' }}>
<path
fill="currentColor"
d="m8.6 22.5l-1.9-3.2l-3.6-.8l.35-3.7L1 12l2.45-2.8l-.35-3.7l3.6-.8l1.9-3.2L12 2.95l3.4-1.45l1.9 3.2l3.6.8l-.35 3.7L23 12l-2.45 2.8l.35 3.7l-3.6.8l-1.9 3.2l-3.4-1.45Zm2.35-6.95L16.6 9.9l-1.4-1.45l-4.25 4.25l-2.15-2.1L7.4 12Z"
/>
</svg>
</div>
);
export const IconContainer = (size = 32) => (
<div title="Container">
<svg width={size} height={size} viewBox="0 0 16 16">
<path
fill="currentColor"
fillRule="evenodd"
d="m10.41.24l4.711 2.774A1.767 1.767 0 0 1 16 4.54v5.01a1.77 1.77 0 0 1-.88 1.53l-7.753 4.521l-.002.001a1.767 1.767 0 0 1-1.774 0H5.59L.873 12.85A1.762 1.762 0 0 1 0 11.327V6.292c0-.304.078-.598.22-.855l.004-.005l.01-.019c.15-.262.369-.486.64-.643L8.641.239a1.75 1.75 0 0 1 1.765 0l.002.001zM9.397 1.534a.25.25 0 0 1 .252 0l4.115 2.422l-7.152 4.148a.267.267 0 0 1-.269 0L2.227 5.716l7.17-4.182zM7.365 9.402L8.73 8.61v4.46l-1.5.875V9.473a1.77 1.77 0 0 0 .136-.071zm2.864 2.794V7.741l1.521-.882v4.45l-1.521.887zm3.021-1.762l1.115-.65h.002a.268.268 0 0 0 .133-.232V5.264l-1.25.725v4.445zm-11.621 1.12l4.1 2.393V9.474a1.77 1.77 0 0 1-.138-.072L1.5 7.029v4.298c0 .095.05.181.129.227z"
/>
</svg>
</div>
);
export const IconWebsite = (size = 32) => (
<svg width={size} height={size} viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" strokeWidth="1.5">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12s4.477 10 10 10"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13 2.05S16 6 16 12m-5 9.95S8 18 8 12c0-6 3-9.95 3-9.95M2.63 15.5H12m-9.37-7h18.74"
/>
<path
d="M21.879 17.917c.494.304.463 1.043-.045 1.101l-2.567.291l-1.151 2.312c-.228.459-.933.234-1.05-.334l-1.255-6.116c-.099-.48.333-.782.75-.525l5.318 3.271Z"
clipRule="evenodd"
/>
</g>
</svg>
);
@@ -1,31 +1,70 @@
import React from 'react';
import clsx from 'clsx';
import Layout from '@theme/Layout';
import { WoodpeckerPlugin as WoodpeckerPluginType } from '../types';
import { IconContainer, IconVerified, IconWebsite } from './Icons';
export function WoodpeckerPlugin({ plugin }: { plugin: WoodpeckerPluginType }) {
return (
<Layout
title="Woodpecker CI plugins"
description="List of Woodpecker-CI plugins"
>
<main className={clsx("container margin-vert--lg")}>
<Layout title="Woodpecker CI plugins" description="List of Woodpecker-CI plugins">
<main className="container margin-vert--lg">
<section>
<div className={clsx("container")}>
<a href="/plugins">&lt;&lt; Back to plugin list</a>
<div className={clsx("row")}>
<div className={clsx("col col--10")}>
<h1>{plugin.name}</h1>
<p>{plugin.description}</p>
<a href={plugin.url} target="_blank" rel="noopener noreferrer">
{plugin.url}
</a>
<div className="container">
<div className="wp-plugin-breadcrumbs">
<a href="/plugins">Plugins</a>
<span> / </span>
<span>{plugin.name}</span>
</div>
<div className="row">
<div className="col col--10">
<div style={{ display: 'flex', alignItems: 'center' }}>
<h1 style={{ marginBottom: 0 }}>{plugin.name}</h1>
{plugin.verified && IconVerified()}
</div>
{plugin.author && <span>by {plugin.author}</span>}
<div style={{ marginTop: '1rem' }}>
{plugin.containerImage && (
<div style={{ display: 'flex', gap: '.5rem', alignItems: 'center' }}>
{IconContainer(20)}
{plugin.containerImageUrl ? (
<a href={plugin.containerImageUrl} target="_blank" rel="noopener noreferrer">
{plugin.containerImage}
</a>
) : (
<span>{plugin.containerImage}</span>
)}
</div>
)}
{plugin.url && (
<a
href={plugin.url}
target="_blank"
rel="noopener noreferrer"
style={{ display: 'flex', gap: '.5rem', alignItems: 'center' }}
>
<div style={{ color: 'var(--ifm-font-color-base)' }}>{IconWebsite(20)}</div> Website
</a>
)}
{plugin.tags && (
<div className="wp-plugin-tags" style={{ marginTop: '.5rem' }}>
{plugin.tags.map((tag, idx) => (
<span className="badge badge--success" key={idx}>
{tag}
</span>
))}
</div>
)}
</div>
<p style={{ marginTop: '2rem', marginBottom: '1rem' }}>{plugin.description}</p>
</div>
<div className={clsx("col col--2")}>
<div className="col col--2">
<img src={plugin.icon} width="150" height="150" />
</div>
</div>
<hr />
<hr style={{ margin: '1rem 0' }} />
<div dangerouslySetInnerHTML={{ __html: plugin.docs }} />
</div>
</section>
@@ -1,85 +1,95 @@
import React from 'react';
import clsx from 'clsx';
import React, { useState } from 'react';
import Fuse from 'fuse.js';
import Layout from '@theme/Layout';
import './style.css';
import { WoodpeckerPlugin } from '../types';
import { IconVerified } from './Icons';
function PluginPanel({ plugin }: { plugin: WoodpeckerPlugin }) {
const pluginUrl = `/plugins/${plugin.repoName}`;
const pluginUrl = `/plugins/${plugin.name}`;
return (
<div className={clsx('col col--6')}>
<div className={clsx('card margin-horiz--sm margin-vert--md ')}>
<div className={clsx('card__header row')}>
<div className={clsx('col col--8')}>
<a href={pluginUrl}>
<h3>{plugin.name}</h3>
</a>
<p>{plugin.description}</p>
</div>
<a href={pluginUrl} className={clsx('col col--4 text--right')}>
<img src={plugin.icon} width="100" height="100" />
</a>
<a href={pluginUrl} className="card shadow--md wp-plugin-card">
<div className="card__header row">
<div className="col col--2 text--left">
<img src={plugin.icon} width="50" height="50" />
</div>
<div className={clsx('card__footer')}>
<a href={pluginUrl} className={clsx('button button--secondary button--outline button--block ')}>
Open {plugin.name}
</a>
<div className="col col--10">
<h3>{plugin.name}</h3>
<p>{plugin.description}</p>
{plugin.tags && (
<div className="wp-plugin-tags">
{plugin.tags.map((tag, idx) => (
<span className="badge badge--success" key={idx}>
{tag}
</span>
))}
</div>
)}
</div>
</div>
</div>
{plugin.verified && <div className="wp-plugin-verified">{IconVerified()}</div>}
</a>
);
}
export function WoodpeckerPluginList({ plugins }: { plugins: WoodpeckerPlugin[] }) {
const applyForIndexUrl =
'https://github.com/woodpecker-ci/woodpecker/issues/new?labels=plugin&template=plugin_index.yml';
'https://github.com/woodpecker-ci/woodpecker/edit/master/docs/plugins/woodpecker-plugins/plugins.json';
const NewPluginPanel = () => (
<a href={applyForIndexUrl} target="_blank" rel="noopener noreferrer" className="card shadow--md wp-plugin-card">
<div className="card__header row">
<div className="col col--2">
<svg width="50" height="50" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M88.2357 38.0952H61.9048V11.7643C61.9048 5.29524 56.4714 0 50 0C43.5286 0 38.0952 5.29524 38.0952 11.7643V38.0952H11.7643C5.29524 38.0952 0 43.5286 0 50C0 56.4714 5.29524 61.9048 11.7643 61.9048H38.0952V88.2357C38.0952 94.7048 43.5286 100 50 100C56.4714 100 61.9048 94.7048 61.9048 88.2357V61.9048H88.2357C94.7048 61.9048 100 56.4714 100 50C100 43.5286 94.7048 38.0952 88.2357 38.0952Z"
fill="#4CAF50"
/>
</svg>
</div>
<div className="col col--10">
<h3>Add your own plugin</h3>
<p>You can simply add your own plugin to this index.</p>
</div>
</div>
</a>
);
const fuse = new Fuse(plugins, {
keys: ['name', 'description'],
threshold: 0.3,
});
const [query, setQuery] = useState('');
const searchedPlugins = query.length >= 1 ? fuse.search(query) : plugins.map((p) => ({ item: p }));
return (
<Layout title="Woodpecker CI plugins" description="List of all Woodpecker-CI plugins">
<main className="container margin-vert--lg">
<section>
<div className="container">
<div className="row">
{plugins.map((plugin, idx) => (
<PluginPanel key={idx} plugin={plugin} />
<div style={{ display: 'flex', flexFlow: 'column', alignItems: 'center' }}>
<h1>Woodpecker CI plugins</h1>
<p>This list contains plugins which you can use to easily execute usual pipeline tasks.</p>
<a href={applyForIndexUrl} target="_blank" rel="noopener noreferrer" className="button button--primary">
🎉 Add your plugin
</a>
</div>
<div className="container" style={{ display: 'flex', flexFlow: 'column', marginTop: '4rem' }}>
<input
type="search"
autoComplete="off"
value={query}
onChange={(event) => setQuery(event.currentTarget.value)}
placeholder="Search for a plugin ..."
className="wp-plugin-search"
/>
<div className="wp-plugins-list">
{/* {query.length == 0 && <NewPluginPanel />} */}
{searchedPlugins.map((plugin, idx) => (
<PluginPanel key={idx} plugin={plugin.item} />
))}
{/* <div className={clsx('col col--6')}>
<div className={clsx('card margin-horiz--sm margin-vert--md ')}>
<div className={clsx('card__header row')}>
<div className={clsx('col col--8')}>
<a href={applyForIndexUrl}>
<h3>Add your own plugin</h3>
</a>
<p>You can simply add your own plugin to this index.</p>
</div>
<a
href={applyForIndexUrl}
target="_blank"
rel="noopener noreferrer"
className={clsx('col col--4 text--right')}
>
<svg
width="100"
height="100"
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M88.2357 38.0952H61.9048V11.7643C61.9048 5.29524 56.4714 0 50 0C43.5286 0 38.0952 5.29524 38.0952 11.7643V38.0952H11.7643C5.29524 38.0952 0 43.5286 0 50C0 56.4714 5.29524 61.9048 11.7643 61.9048H38.0952V88.2357C38.0952 94.7048 43.5286 100 50 100C56.4714 100 61.9048 94.7048 61.9048 88.2357V61.9048H88.2357C94.7048 61.9048 100 56.4714 100 50C100 43.5286 94.7048 38.0952 88.2357 38.0952Z"
fill="#4CAF50"
/>
</svg>
</a>
</div>
<div className={clsx('card__footer')}>
<a
href={applyForIndexUrl}
className={clsx('button button--secondary button--outline button--block ')}
>
Add your own plugin
</a>
</div>
</div>
</div> */}
</div>
</div>
</section>
@@ -0,0 +1,64 @@
.wp-plugins-list {
display: grid;
grid-template-columns: auto auto;
grid-gap: 2rem;
margin-top: 2rem;
}
.wp-plugin-card {
display: flex;
position: relative;
max-width: 32rem;
color: var(--ifm-navbar-link-color);
text-decoration: none;
padding: .5rem 0 1rem;
flex-grow: 1;
}
.wp-plugin-card:hover {
color: var(--ifm-navbar-link-color);
text-decoration: none;
}
.wp-plugin-card:hover h3 {
color: var(--ifm-link-color);
text-decoration: underline;
}
.wp-plugin-card h3 {
color: var(--ifm-link-color);
}
.wp-plugin-verified {
position: absolute;
top: .75rem;
right: 1rem;
color: #0369a1;
}
.wp-plugin-tags {
display: flex;
gap: .5rem;
}
.wp-plugin-search {
width: 100%;
max-width: 32rem;
margin: 0 auto;
padding: 1rem 1rem 1rem 2.25rem;
font-size: 1.1rem;
appearance: none;
background: var(--ifm-navbar-search-input-background-color) var(--ifm-navbar-search-input-icon) no-repeat 0.75rem 1rem / 1.1rem 1.1rem;
border-radius: .5rem;
border: 1px solid var(--ifm-card-background-color);
color: var(--ifm-navbar-search-input-color);
}
.wp-plugin-search::placeholder {
color: var(--ifm-navbar-search-input-color);
}
.wp-plugin-breadcrumbs {
margin-bottom: 2rem;
}
+18 -9
View File
@@ -1,16 +1,25 @@
export type WoodpeckerPluginHeader = {
name?: string;
description?: string;
icon?: string;
name?: string; // name of the plugin
description?: string; // short description of the plugin
url?: string; // url of the plugin normally link to forge
tags?: string[]; // tags to categorize the plugin
author?: string; // author of the plugin
icon?: string; // url pointing to an icon
containerImage?: string; // name of a container image
containerImageUrl?: string; // url to a container image registry
};
export type WoodpeckerPlugin = {
export type WoodpeckerPluginIndexEntry = {
'// todo'?: boolean;
name: string; // name of the plugin
docs: string; // http url to the docs.md file
verified?: boolean; // plugins maintained by trusted parties
};
export type WoodpeckerPlugin = WoodpeckerPluginHeader & {
name: string;
repoName: string;
description: string;
url: string;
icon: string;
docs: string;
docs: string; // body of the docs .md file
verified: boolean; // we set verified to false when not explicitly set
};
export type Content = {