mv microsite-next microsite
Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
.bannerSection {
|
||||
display: block;
|
||||
|
||||
&:global(.greyBackground) {
|
||||
background: linear-gradient(90.49deg, #121212 15.36%, #282828 70.44%);
|
||||
}
|
||||
|
||||
&:global(.greenGradientBackground) {
|
||||
background: linear-gradient(70.44deg, #121212 55%, #5d817b);
|
||||
}
|
||||
|
||||
&:global(.greenBottomGradientBackground) {
|
||||
background: linear-gradient(
|
||||
178.64deg,
|
||||
hsla(0, 0%, 100%, 0) 57.61%,
|
||||
hsla(0, 0%, 100%, 0.17) 127.71%
|
||||
),
|
||||
linear-gradient(
|
||||
144.35deg,
|
||||
rgba(98, 197, 179, 0) 56.68%,
|
||||
rgba(98, 197, 179, 0.59) 109.25%
|
||||
),
|
||||
linear-gradient(
|
||||
192.29deg,
|
||||
rgba(155, 240, 225, 0) 54.17%,
|
||||
rgba(155, 240, 225, 0.67) 137.34%
|
||||
);
|
||||
}
|
||||
|
||||
&:global(.greenCallToActionGradientBackground) {
|
||||
background: linear-gradient(87.29deg, #9bf0e1 17.71%, #36baa2 80.85%);
|
||||
|
||||
color: var(--ifm-color-gray-900);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
import bannerStyles from './bannerSection.module.scss';
|
||||
|
||||
export type IBannerSectionProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
greyBackground?: boolean;
|
||||
greenGradientBackground?: boolean;
|
||||
greenBottomGradientBackground?: boolean;
|
||||
greenCallToActionGradientBackground?: boolean;
|
||||
}>;
|
||||
|
||||
export const BannerSection = ({
|
||||
children,
|
||||
className,
|
||||
greyBackground = false,
|
||||
greenGradientBackground = false,
|
||||
greenBottomGradientBackground = false,
|
||||
greenCallToActionGradientBackground = false,
|
||||
}: IBannerSectionProps) => (
|
||||
<section
|
||||
className={clsx(bannerStyles.bannerSection, className, {
|
||||
greyBackground,
|
||||
greenGradientBackground,
|
||||
greenBottomGradientBackground,
|
||||
greenCallToActionGradientBackground,
|
||||
})}
|
||||
>
|
||||
<div className="container padding-horiz--lg padding-vert--xl">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
$desktopUpBreakpoint: 997px;
|
||||
|
||||
.sectionGridContainer {
|
||||
gap: 2rem;
|
||||
display: grid;
|
||||
|
||||
:global(.gridHeader) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.gridContainer {
|
||||
gap: 2rem;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
|
||||
@media (min-width: $desktopUpBreakpoint) {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import bannerGridStyles from './bannerSectionGrid.module.scss';
|
||||
|
||||
export type IBannerSectionColumnsProps = PropsWithChildren<{
|
||||
header?: ReactNode;
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
export const BannerSectionGrid = ({
|
||||
header,
|
||||
children,
|
||||
className,
|
||||
}: IBannerSectionColumnsProps) => {
|
||||
return (
|
||||
<div className={clsx(bannerGridStyles.sectionGridContainer, className)}>
|
||||
{header && <div className="gridHeader">{header}</div>}
|
||||
|
||||
<div className={bannerGridStyles.gridContainer}>
|
||||
{Array.isArray(children)
|
||||
? children.map((child, index) =>
|
||||
React.cloneElement(child, {
|
||||
key: index,
|
||||
className: clsx(child.props.className),
|
||||
}),
|
||||
)
|
||||
: children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
.contentBlock {
|
||||
height: 100%;
|
||||
|
||||
gap: 1.5rem;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
|
||||
> *,
|
||||
h1,
|
||||
h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.contentBlockTitle) {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
:global(.bulletLine) {
|
||||
width: 50px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:global(.actionButtons) {
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
align-self: end;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, auto));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
import clsx from 'clsx';
|
||||
import React, { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import contentBlockStyles from './contentBlock.module.scss';
|
||||
|
||||
export interface ContentBlockActionButtonProps {
|
||||
label: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export type IContentBlockProps = PropsWithChildren<{
|
||||
title?: ReactNode;
|
||||
className?: string;
|
||||
topImgSrc?: string;
|
||||
hasBulletLine?: boolean;
|
||||
actionButtons?: ContentBlockActionButtonProps[];
|
||||
}>;
|
||||
|
||||
export const ContentBlock = ({
|
||||
children,
|
||||
title,
|
||||
className,
|
||||
topImgSrc,
|
||||
hasBulletLine,
|
||||
actionButtons,
|
||||
}: IContentBlockProps) => (
|
||||
<div className={clsx(className, contentBlockStyles.contentBlock)}>
|
||||
{topImgSrc && <img src={topImgSrc} alt={topImgSrc} />}
|
||||
|
||||
<div className="contentBlockTitle">
|
||||
{hasBulletLine && <div className="bulletLine" />}
|
||||
|
||||
{title && React.isValidElement(title) ? title : <h2>{title}</h2>}
|
||||
</div>
|
||||
|
||||
{children && React.isValidElement(children) ? children : <p>{children}</p>}
|
||||
|
||||
{actionButtons && (
|
||||
<div className="actionButtons">
|
||||
{actionButtons.map(({ link, label }, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
className="button button--primary button--lg"
|
||||
to={link}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface ICardData {
|
||||
header: React.ReactNode;
|
||||
body: React.ReactNode;
|
||||
footer: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SimpleCard = ({ header, body, footer }: ICardData) => (
|
||||
<div className="card">
|
||||
<div className="card__header">{header}</div>
|
||||
|
||||
<div className="card__body">{body}</div>
|
||||
|
||||
<div className="card__footer">{footer}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
:root {
|
||||
--ifm-color-primary-lightest: #66d2bf;
|
||||
--ifm-color-primary-lighter: #4acab3;
|
||||
--ifm-color-primary-light: #40c8af;
|
||||
--ifm-color-primary: #36baa2;
|
||||
--ifm-color-primary-dark: #31a792;
|
||||
--ifm-color-primary-darker: #2e9e8a;
|
||||
--ifm-color-primary-darkest: #268271;
|
||||
}
|
||||
|
||||
.footerLogo {
|
||||
background-image: url(/img/logo.svg);
|
||||
background-repeat: no-repeat;
|
||||
height: 40px;
|
||||
width: 180px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
/* logo.md */
|
||||
.logoWhite {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* #region Utility API Styling */
|
||||
.textGreen {
|
||||
color: #82b366;
|
||||
}
|
||||
.textBlue {
|
||||
color: #6c8ebf;
|
||||
}
|
||||
.textPurple {
|
||||
color: #9673a6;
|
||||
}
|
||||
.textYellow {
|
||||
color: #d6b656;
|
||||
}
|
||||
.textRed {
|
||||
color: #b85450;
|
||||
}
|
||||
.textAlignCenter {
|
||||
text-align: center;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region Utility component styles */
|
||||
.bulletLine {
|
||||
height: 3px;
|
||||
background: linear-gradient(
|
||||
89.75deg,
|
||||
#9bf0e1 -1.5%,
|
||||
rgba(155, 240, 225, 0) 111.48%
|
||||
);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* prism magic comments https://docusaurus.io/docs/markdown-features/code-blocks#custom-magic-comments */
|
||||
|
||||
.code-block-add-line {
|
||||
background-color: rgba(0, 255, 0, 0.125);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
border-left: 3px solid rgba(0, 255, 0, 0.5);
|
||||
}
|
||||
|
||||
.code-block-remove-line {
|
||||
background-color: rgba(255, 0, 0, 0.125);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
border-left: 3px solid rgba(255, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -0,0 +1,28 @@
|
||||
.communityPage {
|
||||
// --ifm-spacing-horizontal: 2rem;
|
||||
|
||||
font-size: 1.25rem;
|
||||
|
||||
h1 {
|
||||
font-size: 54px;
|
||||
line-height: 56px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
height: 100%;
|
||||
align-items: stretch !important;
|
||||
grid-template-rows: auto 1fr;
|
||||
|
||||
ul {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import { BannerSection } from '@site/src/components/bannerSection/bannerSection';
|
||||
import { BannerSectionGrid } from '@site/src/components/bannerSection/bannerSectionGrid';
|
||||
import Layout from '@theme/Layout';
|
||||
import { clsx } from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { ContentBlock } from '../../components/contentBlock/contentBlock';
|
||||
import communityStyles from './community.module.scss';
|
||||
|
||||
interface ICollectionItem {
|
||||
title?: string;
|
||||
content: React.ReactNode;
|
||||
label: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
const Community = () => {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
|
||||
//#region Collection Data
|
||||
const communityListItems: ICollectionItem[] = [
|
||||
{
|
||||
content: 'Chat and get support on our',
|
||||
label: 'Discord',
|
||||
link: 'https://discord.gg/backstage-687207715902193673',
|
||||
},
|
||||
{
|
||||
content: 'Get into contributing with the',
|
||||
label: 'Good First Issues',
|
||||
link: 'https://github.com/backstage/backstage/contribute',
|
||||
},
|
||||
{
|
||||
content: 'Subscribe to the',
|
||||
label: 'Community newsletter',
|
||||
link: 'https://info.backstage.spotify.com/newsletter_subscribe',
|
||||
},
|
||||
{
|
||||
content: 'Join the',
|
||||
label: 'Twitter community',
|
||||
link: 'https://twitter.com/i/communities/1494019781716062215',
|
||||
},
|
||||
];
|
||||
|
||||
const officialInitiatives: ICollectionItem[] = [
|
||||
{
|
||||
title: 'Community sessions',
|
||||
content:
|
||||
'Maintainers and adopters meet monthly to share updates, demos, and ideas. Yep, all sessions are recorded!',
|
||||
link: '/on-demand',
|
||||
label: 'Join a session',
|
||||
},
|
||||
{
|
||||
title: 'Newsletter',
|
||||
content:
|
||||
"The official monthly Backstage newsletter. Don't miss the latest news from your favorite project!",
|
||||
link: 'https://info.backstage.spotify.com/newsletter_subscribe',
|
||||
label: 'Subscribe',
|
||||
},
|
||||
{
|
||||
title: 'Contributor Spotlight',
|
||||
content:
|
||||
"A recognition for valuable community work. Nominate contributing members for their efforts! We'll put them in the spotlight ❤️",
|
||||
link: '/nominate',
|
||||
label: 'Nominate now',
|
||||
},
|
||||
];
|
||||
|
||||
const communityInitiatives: ICollectionItem[] = [
|
||||
{
|
||||
title: 'Open Mic Meetup',
|
||||
content: (
|
||||
<>
|
||||
A casual get together of Backstage users sharing their experiences and
|
||||
helping each other. Hosted by{' '}
|
||||
<Link to="https://roadie.io/">Roadie.io</Link> and{' '}
|
||||
<Link to="https://frontside.com/">Frontside Software</Link>.
|
||||
</>
|
||||
),
|
||||
link: 'https://backstage-openmic.com/',
|
||||
label: 'Learn more',
|
||||
},
|
||||
{
|
||||
title: 'Backstage Weekly Newsletter',
|
||||
content: (
|
||||
<>
|
||||
A weekly newsletter with news, updates and things community from your
|
||||
friends at <Link to="https://roadie.io/">Roadie.io</Link>.
|
||||
</>
|
||||
),
|
||||
link: 'https://roadie.io/backstage-weekly/',
|
||||
label: 'Learn more',
|
||||
},
|
||||
];
|
||||
|
||||
const trainingNCertifications: ICollectionItem[] = [
|
||||
{
|
||||
title: 'Open Mic Meetup',
|
||||
content:
|
||||
'This is a course produced and curated by the Linux Foundation. This course introduces you to Backstage and how to get started with the project.',
|
||||
link: 'https://training.linuxfoundation.org/training/introduction-to-backstage-developer-portals-made-easy-lfs142x/',
|
||||
label: 'Learn more',
|
||||
},
|
||||
];
|
||||
|
||||
const partners: { name: string; url: string; logo: string }[] = [
|
||||
{
|
||||
name: 'Frontside Software',
|
||||
url: 'https://frontside.com/backstage/',
|
||||
logo: 'img/partner-logo-frontside.png',
|
||||
},
|
||||
{
|
||||
name: 'Roadie',
|
||||
url: 'https://roadie.io/',
|
||||
logo: 'img/partner-logo-roadie.png',
|
||||
},
|
||||
{
|
||||
name: 'ThoughtWorks',
|
||||
url: 'https://www.thoughtworks.com',
|
||||
logo: 'img/partner-logo-thoughtworks.png',
|
||||
},
|
||||
{
|
||||
name: 'VMWare',
|
||||
url: 'https://www.vmware.com',
|
||||
logo: 'img/partner-logo-vmware.png',
|
||||
},
|
||||
];
|
||||
//#endregion
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className={clsx(communityStyles.communityPage)}>
|
||||
<BannerSection greyBackground>
|
||||
<BannerSectionGrid>
|
||||
<ContentBlock
|
||||
className="padding-right--xl"
|
||||
title={<h1>Backstage Community</h1>}
|
||||
>
|
||||
Join the vibrant community around Backstage through social media
|
||||
and different meetups. To ensure that you have a welcoming
|
||||
environment, we follow the
|
||||
<Link to="https://github.com/cncf/foundation/blob/master/code-of-conduct.md">
|
||||
{' '}
|
||||
CNCF Code of Conduct{' '}
|
||||
</Link>
|
||||
in everything we do.
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock
|
||||
className={clsx(
|
||||
'padding-left--xl',
|
||||
communityStyles.listContainer,
|
||||
)}
|
||||
title="Get started in our community!"
|
||||
>
|
||||
<ul>
|
||||
{communityListItems.map(
|
||||
({ content: text, link, label }, index) => (
|
||||
<li key={index}>
|
||||
<p className="margin-bottom--none">
|
||||
{text} <Link to={link}>{label}</Link>
|
||||
</p>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</ContentBlock>
|
||||
</BannerSectionGrid>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenBottomGradientBackground>
|
||||
<BannerSectionGrid
|
||||
header={
|
||||
<>
|
||||
<h2 className="text--primary">Offical Backstage initiatives</h2>
|
||||
|
||||
<h1>Stay tuned to the latest developments</h1>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{officialInitiatives.map(
|
||||
({ title, content, link, label }, index) => (
|
||||
<ContentBlock
|
||||
key={index}
|
||||
title={title}
|
||||
hasBulletLine
|
||||
actionButtons={[
|
||||
{
|
||||
link,
|
||||
label,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{content}
|
||||
</ContentBlock>
|
||||
),
|
||||
)}
|
||||
</BannerSectionGrid>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greyBackground>
|
||||
<BannerSectionGrid
|
||||
header={<h2 className="text--primary">Community initiatives</h2>}
|
||||
>
|
||||
{communityInitiatives.map(
|
||||
({ title, content, link, label }, index) => (
|
||||
<ContentBlock
|
||||
key={index}
|
||||
title={title}
|
||||
hasBulletLine
|
||||
actionButtons={[
|
||||
{
|
||||
link,
|
||||
label,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>{content}</p>
|
||||
</ContentBlock>
|
||||
),
|
||||
)}
|
||||
</BannerSectionGrid>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection>
|
||||
<BannerSectionGrid
|
||||
header={
|
||||
<h2 className="text--primary">Trainings and Certifications</h2>
|
||||
}
|
||||
>
|
||||
{trainingNCertifications.map(
|
||||
({ title, content, link, label }, index) => (
|
||||
<ContentBlock
|
||||
key={index}
|
||||
title={title}
|
||||
hasBulletLine
|
||||
actionButtons={[
|
||||
{
|
||||
link,
|
||||
label,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>{content}</p>
|
||||
</ContentBlock>
|
||||
),
|
||||
)}
|
||||
</BannerSectionGrid>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greyBackground>
|
||||
<BannerSectionGrid
|
||||
header={<h2 className="text--primary">Commercial Partners</h2>}
|
||||
>
|
||||
{partners.map(({ name, url, logo }, index) => (
|
||||
<div key={index}>
|
||||
<Link to={url}>
|
||||
<img src={`${siteConfig.baseUrl}${logo}`} alt={name} />
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</BannerSectionGrid>
|
||||
</BannerSection>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Community;
|
||||
@@ -0,0 +1,24 @@
|
||||
.communityPage {
|
||||
// --ifm-spacing-horizontal: 2rem;
|
||||
|
||||
font-size: 1.25rem;
|
||||
|
||||
h1 {
|
||||
font-size: 54px;
|
||||
line-height: 56px;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.banner {
|
||||
p {
|
||||
flex: 1;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import { BannerSection } from '@site/src/components/bannerSection/bannerSection';
|
||||
import { BannerSectionGrid } from '@site/src/components/bannerSection/bannerSectionGrid';
|
||||
import { ContentBlock } from '@site/src/components/contentBlock/contentBlock';
|
||||
import Layout from '@theme/Layout';
|
||||
import { clsx } from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import demosStyles from './demos.module.scss';
|
||||
|
||||
interface IDemoItem {
|
||||
title: string;
|
||||
content: React.ReactNode;
|
||||
actionItemLink?: string;
|
||||
media: {
|
||||
type: 'image' | 'video';
|
||||
link: string;
|
||||
};
|
||||
}
|
||||
|
||||
const Demos = () => {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
|
||||
//#region Collection Data
|
||||
const demoItems: IDemoItem[] = [
|
||||
{
|
||||
title: 'See us in action',
|
||||
content: (
|
||||
<>
|
||||
<p>
|
||||
Watch the videos below to get an introduction to Backstage and to
|
||||
see how we use different plugins to customize{' '}
|
||||
<Link to="https://engineering.atspotify.com/2020/04/21/how-we-use-backstage-at-spotify/">
|
||||
our internal version of Backstage at Spotify
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
To see how other companies have already started using Backstage,
|
||||
watch these presentations from{' '}
|
||||
<Link to="https://youtu.be/rRphwXeq33Q?t=1508">Expedia</Link>,{' '}
|
||||
<Link to="https://youtu.be/6sg5uMCLxTA?t=153">Zalando</Link>, and{' '}
|
||||
<Link to="https://youtu.be/UZTVjv-AvZA?t=188">TELUS</Link>. For
|
||||
more, join our{' '}
|
||||
<Link to="https://github.com/backstage/community">
|
||||
Community Sessions
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
To explore the UI and basic features of Backstage firsthand, go to:{' '}
|
||||
<Link to="https://demo.backstage.io">demo.backstage.io</Link>. (Tip:{' '}
|
||||
click “All” to view all the example components in the software
|
||||
catalog.)
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
media: {
|
||||
type: 'image',
|
||||
link: `${siteConfig.baseUrl}img/demo-screen.png`,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Introduction to Backstage',
|
||||
content:
|
||||
'Backstage is an open source platform for building developer portals. We’ve been using our homegrown version at Spotify for years — so it’s already packed with features. (We have over 120 internal plugins, built by 60 different teams.) In this live demo recording, Stefan Ålund, product manager for Backstage, tells the origin story of Backstage and gives you a tour of how we use it here at Spotify.',
|
||||
actionItemLink: 'https://www.youtube.com/watch?v=1XtJ5FAOjPk',
|
||||
media: {
|
||||
type: 'video',
|
||||
link: 'https://www.youtube.com/embed/1XtJ5FAOjPk',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Control cloud costs',
|
||||
content: (
|
||||
<>
|
||||
<p>
|
||||
How do you control cloud costs while maintaining the speed and
|
||||
independence of your development teams? With the{' '}
|
||||
<Link to="https://backstage.io/plugins">Cost Insights plugin</Link>{' '}
|
||||
for Backstage, managing cloud costs becomes just another part of an
|
||||
engineer’s daily development process. They get a clear view of their
|
||||
spending — and can decide for themselves how they want to optimize
|
||||
it. Learn more about the{' '}
|
||||
<Link to="https://backstage.io/blog/2020/10/22/cost-insights-plugin">
|
||||
Cost Insights plugin
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
actionItemLink: 'https://youtu.be/YLAd5hdXR_Q',
|
||||
media: {
|
||||
type: 'video',
|
||||
link: 'https://www.youtube.com/embed/YLAd5hdXR_Q',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Make documentation easy',
|
||||
content: (
|
||||
<>
|
||||
<p>
|
||||
Documentation! Everyone needs it, no one wants to create it, and no
|
||||
one can ever find it. Backstage follows a “docs like code” approach:
|
||||
you write documentation in Markdown files right alongside your code.
|
||||
This makes documentation easier to create, maintain, find — and, you
|
||||
know, actually use. This demo video showcases Spotify’s internal
|
||||
version of TechDocs. Learn more about{' '}
|
||||
<Link to="https://backstage.io/blog/2020/09/08/announcing-tech-docs">
|
||||
TechDocs
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
actionItemLink: 'https://youtu.be/mOLCgdPw1iA',
|
||||
media: {
|
||||
type: 'video',
|
||||
link: 'https://www.youtube.com/embed/mOLCgdPw1iA',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Manage your tech health',
|
||||
content:
|
||||
'Instead of manually updating a spreadsheet, what if you had a beautiful dashboard that could give you an instant, interactive picture of your entire org’s tech stack? That’s how we do it at Spotify. With our Tech Insights plugin for Backstage, anyone at Spotify can see which version of which software anyone else at Spotify is using — and a whole a lot more. From managing migrations to fighting tech entropy, Backstage makes managing our tech health actually kind of pleasant.',
|
||||
actionItemLink:
|
||||
'https://www.youtube.com/watch?v=K3xz6VAbgH8&list=PLf1KFlSkDLIBtMGwRDfaVlKMqTMrjD2yO&index=6',
|
||||
media: {
|
||||
type: 'video',
|
||||
link: 'https://www.youtube.com/embed/K3xz6VAbgH8',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Create a microservice',
|
||||
content:
|
||||
'You’re a Spotify engineer about to build a new microservice (or any component) using Spring Boot. Where do you start? Search for a quick start guide online? Create an empty repo on GitHub? Copy and paste an old project? Nope. Just go to Backstage, and you’ll be up and running in two minutes — with a “Hello World” app, CI, and documentation all automatically set up and configured in a standardized way.',
|
||||
actionItemLink: 'https://www.youtube.com/watch?v=U1iwe3L5pzc',
|
||||
media: {
|
||||
type: 'video',
|
||||
link: 'https://www.youtube.com/embed/U1iwe3L5pzc',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Search all your services',
|
||||
content:
|
||||
'All of Spotify’s services are automatically indexed in Backstage. So our engineers can stop playing detective — no more spamming Slack channels asking if anyone knows who owns a particular service and where you can find its API, only to discover that the owner went on sabbatical three months ago and you have to hunt them down on a mountain in Tibet where they’re on a 12-day silent meditation retreat. At Spotify, anyone can always find anyone else’s service, inspect its APIs, and contact its current owner — all with one search.',
|
||||
actionItemLink: 'https://www.youtube.com/watch?v=vcDL9tOv7Eo',
|
||||
media: {
|
||||
type: 'video',
|
||||
link: 'https://www.youtube.com/embed/vcDL9tOv7Eo',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Manage data pipelines',
|
||||
content:
|
||||
'We manage a lot of data pipelines (also known as workflows) here at Spotify. So, of course, we made a great workflows plugin for our version of Backstage. All our workflow tools — including a scheduler, log inspector, data lineage graph, and configurable alerts — are integrated into one simple interface.',
|
||||
actionItemLink: 'https://www.youtube.com/watch?v=rH46MLNZIPM',
|
||||
media: {
|
||||
type: 'video',
|
||||
link: 'https://www.youtube.com/embed/rH46MLNZIPM',
|
||||
},
|
||||
},
|
||||
];
|
||||
//#endregion
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className={clsx(demosStyles.communityPage)}>
|
||||
{demoItems.map((demoItem, index) => (
|
||||
<BannerSection
|
||||
key={index}
|
||||
greyBackground={index % 2 === 0}
|
||||
className={demosStyles.banner}
|
||||
>
|
||||
<BannerSectionGrid>
|
||||
<ContentBlock
|
||||
title={<h1>{demoItem.title}</h1>}
|
||||
actionButtons={[
|
||||
{
|
||||
link: demoItem.actionItemLink,
|
||||
label: 'WATCH NOW',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{demoItem.content}
|
||||
</ContentBlock>
|
||||
|
||||
<div className={clsx()}>
|
||||
{demoItem.media.type === 'image' ? (
|
||||
<img src={demoItem.media.link} alt={demoItem.title} />
|
||||
) : (
|
||||
<iframe
|
||||
src={demoItem.media.link}
|
||||
title={demoItem.title}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
)}
|
||||
</div>
|
||||
</BannerSectionGrid>
|
||||
</BannerSection>
|
||||
))}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Demos;
|
||||
@@ -0,0 +1,538 @@
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import Link from '@docusaurus/Link';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import { BannerSection } from '@site/src/components/bannerSection/bannerSection';
|
||||
import { BannerSectionGrid } from '@site/src/components/bannerSection/bannerSectionGrid';
|
||||
import { ContentBlock } from '@site/src/components/contentBlock/contentBlock';
|
||||
import Layout from '@theme/Layout';
|
||||
import { clsx } from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import homeStyles from './home.module.scss';
|
||||
import { HubSpotNewAdoptersForm } from './_hubSpotNewAdoptersForm';
|
||||
|
||||
const hiddenNewsletterBannerKey = 'hiddenNewsletterBanner';
|
||||
|
||||
const HomePage = () => {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
|
||||
const [hiddenNewsletterBanner, setHideNewsletterBanner] = useState(() => {
|
||||
return JSON.parse(localStorage.getItem(hiddenNewsletterBannerKey)) || false;
|
||||
});
|
||||
|
||||
const hideNewsletterBanner = (shouldHide: boolean) => {
|
||||
localStorage.setItem(hiddenNewsletterBannerKey, JSON.stringify(shouldHide));
|
||||
setHideNewsletterBanner(shouldHide);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className={homeStyles.homePage}>
|
||||
<BannerSection greyBackground>
|
||||
<BannerSectionGrid
|
||||
header={
|
||||
!hiddenNewsletterBanner && (
|
||||
<div
|
||||
className={clsx(
|
||||
'card',
|
||||
'padding--md',
|
||||
homeStyles.newsletterBanner,
|
||||
)}
|
||||
>
|
||||
<div className="text--left bannerContent">
|
||||
🗞️ Want to stay up to date with Backstage? Sign up for our{' '}
|
||||
<Link
|
||||
to="https://info.backstage.spotify.com/newsletter_subscribe"
|
||||
className="text--secondary"
|
||||
>
|
||||
Newsletter
|
||||
</Link>
|
||||
!
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx('button button--link', 'bannerCloseButton')}
|
||||
onClick={() => hideNewsletterBanner(true)}
|
||||
>
|
||||
<svg
|
||||
className="text--secondary"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<ContentBlock
|
||||
className={homeStyles.openPlatformBanner}
|
||||
title={<h1> An open platform for building developer portals</h1>}
|
||||
actionButtons={[
|
||||
{
|
||||
link: 'https://github.com/backstage/backstage#getting-started',
|
||||
label: 'GITHUB',
|
||||
},
|
||||
{
|
||||
link: 'https://info.backstage.spotify.com/office-hours',
|
||||
label: 'OFFICE HOURS',
|
||||
},
|
||||
]}
|
||||
>
|
||||
Powered by a centralized software catalog, Backstage restores
|
||||
order to your infrastructure and enables your product teams to
|
||||
ship high-quality code quickly — without compromising autonomy.
|
||||
</ContentBlock>
|
||||
|
||||
<div className={homeStyles.svgContainer}>
|
||||
<img
|
||||
className="laptopSvg"
|
||||
src={`${siteConfig.baseUrl}img/laptop.svg`}
|
||||
/>
|
||||
<img
|
||||
className="laptopScreenGif"
|
||||
src={`${siteConfig.baseUrl}animations/backstage-logos-hero-8.gif`}
|
||||
/>
|
||||
</div>
|
||||
</BannerSectionGrid>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection>
|
||||
<BannerSectionGrid>
|
||||
<ContentBlock
|
||||
title="The Speed Paradox"
|
||||
topImgSrc={`${siteConfig.baseUrl}animations/backstage-speed-paradox-7.gif`}
|
||||
>
|
||||
At Spotify, we've always believed in the speed and ingenuity that
|
||||
comes from having autonomous development teams. But as we learned
|
||||
firsthand, the faster you grow, the more fragmented and complex
|
||||
your software ecosystem becomes. And then everything slows down
|
||||
again.
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock
|
||||
title="The Standards Paradox"
|
||||
topImgSrc={`${siteConfig.baseUrl}animations/backstage-standards-paradox-4.gif`}
|
||||
>
|
||||
By centralizing services and standardizing your tooling, Backstage
|
||||
streamlines your development environment from end to end. Instead
|
||||
of restricting autonomy, standardization frees your engineers from
|
||||
infrastructure complexity. So you can return to building and
|
||||
scaling, quickly and safely.
|
||||
</ContentBlock>
|
||||
</BannerSectionGrid>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenGradientBackground>
|
||||
<div className={homeStyles.catalogContainer}>
|
||||
<div className="catalogTitle">
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}animations/backstage-software-catalog-icon-1.gif`}
|
||||
alt="Software Catalog Planet GIF"
|
||||
/>
|
||||
|
||||
<h2 className="text--primary">Backstage Software Catalog</h2>
|
||||
|
||||
<h1>Build an ecosystem, not a wilderness</h1>
|
||||
</div>
|
||||
|
||||
<picture className="catalogImg">
|
||||
<source
|
||||
srcSet={`${siteConfig.baseUrl}img/components-with-filter.png`}
|
||||
media="(min-width: 997px)"
|
||||
/>
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}img/components-with-filter-small.png`}
|
||||
alt="Software Catalog Filter Sidebar"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
<ContentBlock
|
||||
title="Manage all your software, all in one place"
|
||||
hasBulletLine
|
||||
>
|
||||
Backstage makes it easy for one team to manage 10 services — and
|
||||
makes it possible for your company to manage thousands of them
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="A uniform overview" hasBulletLine>
|
||||
Every team can see all the services they own and related resources
|
||||
(deployments, data pipelines, pull request status, etc.)
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Metadata on tap" hasBulletLine>
|
||||
All that information can be shared with plugins inside Backstage
|
||||
to enable other management features, like resource monitoring and
|
||||
testing
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Not just services" hasBulletLine>
|
||||
Libraries, websites, ML models — you name it, Backstage knows all
|
||||
about it, including who owns it, dependencies, and more
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock
|
||||
title="Discoverability & accountability"
|
||||
hasBulletLine
|
||||
>
|
||||
No more orphan software hiding in the dark corners of your tech
|
||||
stack
|
||||
</ContentBlock>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenCallToActionGradientBackground>
|
||||
<div className="padding--lg text--center">
|
||||
<h1>Learn more about the software catalog</h1>
|
||||
|
||||
<Link
|
||||
to="https://backstage.io/docs/features/software-catalog/"
|
||||
className="button button--secondary"
|
||||
>
|
||||
READ
|
||||
</Link>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenBottomGradientBackground>
|
||||
<div className={homeStyles.softwareTemplatesContainer}>
|
||||
<div className="softwareTemplatesTitle">
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}animations/backstage-software-templates-icon-5.gif`}
|
||||
alt="Software Templates Rocket GIF"
|
||||
/>
|
||||
|
||||
<h2 className="text--primary">Backstage Software Templates</h2>
|
||||
|
||||
<h1>Standards can set you free</h1>
|
||||
</div>
|
||||
|
||||
<picture className="softwareTemplatesImg">
|
||||
<source
|
||||
srcSet={`${siteConfig.baseUrl}img/cards.png`}
|
||||
media="(min-width: 997px)"
|
||||
/>
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}img/service-cards.png`}
|
||||
alt="Software Templates Cards"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
<ContentBlock
|
||||
title="Like automated getting started guides"
|
||||
hasBulletLine
|
||||
>
|
||||
Using templates, engineers can spin up a new microservice with
|
||||
your organization's best practices built-in, right from the start
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Push-button deployment" hasBulletLine>
|
||||
Click a button to create a Spring Boot project with your repo
|
||||
automatically configured on GitHub and your CI already running the
|
||||
first build
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Built to your standards" hasBulletLine>
|
||||
Go instead of Java? CircleCI instead of Jenkins? Serverless
|
||||
instead of Kubernetes? GCP instead of AWS? Customize your recipes
|
||||
with your best practices baked-in
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Golden Paths pave the way" hasBulletLine>
|
||||
When the right way is also the easiest way, engineers get up and
|
||||
running faster — and more safely
|
||||
</ContentBlock>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenCallToActionGradientBackground>
|
||||
<div className="padding--lg text--center">
|
||||
<h1>Build your own software templates</h1>
|
||||
|
||||
<Link
|
||||
to="https://backstage.io/docs/features/software-templates"
|
||||
className="button button--secondary"
|
||||
>
|
||||
CONTRIBUTE
|
||||
</Link>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenBottomGradientBackground>
|
||||
<div className={homeStyles.softwareTemplatesContainer}>
|
||||
<div className="softwareTemplatesTitle">
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}animations/backstage-techdocs-icon-1.gif`}
|
||||
alt="Backstage TechDocs File Copy GIF"
|
||||
/>
|
||||
|
||||
<h2 className="text--primary">Backstage TechDocs</h2>
|
||||
|
||||
<h1>Docs like code</h1>
|
||||
</div>
|
||||
|
||||
<picture className="softwareTemplatesImg">
|
||||
<source
|
||||
srcSet={`${siteConfig.baseUrl}img/techdocs2.gif`}
|
||||
media="(min-width: 997px)"
|
||||
/>
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}img/techdocs-static-mobile.png`}
|
||||
alt="Backstage TechDocs Markdown to HTML"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
<ContentBlock title="Free documentation" hasBulletLine>
|
||||
Whenever you use a Backstage Software Template, your project
|
||||
automatically gets a TechDocs site, for free
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Easy to write" hasBulletLine>
|
||||
With our docs-like-code approach, engineers write their
|
||||
documentation in Markdown files right alongside their code
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Easy to maintain" hasBulletLine>
|
||||
Updating code? Update your documentation while you're there — with
|
||||
docs and code in the same place, it becomes a natural part of your
|
||||
workstream
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Easy to find and use" hasBulletLine>
|
||||
Since all your documentation is in Backstage, finding any TechDoc
|
||||
is just a search query away
|
||||
</ContentBlock>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenCallToActionGradientBackground>
|
||||
<div className="padding--lg text--center">
|
||||
<h1>Learn more about TechDocs</h1>
|
||||
|
||||
<Link
|
||||
to="https://backstage.io/docs/features/techdocs/"
|
||||
className="button button--secondary"
|
||||
>
|
||||
DOCS
|
||||
</Link>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenGradientBackground>
|
||||
<div className={homeStyles.softwareTemplatesContainer}>
|
||||
<div className="softwareTemplatesTitle">
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}animations/backstage-search-platform-icon-1.gif`}
|
||||
alt="Search Platform Telescope GIF"
|
||||
/>
|
||||
|
||||
<h2 className="text--primary">Backstage Search Platform</h2>
|
||||
|
||||
<h1>A search platform made just for you</h1>
|
||||
</div>
|
||||
|
||||
<picture className="softwareTemplatesImg">
|
||||
<source
|
||||
srcSet={`${siteConfig.baseUrl}img/search-platform-overview.png`}
|
||||
media="(min-width: 997px)"
|
||||
/>
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}img/search-platform-overview-small.png`}
|
||||
alt="Search Platform Search Bar"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
<ContentBlock title="Way more than a text box" hasBulletLine>
|
||||
Backstage Search more than just a box you type questions into —
|
||||
it's an entire platform all by itself, which you can customize to
|
||||
fit your organization's needs
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Search the way you want" hasBulletLine>
|
||||
Bring your own search engine, create a customized search page
|
||||
experience, or edit the look and feel of each search result
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Index everything, find anything" hasBulletLine>
|
||||
With an extensible backend, you can search beyond the Software
|
||||
Catalog and index any source you'd like — whether it's TechDocs or
|
||||
Confluence and Stack Overflow
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Discoverability unlocked" hasBulletLine>
|
||||
New hires and seasoned employees alike can easily search your
|
||||
infrastructure instead of getting lost in it
|
||||
</ContentBlock>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenCallToActionGradientBackground>
|
||||
<div className="padding--lg text--center">
|
||||
<h1>Learn more about Backstage Search</h1>
|
||||
|
||||
<Link
|
||||
to="https://backstage.io/docs/features/search/"
|
||||
className="button button--secondary"
|
||||
>
|
||||
READ
|
||||
</Link>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenBottomGradientBackground>
|
||||
<BannerSectionGrid
|
||||
className={homeStyles.kubernetesSectionContainer}
|
||||
header={
|
||||
<>
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}animations/backstage-kubernetes-icon-1.gif`}
|
||||
alt="Backstage Kubernetes Flag GIF"
|
||||
/>
|
||||
|
||||
<h2 className="text--primary">Backstage Kubernetes</h2>
|
||||
|
||||
<h1>Manage your services, not clusters</h1>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ContentBlock
|
||||
title="Kubernetes made just for service owners"
|
||||
hasBulletLine
|
||||
>
|
||||
Backstage features the first Kubernetes monitoring tool designed
|
||||
around the needs of service owners, not cluster admins
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Your service at a glance" hasBulletLine>
|
||||
Get all your service's deployments in one, aggregated view — no
|
||||
more digging through cluster logs in a CLI, no more combing
|
||||
through lists of services you don't own
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Pick a cloud, any cloud" hasBulletLine>
|
||||
Since Backstage uses the Kubernetes API, it's cloud agnostic — so
|
||||
it works no matter which cloud provider or managed Kubernetes
|
||||
service you use, and even works in multi-cloud orgs
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Any K8s, one UI" hasBulletLine>
|
||||
Now you don't have to switch dashboards when you move from local
|
||||
testing to production, or from one cloud provider to another
|
||||
</ContentBlock>
|
||||
</BannerSectionGrid>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenCallToActionGradientBackground>
|
||||
<div className="padding--lg text--center">
|
||||
<h1>Learn more about the K8s plugin</h1>
|
||||
|
||||
<Link
|
||||
to="https://backstage.io/blog/2021/01/12/new-backstage-feature-kubernetes-for-service-owners"
|
||||
className="button button--secondary"
|
||||
>
|
||||
READ
|
||||
</Link>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenBottomGradientBackground>
|
||||
<div className={homeStyles.softwareTemplatesContainer}>
|
||||
<div className="softwareTemplatesTitle">
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}animations/backstage-plugin-icon-2.gif`}
|
||||
alt="Plugins Building Blocks GIF"
|
||||
/>
|
||||
|
||||
<h2 className="text--primary">
|
||||
Customize Backstage with plugins
|
||||
</h2>
|
||||
|
||||
<h1>An app store for your infrastructure</h1>
|
||||
</div>
|
||||
|
||||
<picture className="softwareTemplatesImg">
|
||||
<source
|
||||
srcSet={`${siteConfig.baseUrl}img/cards-plugins.png`}
|
||||
media="(min-width: 997px)"
|
||||
/>
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}img/plugins.png`}
|
||||
alt="Plugins Cards"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
<ContentBlock title="Add functionality" hasBulletLine>
|
||||
Want scalable website testing? Add the{' '}
|
||||
<Link to="https://backstage.io/blog/2020/04/06/lighthouse-plugin">
|
||||
Lighthouse{' '}
|
||||
</Link>
|
||||
plugin. Wondering about recommended frameworks? Add the{' '}
|
||||
<Link to="https://backstage.io/blog/2020/05/14/tech-radar-plugin">
|
||||
Tech Radar{' '}
|
||||
</Link>
|
||||
plugin.
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="BYO Plugins" hasBulletLine>
|
||||
If you don't see the plugin you need, it's simple to build your
|
||||
own
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock
|
||||
title="Integrate your own custom tooling"
|
||||
hasBulletLine
|
||||
>
|
||||
Building internal plugins lets you tailor your version of
|
||||
Backstage to be a perfect fit for your infrastructure
|
||||
</ContentBlock>
|
||||
|
||||
<ContentBlock title="Share with the community" hasBulletLine>
|
||||
Building <Link to="/plugins">open source plugins</Link>{' '}
|
||||
contributes to the entire Backstage ecosystem, which benefits
|
||||
everyone
|
||||
</ContentBlock>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection greenCallToActionGradientBackground>
|
||||
<div className="padding--lg text--center">
|
||||
<h1>Build a plugin</h1>
|
||||
|
||||
<Link
|
||||
to="/docs/plugins/create-a-plugin"
|
||||
className="button button--secondary"
|
||||
>
|
||||
CONTRIBUTE
|
||||
</Link>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<BannerSection>
|
||||
<div className="padding--lg text--center">
|
||||
<h2>
|
||||
Backstage is a{' '}
|
||||
<Link to="https://www.cncf.io">
|
||||
Cloud Native Computing Foundation
|
||||
</Link>{' '}
|
||||
incubation project
|
||||
</h2>
|
||||
|
||||
<img
|
||||
src={`${siteConfig.baseUrl}img/cncf-white.svg`}
|
||||
alt="CNCF Logo"
|
||||
height="100px"
|
||||
/>
|
||||
</div>
|
||||
</BannerSection>
|
||||
|
||||
<HubSpotNewAdoptersForm />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export const Home = () => {
|
||||
return <BrowserOnly>{() => <HomePage />}</BrowserOnly>;
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import hubSpotStyles from './hubSpotNewAdoptersForm.module.scss';
|
||||
|
||||
export const HubSpotNewAdoptersForm = () => {
|
||||
const [isClosed, setClosed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://js.hsforms.net/forms/v2.js';
|
||||
document.body.appendChild(script);
|
||||
|
||||
script.addEventListener('load', () => {
|
||||
// @ts-ignore
|
||||
if (window.hbspt) {
|
||||
// @ts-ignore
|
||||
window.hbspt.forms.create({
|
||||
portalId: '21894833',
|
||||
formId: '9a5aa2af-87f3-4a44-819f-88ee243bb61e',
|
||||
target: `.${hubSpotStyles.hubSpotNewAdopterFormContent}`,
|
||||
pageId: '79735607665',
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
hubSpotStyles.hubSpotNewAdopterFormContainer,
|
||||
isClosed && 'adoptersFormHidden',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="button button--primary"
|
||||
onClick={() => {
|
||||
setClosed(!isClosed);
|
||||
}}
|
||||
>
|
||||
New Adopters
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
'padding-left--lg padding-vert--lg thin-scrollbar',
|
||||
hubSpotStyles.hubSpotNewAdopterFormContent,
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
$desktopUpBreakpoint: 997px;
|
||||
|
||||
.homePage {
|
||||
font-size: 1.25rem;
|
||||
|
||||
h1 {
|
||||
font-size: 54px;
|
||||
line-height: 56px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
:global(.buttonsContainer) {
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
}
|
||||
}
|
||||
|
||||
.newsletterBanner {
|
||||
--ifm-link-hover-color: var(--ifm-color-info);
|
||||
|
||||
flex-direction: row;
|
||||
color: var(--ifm-button-color);
|
||||
background-color: var(--ifm-color-primary);
|
||||
|
||||
:global(.bannerContent) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:global(.bannerCloseButton) {
|
||||
color: var(--ifm-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.svgContainer {
|
||||
position: relative;
|
||||
|
||||
:global(img.laptopSvg) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global(img.laptopScreenGif) {
|
||||
position: absolute;
|
||||
top: 3%;
|
||||
left: 17%;
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
// Shared container styles
|
||||
.catalogContainer,
|
||||
.softwareTemplatesContainer {
|
||||
gap: 2rem;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
|
||||
picture {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:global([class*='Title']),
|
||||
:global([class*='title']),
|
||||
:global([class*='gridHeader']) {
|
||||
img {
|
||||
max-width: 190px;
|
||||
max-height: 190px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.catalogContainer {
|
||||
@media (min-width: $desktopUpBreakpoint) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
picture {
|
||||
grid-row: span 2;
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
:global(.catalogTitle) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.softwareTemplatesContainer {
|
||||
@media (min-width: $desktopUpBreakpoint) {
|
||||
grid: repeat(2, auto) / repeat(4, 1fr);
|
||||
|
||||
picture {
|
||||
grid-row-start: 3;
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
:global(.softwareTemplatesTitle) {
|
||||
grid-column: span 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kubernetesSectionContainer {
|
||||
:global([class*='gridHeader']) {
|
||||
img {
|
||||
max-width: 190px;
|
||||
max-height: 190px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
.hubSpotNewAdopterFormContainer {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
transform: translateY(-50%);
|
||||
|
||||
transition: all 250ms ease-in-out;
|
||||
|
||||
&:global(.adoptersFormHidden) {
|
||||
transform: translate(500px, -50%);
|
||||
}
|
||||
|
||||
> button {
|
||||
top: 50%;
|
||||
left: -2rem;
|
||||
position: fixed;
|
||||
|
||||
transform: rotate(-90deg) translateX(-50%);
|
||||
transform-origin: top left;
|
||||
}
|
||||
}
|
||||
|
||||
.hubSpotNewAdopterFormContent {
|
||||
height: 600px;
|
||||
max-width: 500px;
|
||||
overflow-y: hidden;
|
||||
border-radius: 8px 0 0 8px;
|
||||
background-color: var(--ifm-color-white);
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
padding-right: 2rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Home } from './home/_home';
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
import nominateStyles from './nominate.module.scss';
|
||||
import Layout from '@theme/Layout';
|
||||
import clsx from 'clsx';
|
||||
import { BannerSection } from '@site/src/components/bannerSection/bannerSection';
|
||||
import { BannerSectionGrid } from '../../components/bannerSection/bannerSectionGrid';
|
||||
import { ContentBlock } from '@site/src/components/contentBlock/contentBlock';
|
||||
|
||||
const Nominate = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<div className={clsx(nominateStyles.nominatePage)}>
|
||||
<BannerSection greyBackground>
|
||||
<ContentBlock title={<h1>Contributor Spotlight nomination</h1>}>
|
||||
<iframe
|
||||
src="https://docs.google.com/forms/d/e/1FAIpQLSdiZ28O7vwHo6NrwirEzGSbuVyBANSv7ItHqRlgVvSz3Z5xqQ/viewform?embedded=true"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
</ContentBlock>
|
||||
</BannerSection>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nominate;
|
||||
@@ -0,0 +1,32 @@
|
||||
.nominatePage {
|
||||
font-size: 1.25rem;
|
||||
|
||||
&,
|
||||
& > section,
|
||||
& > section > div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& > section > div > div {
|
||||
flex: 1;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 54px;
|
||||
line-height: 56px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
import { SimpleCard } from '@site/src/components/simpleCard/simpleCard';
|
||||
import React from 'react';
|
||||
|
||||
export interface IOnDemandData {
|
||||
title: string;
|
||||
category: string;
|
||||
description: string;
|
||||
date: string;
|
||||
youtubeUrl: string;
|
||||
youtubeImgUrl: string;
|
||||
rsvpUrl: string;
|
||||
eventUrl: string;
|
||||
}
|
||||
|
||||
export const OnDemandCard = ({
|
||||
title,
|
||||
category,
|
||||
description,
|
||||
date,
|
||||
youtubeUrl,
|
||||
youtubeImgUrl,
|
||||
rsvpUrl,
|
||||
eventUrl,
|
||||
}: IOnDemandData) => (
|
||||
<SimpleCard
|
||||
header={
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
|
||||
<p className="PluginCardAuthor">on {date}</p>
|
||||
|
||||
<span className="badge badge--primary">{category}</span>
|
||||
|
||||
<img src={youtubeImgUrl} alt={title} />
|
||||
</>
|
||||
}
|
||||
body={<p>{description}</p>}
|
||||
footer={
|
||||
category.toLowerCase() === 'upcoming' ? (
|
||||
<>
|
||||
<Link
|
||||
to={eventUrl}
|
||||
className="button button--outline button--sm button--primary"
|
||||
>
|
||||
Event page
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to={rsvpUrl}
|
||||
className="button button--outline button--sm button--primary"
|
||||
>
|
||||
Remind me
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
to={youtubeUrl}
|
||||
className="button button--outline button--primary button--block"
|
||||
>
|
||||
Watch on YouTube
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
import Layout from '@theme/Layout';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { IOnDemandData, OnDemandCard } from './_onDemandCard';
|
||||
import pluginsStyles from './onDemand.module.scss';
|
||||
import { truncateDescription } from '@site/src/util/truncateDescription';
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
//#region Plugin data import
|
||||
const onDemandContext = require.context(
|
||||
'../../../data/on-demand',
|
||||
false,
|
||||
/\.ya?ml/,
|
||||
);
|
||||
|
||||
const onDemandData = onDemandContext.keys().reduce(
|
||||
(acum, id) => {
|
||||
const pluginData: IOnDemandData = onDemandContext(id).default;
|
||||
|
||||
acum[
|
||||
pluginData.category === 'Upcoming' ? 'upcomingEvents' : 'onDemandEvents'
|
||||
].push(truncateDescription(pluginData));
|
||||
|
||||
return acum;
|
||||
},
|
||||
{
|
||||
upcomingEvents: [] as IOnDemandData[],
|
||||
onDemandEvents: [] as IOnDemandData[],
|
||||
},
|
||||
);
|
||||
//#endregion
|
||||
|
||||
const Plugins = () => (
|
||||
<Layout>
|
||||
<div
|
||||
className={clsx('container', 'padding--lg', pluginsStyles.onDemandPage)}
|
||||
>
|
||||
<div className="marketplaceBanner">
|
||||
<div className="marketplaceContent">
|
||||
<h2>Community sessions</h2>
|
||||
|
||||
<p>
|
||||
Upcoming events and recorded sessions about updates, demos and
|
||||
discussions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/docs/overview/support"
|
||||
className="button button--outline button--primary"
|
||||
>
|
||||
Add an event or recording
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bulletLine margin-bottom--lg"></div>
|
||||
|
||||
<h2>Upcoming live events</h2>
|
||||
|
||||
<div className="cardsContainer margin-bottom--lg">
|
||||
{onDemandData.upcomingEvents.map(eventData => (
|
||||
<OnDemandCard key={eventData.title} {...eventData}></OnDemandCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2>Community on demand</h2>
|
||||
|
||||
<div className="cardsContainer margin-bottom--lg">
|
||||
{onDemandData.onDemandEvents.map(eventData => (
|
||||
<OnDemandCard key={eventData.title} {...eventData}></OnDemandCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default Plugins;
|
||||
@@ -0,0 +1,44 @@
|
||||
.onDemandPage {
|
||||
:global(.marketplaceBanner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.marketplaceContent) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:global(.card) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:global(.card .card__header) {
|
||||
display: grid;
|
||||
row-gap: 0.25rem;
|
||||
column-gap: 1rem;
|
||||
justify-items: start;
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:global(img) {
|
||||
width: 250px;
|
||||
max-width: 100%;
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.card .card__footer) {
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
}
|
||||
|
||||
:global(.cardsContainer) {
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
justify-items: start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
import { SimpleCard } from '@site/src/components/simpleCard/simpleCard';
|
||||
import React from 'react';
|
||||
|
||||
export interface IPluginData {
|
||||
author: string;
|
||||
authorUrl: string;
|
||||
category: string;
|
||||
description: string;
|
||||
documentation: string;
|
||||
iconUrl: string;
|
||||
title: string;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
const defaultIconUrl = 'img/logo-gradient-on-dark.svg';
|
||||
|
||||
export const PluginCard = ({
|
||||
author,
|
||||
authorUrl,
|
||||
category,
|
||||
description,
|
||||
documentation,
|
||||
iconUrl,
|
||||
title,
|
||||
}: IPluginData) => (
|
||||
<SimpleCard
|
||||
header={
|
||||
<>
|
||||
<img src={iconUrl || defaultIconUrl} alt={title} />
|
||||
|
||||
<h3>{title}</h3>
|
||||
|
||||
<p className="PluginCardAuthor">
|
||||
by <a href={authorUrl}>{author}</a>
|
||||
</p>
|
||||
|
||||
<span className="button button--sm button--outline button--primary">
|
||||
{category}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
body={<p>{description}</p>}
|
||||
footer={
|
||||
<Link
|
||||
to={documentation}
|
||||
className="button button--outline button--primary button--block"
|
||||
>
|
||||
Explore
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
import Link from '@docusaurus/Link';
|
||||
import Layout from '@theme/Layout';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { IPluginData, PluginCard } from './_pluginCard';
|
||||
import pluginsStyles from './plugins.module.scss';
|
||||
import { truncateDescription } from '@site/src/util/truncateDescription';
|
||||
|
||||
//#region Plugin data import
|
||||
const pluginsContext = require.context(
|
||||
'../../../data/plugins',
|
||||
false,
|
||||
/\.ya?ml/,
|
||||
);
|
||||
|
||||
const plugins = pluginsContext.keys().reduce(
|
||||
(acum, id) => {
|
||||
const pluginData: IPluginData = pluginsContext(id).default;
|
||||
|
||||
acum[
|
||||
pluginData.category === 'Core Feature' ? 'corePlugins' : 'otherPlugins'
|
||||
].push(truncateDescription(pluginData));
|
||||
|
||||
return acum;
|
||||
},
|
||||
{ corePlugins: [] as IPluginData[], otherPlugins: [] as IPluginData[] },
|
||||
);
|
||||
|
||||
plugins.corePlugins.sort((a, b) => a.order - b.order);
|
||||
plugins.otherPlugins.sort((a, b) => a.order - b.order);
|
||||
//#endregion
|
||||
|
||||
const Plugins = () => (
|
||||
<Layout>
|
||||
<div
|
||||
className={clsx('container', 'padding--lg', pluginsStyles.pluginsPage)}
|
||||
>
|
||||
<div className="marketplaceBanner">
|
||||
<div className="marketplaceContent">
|
||||
<h2>Plugin Marketplace</h2>
|
||||
|
||||
<p>
|
||||
Open source plugins that you can add to your Backstage deployment.
|
||||
Learn how to build a <Link to="/docs/plugins">plugin</Link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/docs/plugins/add-to-marketplace"
|
||||
className="button button--outline button--primary"
|
||||
>
|
||||
Add to Marketplace
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bulletLine margin-bottom--lg"></div>
|
||||
|
||||
<h2>Core Features</h2>
|
||||
|
||||
<div className="pluginsContainer margin-bottom--lg">
|
||||
{plugins.corePlugins.map(pluginData => (
|
||||
<PluginCard key={pluginData.title} {...pluginData}></PluginCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2>All Plugins</h2>
|
||||
|
||||
<div className="pluginsContainer margin-bottom--lg">
|
||||
{plugins.otherPlugins.map(pluginData => (
|
||||
<PluginCard key={pluginData.title} {...pluginData}></PluginCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
export default Plugins;
|
||||
@@ -0,0 +1,33 @@
|
||||
.pluginsPage {
|
||||
:global(.marketplaceBanner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.marketplaceContent) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:global(.card .card__header) {
|
||||
display: grid;
|
||||
row-gap: 0.25rem;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
grid-template-columns: auto 1fr;
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:global(img) {
|
||||
width: 80px;
|
||||
grid-row: 1/4;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.pluginsContainer) {
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const maxDescLength = 160;
|
||||
|
||||
export const truncateDescription = <T extends { description: string }>(
|
||||
itemData: T,
|
||||
) =>
|
||||
itemData.description.length > maxDescLength
|
||||
? {
|
||||
...itemData,
|
||||
description: itemData.description.slice(0, maxDescLength) + '...',
|
||||
}
|
||||
: itemData;
|
||||
Reference in New Issue
Block a user