diff --git a/README.md b/README.md index 5a93eca..8d6a645 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,25 @@ Some of the features of **CLOTributor** are controlled by some special labels (o - `good first issue`: use this label to highlight issues that may be a good fit for new contributors to the project. - `mentor available` or `mentorship`: to indicate that someone may be available to guide contributors with this issue. +## Maintainers wanted + +If your project is looking for maintainers, CLOTributor can highlight this in a special way to let potential candidates know. This feature can be enabled by submitting a PR to add the block below to the corresponding project in the [data files](https://github.com/cncf/clomonitor/tree/main/data). You can add as many links or contacts as you need, or omit any of them if you prefer. + +```yaml +maintainers_wanted: + enabled: true + links: + - title: How to contribute to the project + url: https://github.com/org/repo/CONTRIBUTING.md + - title: Development environment setup + url: https://github.com/org/repo/docs/dev_env_setup.md + contacts: + - github_handle: user1 + - github_handle: user2 +``` + +*NOTE: the user submitting the pull request **must** already be a project's maintainer.* + ## Projects and repositories **CLOTributor's** data source for projects and repositories is [**CLOMonitor**](https://github.com/cncf/clomonitor#projects), which lists most of the projects in the [CNCF](https://www.cncf.io/projects/) and [LF AI & DATA](https://lfaidata.foundation/projects/) foundations. diff --git a/clotributor-registrar/src/registrar.rs b/clotributor-registrar/src/registrar.rs index ede0b60..1d12c39 100644 --- a/clotributor-registrar/src/registrar.rs +++ b/clotributor-registrar/src/registrar.rs @@ -30,6 +30,8 @@ pub(crate) struct Project { pub devstats_url: Option, pub accepted_at: Option, pub maturity: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub maintainers_wanted: Option, pub digest: Option, pub repositories: Vec, } @@ -51,6 +53,31 @@ pub(crate) struct Repository { pub url: String, } +/// Defines if the project is looking for maintainers, as well as some extra +/// reference and contact information. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) struct MaintainersWanted { + enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + links: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + contacts: Option>, +} + +/// Represents some information about a link. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub(crate) struct Link { + title: Option, + url: String, +} + +/// Represents some information about a contact. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub(crate) struct Contact { + github_handle: String, +} + /// Process foundations registered in the database. #[instrument(skip_all, err)] pub(crate) async fn run(cfg: &Config, db: DynDB) -> Result<()> { @@ -362,7 +389,8 @@ mod tests { repositories: vec![Repository{ name: "artifact-hub".to_string(), url: "https://github.com/artifacthub/hub".to_string(), - }] + }], + maintainers_wanted: None, }), ) .times(1) diff --git a/database/migrations/functions/issues/search_issues.sql b/database/migrations/functions/issues/search_issues.sql index e52d2bd..fa7dd76 100644 --- a/database/migrations/functions/issues/search_issues.sql +++ b/database/migrations/functions/issues/search_issues.sql @@ -86,6 +86,7 @@ begin p.devstats_url as project_devstats_url, p.accepted_at as project_accepted_at, p.maturity as project_maturity, + p.maintainers_wanted as project_maintainers_wanted, p.foundation_id as project_foundation, ( case when v_tsquery_web is not null then @@ -158,6 +159,7 @@ begin 'devstats_url', project_devstats_url, 'accepted_at', project_accepted_at, 'maturity', project_maturity, + 'maintainers_wanted', project_maintainers_wanted, 'foundation', project_foundation ), '_relevance', relevance diff --git a/database/migrations/functions/projects/register_project.sql b/database/migrations/functions/projects/register_project.sql index 24b6b0f..a4df6b9 100644 --- a/database/migrations/functions/projects/register_project.sql +++ b/database/migrations/functions/projects/register_project.sql @@ -15,6 +15,7 @@ begin devstats_url, accepted_at, maturity, + maintainers_wanted, digest, foundation_id ) values ( @@ -26,6 +27,7 @@ begin p_project->>'devstats_url', (p_project->>'accepted_at')::date, (p_project->>'maturity')::maturity, + p_project->'maintainers_wanted', p_project->>'digest', p_foundation_id ) @@ -38,6 +40,7 @@ begin devstats_url = excluded.devstats_url, accepted_at = excluded.accepted_at, maturity = excluded.maturity, + maintainers_wanted = excluded.maintainers_wanted, digest = excluded.digest returning project_id into v_project_id; diff --git a/database/migrations/schema/001_initial.sql b/database/migrations/schema/001_initial.sql index 9f2df38..4480b37 100644 --- a/database/migrations/schema/001_initial.sql +++ b/database/migrations/schema/001_initial.sql @@ -22,6 +22,7 @@ create table if not exists project ( devstats_url text check (devstats_url <> ''), accepted_at date, maturity maturity not null, + maintainers_wanted jsonb, digest text, created_at timestamptz not null default current_timestamp, updated_at timestamptz not null default current_timestamp, diff --git a/web/src/layout/common/Card.module.css b/web/src/layout/common/Card.module.css index eabd041..5af00a7 100644 --- a/web/src/layout/common/Card.module.css +++ b/web/src/layout/common/Card.module.css @@ -146,6 +146,10 @@ background-color: var(--clo-secondary-15); } +.negativeMarginTop { + margin-top: -1.3rem; +} + @media only screen and (max-width: 575.98px) { .title { font-size: 1rem; diff --git a/web/src/layout/common/Card.tsx b/web/src/layout/common/Card.tsx index f8dccfc..eeae3ce 100644 --- a/web/src/layout/common/Card.tsx +++ b/web/src/layout/common/Card.tsx @@ -24,6 +24,7 @@ import prepareQueryString from '../../utils/prepareQueryString'; import removeEmojis from '../../utils/removeEmojis'; import removeLastDot from '../../utils/removeLastDot'; import styles from './Card.module.css'; +import MaintainersWantedBadge from './MaintainersWantedBadge'; interface Props { issue: Issue; @@ -34,6 +35,8 @@ const Card = (props: Props) => { const { ctx } = useContext(AppContext); const { effective } = ctx.prefs.theme; const [availableTopics, setAvailableTopics] = useState([]); + const isMaintainersWantedAvailable: boolean = + !isUndefined(props.issue.project.maintainers_wanted) && props.issue.project.maintainers_wanted.enabled; const searchByText = (text: string) => { navigate({ @@ -124,6 +127,13 @@ const Card = (props: Props) => {
+ {isMaintainersWantedAvailable && ( + + )} {
-
-
+
+
{`${props.issue.project.display_name { effective_theme={effective} />
-
+
@@ -212,6 +232,10 @@ const Card = (props: Props) => {
+ + {isMaintainersWantedAvailable && ( + + )}
diff --git a/web/src/layout/common/MaintainersWantedBadge.module.css b/web/src/layout/common/MaintainersWantedBadge.module.css new file mode 100644 index 0000000..ca4c26f --- /dev/null +++ b/web/src/layout/common/MaintainersWantedBadge.module.css @@ -0,0 +1,46 @@ +.dropdown { + margin-top: 25px; +} + +.badge { + padding: 0.2em 0.5em; + background-color: var(--bs-white); + border: 1px solid rgba(0, 0, 0, 0.176); + color: var(--clo-tertiary); +} + +[data-theme='dark'] .badge { + border-color: var(--solid-border); + background-color: var(--clo-secondary-15); +} + +.icon { + top: -1px; +} + +.dot { + top: 1px; +} + +.wrapper { + left: 3rem; + right: 3rem; + bottom: -1px; +} + +.badgeInBottom { + padding-top: 3.64px; + padding-bottom: 2.64px; + font-size: 0.65rem; + line-height: 0.6rem; + letter-spacing: 0.05rem; + border-top-right-radius: 5px; + border-top-left-radius: 5px; +} + +@media only screen and (max-width: 1199.98px) { + .wrapper { + left: 1rem; + right: 1rem; + } +} diff --git a/web/src/layout/common/MaintainersWantedBadge.tsx b/web/src/layout/common/MaintainersWantedBadge.tsx new file mode 100644 index 0000000..5cfeacd --- /dev/null +++ b/web/src/layout/common/MaintainersWantedBadge.tsx @@ -0,0 +1,107 @@ +import classNames from 'classnames'; +import { DropdownOnHover, ExternalLink } from 'clo-ui'; +import { isUndefined } from 'lodash'; +import { MdInfoOutline } from 'react-icons/md'; +import { RxDotFilled } from 'react-icons/rx'; + +import { MaintainersWanted, MaintainersWantedContact, MaintainersWantedLink } from '../../types'; +import styles from './MaintainersWantedBadge.module.css'; + +interface Props { + className?: string; + maintainers_wanted: MaintainersWanted; + buttonStyle?: boolean; +} + +const MaintainersWantedBadge = (props: Props) => { + const activeTooltip = + (props.maintainers_wanted.contacts && props.maintainers_wanted.contacts.length > 0) || + (props.maintainers_wanted.links && props.maintainers_wanted.links.length > 0); + const isButton = !isUndefined(props.buttonStyle) && props.buttonStyle; + + const maintainersBadge = ( + <> + {!isButton ? ( +
+
+
Maintainers wanted
+ +
+
+ ) : ( +
+
+ +
Maintainers wanted
+
+
+ )} + + ); + + return ( +
+ {activeTooltip ? ( + +
+ {props.maintainers_wanted.links && props.maintainers_wanted.links.length > 0 && ( + <> +
Links
+
0, + })} + > + {props.maintainers_wanted.links.map((link: MaintainersWantedLink, index: number) => { + return ( +
+ + + {link.title || link.url} + +
+ ); + })} +
+ + )} + {props.maintainers_wanted.contacts && props.maintainers_wanted.contacts.length > 0 && ( + <> +
Contacts
+
+ {props.maintainers_wanted.contacts.map((contact: MaintainersWantedContact, index: number) => { + return ( +
+ + + {contact.github_handle} + +
+ ); + })} +
+ + )} +
+
+ ) : ( + <>{maintainersBadge} + )} +
+ ); +}; + +export default MaintainersWantedBadge; diff --git a/web/src/types.ts b/web/src/types.ts index 8c57702..937fff8 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -29,6 +29,21 @@ export interface Issue { repository: Repository; } +export interface MaintainersWanted { + enabled: boolean; + links?: MaintainersWantedLink[]; + contacts?: MaintainersWantedContact[]; +} + +export interface MaintainersWantedLink { + title?: string; + url: string; +} + +export interface MaintainersWantedContact { + github_handle: string; +} + export interface Project { name: string; display_name?: string; @@ -39,6 +54,7 @@ export interface Project { accepted_at: number; maturity: Maturity; foundation: Foundation; + maintainers_wanted?: MaintainersWanted; } export interface Repository {