Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow associating projects with multiple TAGs #780

Merged
merged 1 commit into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/cli/src/build/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ pub(crate) struct Item {
pub stack_overflow_url: Option<String>,

#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
pub tag: Option<Vec<String>>,

#[serde(skip_serializing_if = "Option::is_none")]
pub training_certifications: Option<String>,
Expand Down
4 changes: 2 additions & 2 deletions crates/cli/src/build/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ impl From<&data::Item> for Item {
}

// Tag
if let Some(tag) = &di.tag {
item.tag = Some(tag.clone());
if let Some(tags) = &di.tag {
item.tag = Some(tags.join(","));
}

// GitHub values
Expand Down
5 changes: 2 additions & 3 deletions crates/cli/src/build/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ pub(crate) fn collect_projects(landscape_data: &LandscapeData) -> Vec<Project> {
.iter()
.cloned()
.filter_map(|item| {
// Prepare maturity and tag
// Prepare maturity (projects must have it)
let maturity = item.maturity?;
let tag = item.tag?;

// Prepare sandbox date
let sandbox_at = if item.accepted_at == item.incubating_at {
Expand All @@ -66,7 +65,7 @@ pub(crate) fn collect_projects(landscape_data: &LandscapeData) -> Vec<Project> {
num_security_audits: num_security_audits.unwrap_or_default().to_string(),
last_security_audit: fmt_date(&last_security_audit),
sandbox_at: fmt_date(&sandbox_at),
tag: tag.to_string(),
tag: item.tag.unwrap_or_default().join(","),
};
Some(project)
})
Expand Down
18 changes: 9 additions & 9 deletions crates/core/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ impl LandscapeData {

// Iterate over items and set TAG when found
for item in &mut self.items {
// Only projects should be owned by a TAG
// Only projects should be associated to a TAG
if item.maturity.is_none() {
continue;
}
Expand All @@ -278,7 +278,7 @@ impl LandscapeData {

// Try to find the appropriate TAG based on the settings
if let Some(tag) = find_tag(item) {
item.tag = Some(tag);
item.tag = Some(vec![tag]);
}
}
}
Expand Down Expand Up @@ -662,7 +662,7 @@ pub struct Item {
pub summary: Option<ItemSummary>,

#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
pub tag: Option<Vec<String>>,

#[serde(skip_serializing_if = "Option::is_none")]
pub training_certifications: Option<String>,
Expand Down Expand Up @@ -1202,7 +1202,7 @@ mod tests {
};

landscape_data.add_tags(&settings);
assert_eq!(landscape_data.items[0].tag, Some("tag1".to_string()));
assert_eq!(landscape_data.items[0].tag, Some(vec!["tag1".to_string()]));
}

#[test]
Expand All @@ -1229,7 +1229,7 @@ mod tests {
};

landscape_data.add_tags(&settings);
assert_eq!(landscape_data.items[0].tag, Some("tag1".to_string()));
assert_eq!(landscape_data.items[0].tag, Some(vec!["tag1".to_string()]));
}

#[test]
Expand Down Expand Up @@ -1263,7 +1263,7 @@ mod tests {
landscape_data.items.push(Item {
category: "Category".to_string(),
maturity: Some("graduated".to_string()),
tag: Some("tag2".to_string()),
tag: Some(vec!["tag2".to_string()]),
..Default::default()
});

Expand All @@ -1281,7 +1281,7 @@ mod tests {
};

landscape_data.add_tags(&settings);
assert_eq!(landscape_data.items[0].tag, Some("tag2".to_string()));
assert_eq!(landscape_data.items[0].tag, Some(vec!["tag2".to_string()]));
}

#[test]
Expand Down Expand Up @@ -1419,7 +1419,7 @@ mod tests {
summary_release_rate: Some("summary_release_rate".to_string()),
summary_tags: Some("tag1,tag2".to_string()),
summary_use_case: Some("summary_use_case".to_string()),
tag: Some("tag".to_string()),
tag: Some(vec!["tag".to_string()]),
training_certifications: Some("training_certifications".to_string()),
training_type: Some("training_type".to_string()),
youtube_url: Some("youtube_url".to_string()),
Expand Down Expand Up @@ -1528,7 +1528,7 @@ mod tests {
tags: Some(vec!["tag1".to_string(), "tag2".to_string()]),
use_case: Some("summary_use_case".to_string()),
}),
tag: Some("tag".to_string()),
tag: Some(vec!["tag".to_string()]),
training_certifications: Some("training_certifications".to_string()),
training_type: Some("training_type".to_string()),
twitter_url: Some("twitter_url".to_string()),
Expand Down
20 changes: 11 additions & 9 deletions crates/core/src/data/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,15 @@ impl LandscapeData {
}
}

// Check tag name
if let Some(tag) = &extra.tag {
if !TAG_NAME.is_match(tag) {
return Err(format_err!(
"invalid tag (must use only lowercase letters and hyphens)"
))
.context(ctx);
// Check TAGs names
if let Some(tags) = &extra.tag {
for tag in tags {
if !TAG_NAME.is_match(tag) {
return Err(format_err!(
"invalid tag (must use only lowercase letters and hyphens)"
))
.context(ctx);
}
}
}
}
Expand Down Expand Up @@ -184,7 +186,7 @@ pub(super) struct ItemExtra {
pub summary_release_rate: Option<String>,
pub summary_tags: Option<String>,
pub summary_use_case: Option<String>,
pub tag: Option<String>,
pub tag: Option<Vec<String>>,
pub training_certifications: Option<String>,
pub training_type: Option<String>,
pub youtube_url: Option<String>,
Expand Down Expand Up @@ -475,7 +477,7 @@ mod tests {
homepage_url: "https://example.com".to_string(),
logo: "logo".to_string(),
extra: Some(ItemExtra {
tag: Some("Invalid Tag".to_string()),
tag: Some(vec!["Invalid Tag".to_string()]),
..Default::default()
}),
..Default::default()
Expand Down
6 changes: 3 additions & 3 deletions crates/core/src/datasets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ pub mod base {
pub oss: Option<bool>,

#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
pub tag: Option<Vec<String>>,
}

impl From<&data::Item> for Item {
Expand Down Expand Up @@ -629,7 +629,7 @@ mod tests {
name: "Item".to_string(),
oss: Some(true),
subcategory: "Subcategory 1".to_string(),
tag: Some("tag1".to_string()),
tag: Some(vec!["tag1".to_string()]),
..Default::default()
};

Expand All @@ -650,7 +650,7 @@ mod tests {
name: "Item".to_string(),
oss: Some(true),
subcategory: "Subcategory 1".to_string(),
tag: Some("tag1".to_string()),
tag: Some(vec!["tag1".to_string()]),
};
pretty_assertions::assert_eq!(item, expected_item);
}
Expand Down
10 changes: 6 additions & 4 deletions crates/core/src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,10 @@ impl ProjectsStats {
}

// Number of projects per TAG
if let Some(tag) = &item.tag {
increment(&mut stats.tag, tag, 1);
if let Some(tags) = &item.tag {
for tag in tags {
increment(&mut stats.tag, tag, 1);
}
}
}
}
Expand Down Expand Up @@ -646,7 +648,7 @@ mod tests {
accepted_at: NaiveDate::from_ymd_opt(2024, 4, 2),
incubating_at: NaiveDate::from_ymd_opt(2024, 4, 2),
graduated_at: NaiveDate::from_ymd_opt(2024, 4, 2),
tag: Some("tag1".to_string()),
tag: Some(vec!["tag1".to_string()]),
audits: Some(vec![ItemAudit {
date: NaiveDate::from_ymd_opt(2024, 4, 2).unwrap(),
..Default::default()
Expand All @@ -661,7 +663,7 @@ mod tests {
homepage_url: "https://project2.com".to_string(),
accepted_at: NaiveDate::from_ymd_opt(2024, 5, 1),
incubating_at: NaiveDate::from_ymd_opt(2024, 5, 2),
tag: Some("tag1".to_string()),
tag: Some(vec!["tag1".to_string()]),
audits: Some(vec![ItemAudit {
date: NaiveDate::from_ymd_opt(2024, 5, 2).unwrap(),
..Default::default()
Expand Down
9 changes: 5 additions & 4 deletions docs/config/data.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,12 @@ categories:
# More info: https://github.com/cncf/landscape/blob/master/docs/item_summary.md
summary_use_case: "Provides security for the software supply chain"

# Technical Advisory Group (TAG) this item belongs to (optional). When the
# automatic TAG mapping feature is enabled in the landscape settings, the TAG name
# used here must match exactly one of the TAGs listed in the landscape settings
# Technical Advisory Groups (TAG) this item is associated to (optional). When the
# automatic TAG mapping feature is enabled in the landscape settings, the TAG names
# used here must match exactly any of the TAGs listed in the landscape settings
# (provided it refers to the same TAG).
tag: security
tag:
- security

# YouTube URL (optional).
youtube_url: https://youtube.com/url
2 changes: 1 addition & 1 deletion docs/config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ screenshot_width: 3000

# TAGs (optional)
#
# Projects items can specify which TAG owns them in the `landscape.yml` file
# Projects items can specify which TAGs owns them in the `landscape.yml` file
# (by using the `tag` field in the `extra` item's section). However, sometimes
# this information is not available at the item level. This configuration
# section provides a mechanism to automatically asign a TAG to projects items
Expand Down
23 changes: 21 additions & 2 deletions ui/common/src/components/ItemModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AdditionalCategory, Item, Repository, SecurityAudit, SVGIconKind } from
import { cutString, getItemDescription } from '../utils';
import { formatProfitLabel } from '../utils/formatProfitLabel';
import { formatTAGName } from '../utils/formatTAGName';
import { getMainTag } from '../utils/getMainTag';
import { prettifyNumber } from '../utils/prettifyNumber';
import { AcquisitionsTable } from './AcquisitionsTable';
import { Badge } from './Badge';
Expand Down Expand Up @@ -272,9 +273,9 @@ export const ItemModalContent = (props: Props) => {
<FoundationBadge foundation={props.foundation} />
<MaturityBadge level={itemInfo()!.maturity!} class="mx-2" />

<Show when={!isUndefined(itemInfo()!.tag)}>
<Show when={!isUndefined(itemInfo()!.tag) && itemInfo()!.tag!.length > 0}>
<div class={`badge text-uppercase rounded-0 me-2 ${BadgeOutlineDark} ${TagBadge}`}>
TAG {formatTAGName(itemInfo()!.tag!)}
TAG {formatTAGName(getMainTag(itemInfo()!.tag!))}
</div>
</Show>

Expand Down Expand Up @@ -494,6 +495,24 @@ export const ItemModalContent = (props: Props) => {
</For>
</div>
</Show>
{/* Additional tags */}
<Show when={!isUndefined(itemInfo()!.tag) && itemInfo()!.tag!.length > 1}>
<div class={`fw-bold text-uppercase mt-4 mb-3 ${TitleInSection}`}>Additional tags</div>
<div class="d-flex flex-row flex-wrap align-items-center mb-2">
<For each={itemInfo()!.tag!}>
{(tag, index) => {
return (
// Do not show the first tag as it is already displayed in the main section
<Show when={index() > 0}>
<div class={`badge rounded-0 me-2 mb-2 text-truncate text-uppercase ${BadgeOutlineDark}`}>
TAG {formatTAGName(tag)}
</div>
</Show>
);
}}
</For>
</div>
</Show>
<Show when={!isNull(props.parentInfo)}>
{/* Parent project */}
<ParentProject
Expand Down
1 change: 1 addition & 0 deletions ui/common/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
formatProfitLabel,
formatTAGName,
getItemDescription,
getMainTag,
prettifyNumber,
sortObjectByValue,
} from './utils';
Expand Down
2 changes: 1 addition & 1 deletion ui/common/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface BaseItem {
description?: string;
featured?: Featured;
maturity?: string;
tag?: string;
tag?: string[];
additional_categories?: AdditionalCategory[];
}

Expand Down
4 changes: 4 additions & 0 deletions ui/common/src/utils/getMainTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Get the main tag from a list of tags.
export const getMainTag = (tags: string[]): string => {
return tags[0];
};
1 change: 1 addition & 0 deletions ui/common/src/utils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export { cutString } from './cutString';
export { formatProfitLabel } from './formatProfitLabel';
export { formatTAGName } from './formatTAGName';
export { getItemDescription } from './getItemDescription';
export { getMainTag } from './getMainTag';
export { prettifyNumber } from './prettifyNumber';
export { sortObjectByValue } from './sortObjectByValue';
5 changes: 3 additions & 2 deletions ui/webapp/src/layout/explore/card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
formatTAGName,
FoundationBadge,
getItemDescription,
getMainTag,
Image,
MaturityBadge,
prettifyNumber,
Expand Down Expand Up @@ -171,11 +172,11 @@ const Card = (props: Props) => {
</Switch>
</div>

<Show when={!isUndefined(props.item.tag)}>
<Show when={!isUndefined(props.item.tag) && props.item.tag!.length > 0}>
<div
class={`badge border rounded-0 tagBadge ms-4 mw-100 text-truncate text-uppercase ${styles.badgeOutlineDark}`}
>
TAG {formatTAGName(props.item.tag!)}
TAG {formatTAGName(getMainTag(props.item.tag!))}
</div>
</Show>
</div>
Expand Down
3 changes: 2 additions & 1 deletion ui/webapp/src/layout/explore/mobile/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
formatTAGName,
FoundationBadge,
getItemDescription,
getMainTag,
Image,
MaturityBadge,
prettifyNumber,
Expand Down Expand Up @@ -119,7 +120,7 @@ const Card = (props: Props) => {
<div
class={`badge border rounded-0 tagBadge ms-4 mw-100 text-truncate text-uppercase ${styles.badgeOutlineDark}`}
>
TAG {formatTAGName(props.item.tag!)}
TAG {formatTAGName(getMainTag(props.item.tag!))}
</div>
</Show>
</div>
Expand Down
8 changes: 0 additions & 8 deletions ui/webapp/src/layout/stats/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ const Content = () => {
const [stats, setStats] = createSignal<Stats>();
const from = () => (state() as StateContent).from || undefined;

const sumValues = (numbers: number[]): number => {
return numbers.reduce((a, b) => a + b, 0);
};

onMount(() => {
setStats(window.statsDS);
if (from() === 'header') {
Expand Down Expand Up @@ -154,10 +150,6 @@ const Content = () => {
);
}}
</For>
<tr>
<td class="fw-semibold text-uppercase">Total</td>
<td class="text-end fw-semibold">{sumValues(Object.values(stats()!.projects!.tag!))}</td>
</tr>
</tbody>
</table>
</div>
Expand Down
2 changes: 1 addition & 1 deletion ui/webapp/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface BaseItem {
description?: string;
featured?: Featured;
maturity?: string;
tag?: string;
tag?: string[];
additional_categories?: AdditionalCategory[];
}

Expand Down
Loading