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

feat(onboarding): Add code tabs to platform spring boot #56318

Merged
Merged
Show file tree
Hide file tree
Changes from 9 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
90 changes: 75 additions & 15 deletions static/app/components/codeSnippet.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {useEffect, useRef, useState} from 'react';
import {Fragment, useEffect, useRef, useState} from 'react';
import styled from '@emotion/styled';
import Prism from 'prismjs';

import {Button} from 'sentry/components/button';
import {IconCopy} from 'sentry/icons';
import {t} from 'sentry/locale';
import {prismStyles} from 'sentry/styles/prism';
import {space} from 'sentry/styles/space';
import {loadPrismLanguage} from 'sentry/utils/loadPrismLanguage';

Expand All @@ -26,6 +27,12 @@ interface CodeSnippetProps {
* Fired when the user selects and copies code snippet manually
*/
onSelectAndCopy?: () => void;
onTabClick?: (tab: string) => void;
selectedTab?: string;
tabs?: {
label: string;
value: string;
}[];
}

export function CodeSnippet({
Expand All @@ -38,6 +45,9 @@ export function CodeSnippet({
className,
onSelectAndCopy,
disableUserSelection,
selectedTab,
onTabClick,
tabs,
}: CodeSnippetProps) {
const ref = useRef<HTMLModElement | null>(null);

Expand Down Expand Up @@ -69,6 +79,9 @@ export function CodeSnippet({
onCopy?.(children);
};

const hasTabs = tabs && tabs.length > 0;
const hasSolidHeader = !!(filename || hasTabs);

const tooltipTitle =
tooltipState === 'copy'
? t('Copy')
Expand All @@ -78,18 +91,37 @@ export function CodeSnippet({

return (
<Wrapper className={`${dark ? 'prism-dark ' : ''}${className ?? ''}`}>
<Header hasFileName={!!filename}>
<Header isSolid={hasSolidHeader}>
{hasTabs && (
<Fragment>
<TabsWrapper>
{tabs.map(({label, value}) => (
<Tab
type="button"
isSelected={selectedTab === value}
onClick={() => onTabClick?.(value)}
key={value}
>
{label}
</Tab>
))}
</TabsWrapper>
<FlexSpacer />
</Fragment>
)}
{filename && <FileName>{filename}</FileName>}
{!hasTabs && <FlexSpacer />}
{!hideCopyButton && (
<CopyButton
type="button"
size="xs"
translucentBorder
borderless={!!filename}
borderless
onClick={handleCopy}
title={tooltipTitle}
tooltipProps={{delay: 0, isHoverable: false, position: 'left'}}
onMouseLeave={() => setTooltipState('copy')}
isAlwaysVisible={hasSolidHeader}
>
<IconCopy size="xs" />
</CopyButton>
Expand All @@ -112,31 +144,30 @@ export function CodeSnippet({

const Wrapper = styled('div')`
position: relative;
background: ${p => p.theme.backgroundSecondary};
background: var(--prism-block-background);
border-radius: ${p => p.theme.borderRadius};

${p => prismStyles(p.theme)}
pre {
margin: 0;
}
`;

const Header = styled('div')<{hasFileName: boolean}>`
const Header = styled('div')<{isSolid: boolean}>`
display: flex;
justify-content: space-between;
align-items: center;

font-family: ${p => p.theme.text.familyMono};
font-size: ${p => p.theme.codeFontSize};
color: ${p => p.theme.headingColor};
color: var(--prism-base);
font-weight: 600;
z-index: 2;

${p =>
p.hasFileName
p.isSolid
? `
padding: ${space(0.5)} 0;
margin: 0 ${space(0.5)} 0 ${space(2)};
border-bottom: solid 1px ${p.theme.innerBorder};
margin: 0 ${space(0.5)};
border-bottom: solid 1px var(--prism-highlight-accent);
`
: `
justify-content: flex-end;
Expand All @@ -146,26 +177,55 @@ const Header = styled('div')<{hasFileName: boolean}>`
width: max-content;
height: max-content;
max-height: 100%;
padding: ${space(1)};
padding: ${space(0.5)};
`}
`;

const FileName = styled('p')`
${p => p.theme.overflowEllipsis}
padding: ${space(0.5)} ${space(0.5)};
margin: 0;
width: auto;
`;

const CopyButton = styled(Button)`
color: ${p => p.theme.subText};
const TabsWrapper = styled('div')`
padding: 0;
display: flex;
`;

const Tab = styled('button')<{isSelected: boolean}>`
box-sizing: border-box;
display: block;
margin: 0;
border: none;
background: none;
padding: ${space(1)} ${space(1)};
color: var(--prism-comment);
${p =>
p.isSelected
? `border-bottom: 3px solid ${p.theme.purple300};
padding-bottom: 5px;
color: var(--prism-base);`
: ''}
`;

const FlexSpacer = styled('div')`
flex-grow: 1;
`;

const CopyButton = styled(Button)<{isAlwaysVisible: boolean}>`
color: var(--prism-comment);
transition: opacity 0.1s ease-out;
opacity: 0;

p + &, /* if preceded by FileName */
div:hover > div > &, /* if Wrapper is hovered */
&.focus-visible {
opacity: 1;
}
&:hover {
color: var(--prism-base);
}
${p => (p.isAlwaysVisible ? 'opacity: 1;' : '')}
`;

const Code = styled('code')<{disableUserSelection?: boolean}>`
Expand Down
103 changes: 86 additions & 17 deletions static/app/components/onboarding/gettingStartedDoc/step.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Fragment} from 'react';
import {Fragment, useState} from 'react';
import styled from '@emotion/styled';
import beautify from 'js-beautify';

Expand All @@ -18,6 +18,65 @@ export const StepTitle = {
[StepType.VERIFY]: t('Verify'),
};

interface CodeSnippetTab {
code: string;
label: string;
language: string;
value: string;
}

interface TabbedCodeSnippetProps {
/**
* An array of tabs to be displayed
*/
tabs: CodeSnippetTab[];
/**
* A callback to be invoked when the configuration is copied to the clipboard
*/
onCopy?: () => void;
/**
* A callback to be invoked when the configuration is selected and copied to the clipboard
*/
onSelectAndCopy?: () => void;
/**
* Whether or not the configuration or parts of it are currently being loaded
*/
partialLoading?: boolean;
}

function TabbedCodeSnippet({
tabs,
onCopy,
onSelectAndCopy,
partialLoading,
}: TabbedCodeSnippetProps) {
const [selectedTabValue, setSelectedTabValue] = useState(tabs[0].value);
const selectedTab = tabs.find(tab => tab.value === selectedTabValue) ?? tabs[0];
const {code, language} = selectedTab;

return (
<CodeSnippet
dark
language={language}
onCopy={onCopy}
onSelectAndCopy={onSelectAndCopy}
hideCopyButton={partialLoading}
disableUserSelection={partialLoading}
tabs={tabs}
selectedTab={selectedTabValue}
onTabClick={value => setSelectedTabValue(value)}
>
{language === 'javascript'
? beautify.js(code, {
indent_size: 2,
e4x: true,
brace_style: 'preserve-inline',
})
: code.trim()}
</CodeSnippet>
);
}

type ConfigurationType = {
/**
* Additional information to be displayed below the code snippet
Expand All @@ -26,7 +85,7 @@ type ConfigurationType = {
/**
* The code snippet to display
*/
code?: string;
code?: string | CodeSnippetTab[];
/**
* Nested configurations provide a convenient way to accommodate diverse layout styles, like the Spring Boot configuration.
*/
Expand Down Expand Up @@ -88,23 +147,33 @@ function getConfiguration({
return (
<Configuration>
{description && <Description>{description}</Description>}
{language && code && (
<CodeSnippet
dark
language={language}
{Array.isArray(code) ? (
<TabbedCodeSnippet
tabs={code}
onCopy={onCopy}
onSelectAndCopy={onSelectAndCopy}
hideCopyButton={partialLoading}
disableUserSelection={partialLoading}
>
{language === 'javascript'
? beautify.js(code, {
indent_size: 2,
e4x: true,
brace_style: 'preserve-inline',
})
: code.trim()}
</CodeSnippet>
partialLoading={partialLoading}
/>
) : (
language &&
code && (
<CodeSnippet
dark
language={language}
onCopy={onCopy}
onSelectAndCopy={onSelectAndCopy}
hideCopyButton={partialLoading}
disableUserSelection={partialLoading}
>
{language === 'javascript'
? beautify.js(code, {
indent_size: 2,
e4x: true,
brace_style: 'preserve-inline',
})
: code.trim()}
</CodeSnippet>
)
)}
{additionalInfo && <AdditionalInfo>{additionalInfo}</AdditionalInfo>}
</Configuration>
Expand Down
Loading
Loading