Skip to content

Commit

Permalink
Highlight projects that want maintainers (#283)
Browse files Browse the repository at this point in the history
Closes #249

Signed-off-by: Sergio Castaño Arteaga <[email protected]>
Signed-off-by: Cintia Sanchez Garcia <[email protected]>
Co-authored-by: Sergio Castaño Arteaga <[email protected]>
Co-authored-by: Cintia Sanchez Garcia <[email protected]>
  • Loading branch information
tegioz and cynthia-sg authored May 2, 2023
1 parent a1d6155 commit a925c54
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 4 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 29 additions & 1 deletion clotributor-registrar/src/registrar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub(crate) struct Project {
pub devstats_url: Option<String>,
pub accepted_at: Option<String>,
pub maturity: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub maintainers_wanted: Option<MaintainersWanted>,
pub digest: Option<String>,
pub repositories: Vec<Repository>,
}
Expand All @@ -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<Vec<Link>>,
#[serde(skip_serializing_if = "Option::is_none")]
contacts: Option<Vec<Contact>>,
}

/// Represents some information about a link.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Link {
title: Option<String>,
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<()> {
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions database/migrations/functions/issues/search_issues.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions database/migrations/functions/projects/register_project.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ begin
devstats_url,
accepted_at,
maturity,
maintainers_wanted,
digest,
foundation_id
) values (
Expand All @@ -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
)
Expand All @@ -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;

Expand Down
1 change: 1 addition & 0 deletions database/migrations/schema/001_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions web/src/layout/common/Card.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 27 additions & 3 deletions web/src/layout/common/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +35,8 @@ const Card = (props: Props) => {
const { ctx } = useContext(AppContext);
const { effective } = ctx.prefs.theme;
const [availableTopics, setAvailableTopics] = useState<string[]>([]);
const isMaintainersWantedAvailable: boolean =
!isUndefined(props.issue.project.maintainers_wanted) && props.issue.project.maintainers_wanted.enabled;

const searchByText = (text: string) => {
navigate({
Expand Down Expand Up @@ -124,6 +127,13 @@ const Card = (props: Props) => {
</div>

<div className="d-flex flex-row align-items-center ms-2">
{isMaintainersWantedAvailable && (
<MaintainersWantedBadge
className="d-none d-sm-flex me-2"
maintainers_wanted={props.issue.project.maintainers_wanted!}
buttonStyle
/>
)}
<MaturityBadge
maturityLevel={props.issue.project.maturity}
className="d-none d-sm-flex me-2"
Expand All @@ -136,16 +146,26 @@ const Card = (props: Props) => {
</div>
</div>

<div className={`d-none d-md-flex flex-column flex-sm-row align-items-center ps-3 ${styles.projectWrapper}`}>
<div className={`d-none d-xl-flex align-items-center justify-content-center ${styles.imageWrapper}`}>
<div
className={`d-none d-md-flex flex-column flex-sm-row align-items-center ps-3 position-relative ${styles.projectWrapper}`}
>
<div
className={classNames('d-none d-xl-flex align-items-center justify-content-center', styles.imageWrapper, {
[styles.negativeMarginTop]: isMaintainersWantedAvailable,
})}
>
<Image
alt={`${props.issue.project.display_name || props.issue.project.name} logo`}
url={props.issue.project.logo_url}
dark_url={props.issue.project.logo_dark_url}
effective_theme={effective}
/>
</div>
<div className="ms-0 ms-xl-3 flex-grow-1 w-100 truncateWrapper">
<div
className={classNames('ms-0 ms-xl-3 flex-grow-1 w-100 truncateWrapper', {
[styles.negativeMarginTop]: isMaintainersWantedAvailable,
})}
>
<div className="p-0 p-xl-2 pe-xl-0">
<div className="d-flex flex-row align-items-center">
<div className="d-flex flex-column w-100 truncateWrapper">
Expand Down Expand Up @@ -212,6 +232,10 @@ const Card = (props: Props) => {
</div>
</div>
</div>

{isMaintainersWantedAvailable && (
<MaintainersWantedBadge maintainers_wanted={props.issue.project.maintainers_wanted!} />
)}
</div>

<div className={`flex-grow-1 p-3 text-muted ${styles.issueContent}`}>
Expand Down
46 changes: 46 additions & 0 deletions web/src/layout/common/MaintainersWantedBadge.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
107 changes: 107 additions & 0 deletions web/src/layout/common/MaintainersWantedBadge.tsx
Original file line number Diff line number Diff line change
@@ -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 ? (
<div
className={`position-relative text-center text-uppercase fw-bold w-100 ${styles.badge} ${styles.badgeInBottom}`}
>
<div className="d-flex flex-row align-items-center justify-content-center">
<div>Maintainers wanted</div>
<MdInfoOutline className={`ms-2 position-relative ${styles.icon}`} />
</div>
</div>
) : (
<div className={`d-none d-sm-flex badge rounded-0 me-2 ${styles.badge}`}>
<div className="d-flex flex-row align-items-center justify-content-center">
<MdInfoOutline className="me-1" />
<div>Maintainers wanted</div>
</div>
</div>
)}
</>
);

return (
<div className={classNames({ [`position-absolute ${styles.wrapper}`]: !isButton })}>
{activeTooltip ? (
<DropdownOnHover
dropdownClassName={styles.dropdown}
width={isButton ? 300 : 500}
linkContent={maintainersBadge}
tooltipStyle
>
<div className="text-start p-2">
{props.maintainers_wanted.links && props.maintainers_wanted.links.length > 0 && (
<>
<div className="border-bottom border-1 pb-1 mb-2 fw-bold">Links</div>
<div
className={classNames('mb-1', {
'mb-3': props.maintainers_wanted.contacts && props.maintainers_wanted.contacts.length > 0,
})}
>
{props.maintainers_wanted.links.map((link: MaintainersWantedLink, index: number) => {
return (
<div key={`link_${index}_${link.url}`} className="d-flex flex-row align-items-center ms-2">
<RxDotFilled className={`me-2 position-relative ${styles.dot}`} />
<ExternalLink className="text-truncate w-100" href={link.url}>
{link.title || link.url}
</ExternalLink>
</div>
);
})}
</div>
</>
)}
{props.maintainers_wanted.contacts && props.maintainers_wanted.contacts.length > 0 && (
<>
<div className="border-bottom border-1 pb-1 mb-2 fw-bold">Contacts</div>
<div className="mb-1">
{props.maintainers_wanted.contacts.map((contact: MaintainersWantedContact, index: number) => {
return (
<div
key={`contact_${index}_${contact.github_handle}`}
className="d-flex flex-row align-items-center ms-2"
>
<RxDotFilled className={`me-2 position-relative ${styles.dot}`} />
<ExternalLink
className="text-truncate w-100"
href={`https://github.com/${contact.github_handle}`}
>
{contact.github_handle}
</ExternalLink>
</div>
);
})}
</div>
</>
)}
</div>
</DropdownOnHover>
) : (
<>{maintainersBadge}</>
)}
</div>
);
};

export default MaintainersWantedBadge;
16 changes: 16 additions & 0 deletions web/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,6 +54,7 @@ export interface Project {
accepted_at: number;
maturity: Maturity;
foundation: Foundation;
maintainers_wanted?: MaintainersWanted;
}

export interface Repository {
Expand Down

0 comments on commit a925c54

Please sign in to comment.