[Website] Public Launch Assembly (#438)
* Reduce hero size for placeholder * Clean up Sticky Footer - Increase the specificity of the selector to prevent any accidental collisions - Account for the meganav that exists * [Website] Add Boundary Features section to homepage (#436) * Install and implement ProductFeaturesList * Add images and content * Inline the product features * [Website] Add use cases section (#445) * Commit to illustrate UseCases error * update react image * Add in content from design * Tweaks * Apply handy edits Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> Co-authored-by: Jeff Escalante <jescalan@users.noreply.github.com> Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> * [Website] Add SectionBreakCta component (#452) * Create implement barebones component * Build structure/add content * Get CSS up to spec - Typography - spacing - Layout - box-shadow - Accent line * Mobile treatment * Update URL * Add a break-section to give bg on page * Change link to outbound Design is incorrect for this link Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> * Reduce CSS selectors Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> * [Website] Add branded CTA section (#450) * Create skeleton for component * Bring in line with CallToAction styles * Set up background * Typography and layout (and mobile) * Button polish * Changes to prep for updates These will keep the homepage ready for component updates and the upcoming docs and download pages. Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> * Address feedback - Reduce button hackiness - Adjust pattern position on mobile to not overlap button Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> * Upgrade button, minor tweaks to BrandedCTA (#481) * Updates dependencies * [Website] Homepage hero updates (#503) * Adjustments to align and lay out left side * Implement hero background * Adjust hero URLs * Implement mobile hero pattern/tweaks * Add video! * Mobile tweaks * Remove mobile bg * Center the text/buttons on mobile * Remove mobile hero pattern * Address visual nits * Adjust interface for video * [Website] Add "How it works" section (#483) * Create skeleton and implement component * Add sections * Add CSS and adjust structure * Remove sections ternary * Change nomenclature for clarity * Mobile tweaks * Tweaks inspired by HCP * Create LogoList component skeleton * Implement LogoList on main component - Shift footertext responsibility to it - Assume logos will be passed in, because it doesn't render otherwise anyway with the right checks * Implement some logos/restructure LogoList * CSS adjustments to logolist/section layout * Show example for too many integrations * Remove cutoff logic * Add SVGs for diagram stack * Combine SVGs into a component This is necessary to act on various parts of the image independently. * Fully combine SVG and remove unneeded CSS * Further CSS cleanup * Implement stickyness * Set up active example checks for graphic * Implement reactivity - Apply styles based on inactivity or activity, whichever fits best per-element - Add expected state to the general HowItWorks component, where it will be managed and passed into the diagram - Make icon and arrow spacers use the same spacer class * CSS tweaks for design/organization * Implement scroll-to-animate functionality! * Fill out icons for integrations * Feature updates - Move LogoList into folder - Give descriptions some margin * Mobile treatment tweaks * Import logic from the cloud-platform page on .com This fixes a bug in which the in-view example would not update in an expected manner. * Remove max-width adjustment on UL * Fix need for height on the graphic * Padding/margin tweaks * Minor CSS tweaks * Final padding adjustments * Plugging in proper content into the landing page (#545) * Misc content updates to align with copy doc * Plug in feature SVGs * Move use cases up * Remove the MegaNav - to be replaced soon * Updates the nav * Add footer content, security page * Remove react-mega-nav from styles * Downloads Page (#541) * [Website] Add use cases section (#445) * Commit to illustrate UseCases error * update react image * Add in content from design * Tweaks * Apply handy edits Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> Co-authored-by: Jeff Escalante <jescalan@users.noreply.github.com> Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> * Port new downloader page to Boundary * update placeholder data * missing css property * Fix the weird rebase * Fix padding Co-authored-by: Noel Quiles <3746694+EnMod@users.noreply.github.com> Co-authored-by: Jeff Escalante <jescalan@users.noreply.github.com> Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> * [Website] Add alert banner (#557) * Bring over waypoint's implementation * Prove that it works/theme to boundary * revert to hidden for now * [Website] README polish (#558) * Adjust readme for boundary * Correct URL in first paragraph * [Website] Add community page (#555) * Just import files from Consul * Revert CSS paradigm for VerticalBlockList * Adds community to the nav Co-authored-by: Brandon Romano <BrandonRRomano@gmail.com> * A few small content / bug tweaks (#563) * adds HashiStackMenu * Fix platform agnostic copy * Reduce padding between sections * Fix Subnav indicator for docs page * Fix features padding * Adds Favicon * Update how it works content * Adds share image * Updates use cases * Styling to community page * Remove some color overrides * Add SVGO option to nextconfig (#565) * Add press kit to footer (#567) * Plug in proper link for onboard users * Plug in proper video URL (#584) * Bump HSM version (#582) * Downloads Page Content (#585) * Wire up Boundary Downloads content * Plug in remaining downloads content * Download via Product slug * Tweak with transition timing (#587) * Plug in proper logo into the downloads page (#589) * Content tweaks (#590) * Remove border top from footer (#591) * More content tweaks (#608) * Updates the hero to support UI/CLI previews (#609) * Updates the hero to support UI/CLI previews * Updates video * More content tweaks (#634) * Plugs in light theme UI video * Update infra as code feature * Remove Auth (#636) * Remove auth-related files * Remove auth-related config * Remove auth provider and next-auth pkgs * Remove *env.local from gitignore * Swtich og:image to have a www subdomain Co-authored-by: Noel Quiles <3746694+EnMod@users.noreply.github.com> * Elevate prop destructuring to function call Co-authored-by: Jeff Escalante <jescalan@users.noreply.github.com> * Update website/pages/community/index.jsx Co-authored-by: Jeff Escalante <jescalan@users.noreply.github.com> Co-authored-by: Noel Quiles <3746694+EnMod@users.noreply.github.com> Co-authored-by: Jeff Escalante <jescalan@users.noreply.github.com> Co-authored-by: Zack Tanner <zacktanner@gmail.com> Co-authored-by: Jimmy Merritello <7191639+jmfury@users.noreply.github.com>pull/643/head
@ -1,28 +0,0 @@
|
||||
.signInWrapper {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loadingIconSpin {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
animation: spin 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loginLockup {
|
||||
display: flex;
|
||||
width: 50vw;
|
||||
justify-content: space-around;
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
import { useSession, getCsrfToken, getProviders } from 'next-auth/client'
|
||||
import LoadingIcon from './loading.svg?include'
|
||||
import InlineSvg from '@hashicorp/react-inline-svg'
|
||||
import styles from './auth-gate.module.css'
|
||||
import Button from '@hashicorp/react-button'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import SigninErrorPage from 'components/signin-error-page'
|
||||
|
||||
export default function AuthGate({ children }) {
|
||||
const [session, loading] = useSession()
|
||||
const { query } = useRouter()
|
||||
if (query?.error === 'oAuthCallback') return <SigninErrorPage />
|
||||
|
||||
if (loading)
|
||||
return <InlineSvg className={styles.loadingIconSpin} src={LoadingIcon} />
|
||||
return session ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<div className={styles.signInWrapper}>
|
||||
<SignInForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SignInForm() {
|
||||
const [token, setToken] = useState(null)
|
||||
useEffect(() => {
|
||||
async function getToken() {
|
||||
const t = await getCsrfToken()
|
||||
if (t) {
|
||||
setToken(t)
|
||||
}
|
||||
}
|
||||
getToken()
|
||||
}, [token])
|
||||
return token ? (
|
||||
<section className={styles.signInWrapper}>
|
||||
<Form token={token} callbackUrl={window.location.origin} />
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
|
||||
function Form({ callbackUrl, token }) {
|
||||
const authProviders = useAuthProviders()
|
||||
|
||||
return (
|
||||
<div className={styles.loginLockup}>
|
||||
{authProviders &&
|
||||
Object.keys(authProviders).map((ap) => (
|
||||
<form
|
||||
key={ap}
|
||||
action={`${callbackUrl}/api/auth/signin/${ap}`}
|
||||
method="POST"
|
||||
>
|
||||
<input type="hidden" name="csrfToken" value={token} />
|
||||
<input type="hidden" name="callbackUrl" value={callbackUrl} />
|
||||
<Button
|
||||
type="submit"
|
||||
title={`Sign in with ${authProviders[ap].name}`}
|
||||
theme={{ variant: 'primary', background: 'dark' }}
|
||||
/>
|
||||
</form>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAuthProviders() {
|
||||
const [authProviders, setAuthProviders] = useState([])
|
||||
async function getProviderList() {
|
||||
const providers = await getProviders()
|
||||
if (providers) {
|
||||
setAuthProviders(providers)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
getProviderList()
|
||||
}
|
||||
}, [])
|
||||
return authProviders
|
||||
}
|
||||
|
Before Width: | Height: | Size: 253 B |
@ -1,24 +0,0 @@
|
||||
.authIndicator {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
z-index: 3;
|
||||
padding: 1rem 2rem;
|
||||
text-align: center;
|
||||
background: var(--black);
|
||||
color: var(--white);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@media (--medium-up) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.loggedInText {
|
||||
margin: 1rem 1rem;
|
||||
@media (--medium-up) {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import { signOut, useSession } from 'next-auth/client'
|
||||
import styles from './auth-indicator.module.css'
|
||||
import Button from '@hashicorp/react-button'
|
||||
|
||||
export default function AuthIndicator() {
|
||||
const [session, loading] = useSession()
|
||||
if (loading) return `Loading...`
|
||||
return (
|
||||
<div className={styles.authIndicator}>
|
||||
{session && (
|
||||
<>
|
||||
<span className={`g-type-label ${styles.loggedInText}`}>
|
||||
Signed in as {session.user.email}
|
||||
</span>
|
||||
<span>
|
||||
<Button
|
||||
onClick={signOut}
|
||||
title={`Sign out`}
|
||||
size="small"
|
||||
theme={{ variant: 'secondary-neutral', background: 'dark' }}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 253 B |
@ -0,0 +1,66 @@
|
||||
.brandedCta {
|
||||
padding: 88px 0;
|
||||
background-color: var(--red-l3);
|
||||
background-image: url('./img/bg-pattern.svg');
|
||||
background-position: bottom right;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
@media (--small) {
|
||||
background-position: bottom 0 right -130px;
|
||||
}
|
||||
|
||||
@media (462px <= width < 600px) {
|
||||
background-position: bottom 0 right -260px;
|
||||
}
|
||||
|
||||
@media (width < 462px) {
|
||||
background-position: bottom 0 right -170px;
|
||||
}
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
@media (width >= 992px) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (width < 992px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
color: var(--black);
|
||||
margin-top: 0;
|
||||
|
||||
@media (width >= 992px) {
|
||||
flex-basis: 33.3%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 647px;
|
||||
margin: 0;
|
||||
color: var(--gray-3);
|
||||
}
|
||||
|
||||
.content-and-links {
|
||||
@media (width >= 992px) {
|
||||
flex-basis: 66.6%;
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
& .g-type-body-large {
|
||||
max-width: 35em;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-top: 40px;
|
||||
margin-bottom: -16px;
|
||||
|
||||
& a {
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 26 KiB |
@ -0,0 +1,46 @@
|
||||
import styles from './branded-cta.module.css'
|
||||
import Button from '@hashicorp/react-button'
|
||||
|
||||
export default function BrandedCta({ heading, content, links }) {
|
||||
|
||||
return (
|
||||
<div className={styles.brandedCta}>
|
||||
<div className={`g-grid-container ${styles.contentContainer}`}>
|
||||
<h2
|
||||
data-testid="heading"
|
||||
className={`g-type-display-2 ${styles.heading}`}
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
<div className="content-and-links">
|
||||
<p
|
||||
data-testid="content"
|
||||
className={`g-type-body-large ${styles.content}`}
|
||||
>
|
||||
{content}
|
||||
</p>
|
||||
<div data-testid="links" className={styles.links}>
|
||||
{links.map((link, stableIdx) => {
|
||||
const buttonVariant = stableIdx === 0 ? 'primary' : 'secondary'
|
||||
const linkType = link.type || ''
|
||||
return (
|
||||
<Button
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={stableIdx}
|
||||
linkType={linkType}
|
||||
theme={{
|
||||
variant: buttonVariant,
|
||||
brand: 'red',
|
||||
background: 'light',
|
||||
}}
|
||||
title={link.text}
|
||||
url={link.url}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
import AuthIndicator from 'components/auth-indicator'
|
||||
import AuthGate from 'components/auth-gate'
|
||||
import { Provider as NextAuthProvider } from 'next-auth/client'
|
||||
|
||||
const shouldApplyAuth =
|
||||
process.env.HASHI_ENV === 'production' || process.env.HASHI_ENV === 'preview'
|
||||
|
||||
export default function ConditionalAuthProvider({ children, session }) {
|
||||
return shouldApplyAuth ? (
|
||||
<NextAuthProvider session={session}>
|
||||
<AuthGate>
|
||||
{children}
|
||||
<AuthIndicator />
|
||||
</AuthGate>
|
||||
</NextAuthProvider>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
import Tabs from '@hashicorp/react-tabs'
|
||||
|
||||
import { prettyOs, prettyArch } from 'components/downloader/utils/downloader'
|
||||
import styles from './style.module.css'
|
||||
|
||||
export default function DownloadTabs({
|
||||
defaultTabIdx,
|
||||
tabData,
|
||||
downloads,
|
||||
version,
|
||||
merchandisingSlot,
|
||||
brand,
|
||||
logo,
|
||||
tutorialLink,
|
||||
}) {
|
||||
return (
|
||||
<Tabs
|
||||
key={defaultTabIdx}
|
||||
centered
|
||||
fullWidthBorder
|
||||
theme={brand}
|
||||
className={styles.tabs}
|
||||
defaultTabIdx={defaultTabIdx}
|
||||
items={tabData.map(({ os, packageManagers }) => ({
|
||||
heading: prettyOs(os),
|
||||
tabChildren: function TabChildren() {
|
||||
return (
|
||||
<div className={styles.cards}>
|
||||
<Cards
|
||||
key={os}
|
||||
os={os}
|
||||
downloads={downloads}
|
||||
packageManagers={packageManagers}
|
||||
version={version}
|
||||
theme={brand}
|
||||
logo={logo}
|
||||
tutorialLink={tutorialLink}
|
||||
/>
|
||||
{merchandisingSlot}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Cards({
|
||||
os,
|
||||
downloads,
|
||||
packageManagers,
|
||||
version,
|
||||
theme,
|
||||
logo,
|
||||
tutorialLink,
|
||||
}) {
|
||||
const arches = downloads[os]
|
||||
const hasPackageManager = Boolean(packageManagers)
|
||||
const hasMultiplePackageManagers = Array.isArray(packageManagers)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
hasMultiplePackageManagers
|
||||
? styles.downloadCardsSingle
|
||||
: styles.downloadCards
|
||||
}
|
||||
>
|
||||
{hasPackageManager && (
|
||||
<div className={styles.packageManagers}>
|
||||
<span className={styles.cardTitle}>Package Manager</span>
|
||||
{Array.isArray(packageManagers) ? (
|
||||
<Tabs
|
||||
theme={theme}
|
||||
items={packageManagers.map(({ label, commands }) => ({
|
||||
heading: label,
|
||||
tabChildren: function TabChildren() {
|
||||
return (
|
||||
<div className={styles.install}>
|
||||
{commands.map((command) => (
|
||||
<pre key={command}>{command}</pre>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.install}>
|
||||
{packageManagers.commands.map((command) => (
|
||||
<pre key={command}>{command}</pre>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{tutorialLink && (
|
||||
<div>
|
||||
<a href={tutorialLink.href}>{tutorialLink.label}</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={hasPackageManager ? styles.card : styles.soloCard}>
|
||||
<span className={styles.cardTitle}>Binary Download</span>
|
||||
<div className={styles.logoDownloadWrapper}>
|
||||
<div className={styles.logoWrapper}>
|
||||
{logo}
|
||||
<span className={styles.version}>{version}</span>
|
||||
</div>
|
||||
{Object.entries(arches).map(([arch, url]) => (
|
||||
<a href={url} key={arch} className={styles.downloadLink}>
|
||||
{prettyArch(arch)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.fastly}>
|
||||
Bandwidth courtesy of
|
||||
<img src={require('../logos/fastly.svg')} alt="Fastly" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
.cards {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logoWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.logoDownloadWrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.version {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.downloadCards {
|
||||
justify-content: center;
|
||||
margin: 54px 0 24px;
|
||||
|
||||
& a {
|
||||
color: var(--gray-2);
|
||||
}
|
||||
|
||||
@media (--medium-up) {
|
||||
margin: 64px 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-column-gap: 48px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
& .downloadLink {
|
||||
color: var(--brand);
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.downloadCardsSingle {
|
||||
composes: downloadCards;
|
||||
|
||||
& .card {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
@media (--medium-up) {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
& .card:first-child {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
min-height: 278px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
justify-content: space-between;
|
||||
padding: 40px 32px;
|
||||
color: var(--gray-2);
|
||||
border: 1px solid var(--gray-6);
|
||||
|
||||
&:first-child {
|
||||
border-bottom: none;
|
||||
|
||||
@media (--medium-up) {
|
||||
border-bottom: 1px solid var(--gray-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.packageManagers {
|
||||
composes: card;
|
||||
|
||||
& a {
|
||||
color: var(--brand);
|
||||
}
|
||||
|
||||
& :global {
|
||||
& .g-tabs {
|
||||
width: 100%;
|
||||
|
||||
@media (--medium-up) {
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.soloCard {
|
||||
composes: card;
|
||||
grid-column: span 2;
|
||||
|
||||
&:first-child {
|
||||
border-bottom: 1px solid var(--gray-6);
|
||||
}
|
||||
}
|
||||
|
||||
.install {
|
||||
color: var(--white);
|
||||
background-color: var(--black);
|
||||
padding: 16px 24px;
|
||||
border-radius: 3px;
|
||||
text-align: left;
|
||||
margin: 32px 0;
|
||||
|
||||
& pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: inherit;
|
||||
white-space: normal;
|
||||
|
||||
&::before {
|
||||
color: #e93471;
|
||||
content: '$';
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media (--medium-up) {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-family: var(--font-display);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gray-4);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fastly {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
& svg path {
|
||||
fill: var(--black);
|
||||
}
|
||||
|
||||
& img {
|
||||
margin-top: 12px;
|
||||
width: 48px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import Button from '@hashicorp/react-button'
|
||||
import styles from './style.module.css'
|
||||
|
||||
export default function Dropdown({ options, onChange, title, brand }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const buttonRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (buttonRef.current.contains(event.target)) return
|
||||
|
||||
setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.root} ref={buttonRef}>
|
||||
<Button
|
||||
className="trigger"
|
||||
theme={{ brand }}
|
||||
onClick={() => setOpen((open) => !open)}
|
||||
aria-expanded={open}
|
||||
aria-controls="menu-list"
|
||||
aria-haspopup="true"
|
||||
title={title}
|
||||
icon={{
|
||||
position: 'right',
|
||||
svg:
|
||||
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5 7.5L10 12.5L15 7.5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||
}}
|
||||
/>
|
||||
{open && (
|
||||
<ul className={styles.menu} id="menu-list" role="menu">
|
||||
{options.map((option) => (
|
||||
<li
|
||||
className={styles.option}
|
||||
tabIndex={0}
|
||||
role="menuitem"
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
onChange(option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
.root {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
top: 35px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
background-color: var(--white);
|
||||
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
background-color: var(--brand);
|
||||
color: var(--white);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--brand-d1);
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,158 @@
|
||||
import { useState, Fragment, useEffect } from 'react'
|
||||
|
||||
import Dropdown from 'components/downloader/dropdown'
|
||||
import {
|
||||
prettyArch,
|
||||
prettyOs,
|
||||
sortPlatforms,
|
||||
getVersionLabel,
|
||||
sortAndFilterReleases,
|
||||
} from 'components/downloader/utils/downloader'
|
||||
import styles from './style.module.css'
|
||||
|
||||
export default function ReleaseInformation({
|
||||
productId,
|
||||
productName,
|
||||
latestVersion,
|
||||
packageManagers,
|
||||
containers,
|
||||
tutorials,
|
||||
changelog,
|
||||
brand,
|
||||
}) {
|
||||
const [selectedVersionId, setSelectedVersionId] = useState(latestVersion)
|
||||
const [releases, setReleases] = useState([])
|
||||
const { version, ...selectedVersion } =
|
||||
releases.find((release) => release.version === selectedVersionId) || {}
|
||||
|
||||
useEffect(() => {
|
||||
// lazy load all release versions to populate dropdown & releases section
|
||||
fetch(`https://releases.hashicorp.com/${productId}/index.json`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const latestReleases = sortAndFilterReleases(Object.keys(data.versions))
|
||||
|
||||
const releases = latestReleases.map((releaseVersion) => ({
|
||||
...sortPlatforms(data.versions[releaseVersion]),
|
||||
version: releaseVersion,
|
||||
}))
|
||||
setReleases(releases)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className="g-container">
|
||||
<h2>Release Information</h2>
|
||||
<div className={styles.grid}>
|
||||
{releases.length > 0 && (
|
||||
<>
|
||||
<div className={styles.releases}>Releases:</div>
|
||||
<div>
|
||||
<Dropdown
|
||||
title={`${productName} ${getVersionLabel(
|
||||
version,
|
||||
latestVersion
|
||||
)}`}
|
||||
brand={brand}
|
||||
options={releases.map((releaseData) => ({
|
||||
label: `${productName} ${getVersionLabel(
|
||||
releaseData.version,
|
||||
latestVersion
|
||||
)}`,
|
||||
value: releaseData.version,
|
||||
}))}
|
||||
onChange={(release) => setSelectedVersionId(release)}
|
||||
/>
|
||||
<a
|
||||
href={
|
||||
changelog ||
|
||||
`https://github.com/hashicorp/${productId}/blob/v${version}/CHANGELOG.md`
|
||||
}
|
||||
className={styles.changelog}
|
||||
>
|
||||
Changelog
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.latestDownloads}>Latest Downloads:</div>
|
||||
<div>
|
||||
Package downloads for {productName} {version}
|
||||
<div className={styles.downloads}>
|
||||
{Object.entries(selectedVersion).map(([os, release]) => (
|
||||
<Fragment key={os}>
|
||||
<div className={styles.os}>{prettyOs(os)}</div>
|
||||
<div>
|
||||
{Object.entries(release).map(([arch, file]) => (
|
||||
<a href={file} key={arch}>
|
||||
{prettyArch(arch)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<p>
|
||||
You can find the{' '}
|
||||
<a
|
||||
href={`https://releases.hashicorp.com/${productId}/${version}/${productId}_${version}_SHA256SUMS`}
|
||||
>
|
||||
SHA256 checksums for {productName} {version}
|
||||
</a>{' '}
|
||||
online and you can{' '}
|
||||
<a
|
||||
href={`https://releases.hashicorp.com/${productId}/${version}/${productId}_${version}_SHA256SUMS.sig`}
|
||||
>
|
||||
verify the checksums signature file
|
||||
</a>{' '}
|
||||
which has been signed using{' '}
|
||||
<a href="https://hashicorp.com/security">
|
||||
HashiCorp's GPG key.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{packageManagers?.length > 0 && (
|
||||
<>
|
||||
<div className={styles.heading}>Package Managers</div>
|
||||
<div className={styles.links}>
|
||||
{packageManagers.map((packageManager) => (
|
||||
<div key={packageManager.label}>
|
||||
Install with{' '}
|
||||
<a href={packageManager.url}>{packageManager.label}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{containers?.length > 0 && (
|
||||
<>
|
||||
<div className={styles.heading}>Containers</div>
|
||||
<div className={styles.links}>
|
||||
{containers.map((container) => (
|
||||
<div key={container.label}>
|
||||
Run with <a href={container.url}>{container.label}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{containers?.length > 0 && (
|
||||
<>
|
||||
<div className={styles.heading}>Tutorials</div>
|
||||
<div className={styles.links}>
|
||||
{tutorials.map((tutorial) => (
|
||||
<div key={tutorial.label}>
|
||||
<a href={tutorial.url}>{tutorial.label}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
.root {
|
||||
background-color: var(--gray-7);
|
||||
padding: 42px 0;
|
||||
margin-top: 42px;
|
||||
color: var(--gray-2);
|
||||
|
||||
& p {
|
||||
color: var(--gray-2);
|
||||
}
|
||||
|
||||
& a {
|
||||
color: var(--gray-2);
|
||||
}
|
||||
|
||||
& :global(.g-container) {
|
||||
max-width: 825px;
|
||||
}
|
||||
|
||||
& h2 {
|
||||
text-align: center;
|
||||
color: var(--black);
|
||||
margin-bottom: 54px;
|
||||
|
||||
@media (--medium-up) {
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 24px 0;
|
||||
font-weight: bold;
|
||||
color: var(--black);
|
||||
|
||||
@media (--medium-up) {
|
||||
text-align: right;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.changelog {
|
||||
display: block;
|
||||
margin: 24px 0 0;
|
||||
color: var(--black);
|
||||
|
||||
@media (--medium-up) {
|
||||
display: inline-block;
|
||||
margin: 0 0 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.grid {
|
||||
margin: 48px auto;
|
||||
|
||||
@media (--medium-up) {
|
||||
display: grid;
|
||||
grid-column-gap: 32px;
|
||||
grid-row-gap: 48px;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.latestDownloads {
|
||||
composes: heading;
|
||||
margin-top: 40px;
|
||||
|
||||
@media (--medium-up) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.downloads {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: auto 1fr;
|
||||
margin: 17px 0 40px;
|
||||
grid-column-gap: 28px;
|
||||
grid-row-gap: 16px;
|
||||
|
||||
& a {
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
& .os {
|
||||
font-weight: bold;
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
@media (--medium-up) {
|
||||
margin: 20px 0 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.releases {
|
||||
align-self: center;
|
||||
composes: heading;
|
||||
}
|
||||
|
||||
.links > div:not(:first-child) {
|
||||
margin: 15px 0;
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
import semverRSort from 'semver/functions/rsort'
|
||||
import semverPrerelease from 'semver/functions/prerelease'
|
||||
import semverValid from 'semver/functions/valid'
|
||||
|
||||
export function getVersionLabel(version, latestVersion) {
|
||||
if (version === latestVersion) {
|
||||
return `${version} (latest)`
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
export function sortAndFilterReleases(releases) {
|
||||
const validReleases = releases.filter(semverValid)
|
||||
// descending sort on releases, while filtering out pre-releases
|
||||
return semverRSort(validReleases).filter(
|
||||
(version) => !semverPrerelease(version)
|
||||
)
|
||||
}
|
||||
|
||||
/** TODO: Below utils directly from Product-Downloader component.
|
||||
* Should either be exported, or migrated back to web-components */
|
||||
|
||||
export function prettyArch(arch) {
|
||||
switch (arch) {
|
||||
case 'all':
|
||||
return 'Universal (32 and 64-bit)'
|
||||
case 'i686':
|
||||
case 'i386':
|
||||
case '686':
|
||||
case '386':
|
||||
return '32-bit'
|
||||
case 'x86_64':
|
||||
case '86_64':
|
||||
case 'amd64':
|
||||
return '64-bit'
|
||||
default:
|
||||
if (/-/.test(arch)) {
|
||||
const parts = arch.split(/-(.+)/)
|
||||
return `${prettyArch(parts[0])} (${parts[1]})`
|
||||
} else {
|
||||
const parts = arch.split('_')
|
||||
if (parts.length > 0) {
|
||||
return (
|
||||
parts[parts.length - 1].charAt(0).toUpperCase() +
|
||||
parts[parts.length - 1].slice(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function detectOs(platform) {
|
||||
for (let key in platformMap) {
|
||||
if (platform.indexOf(key) !== -1) {
|
||||
return platformMap[key]
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function prettyOs(os) {
|
||||
switch (os) {
|
||||
case 'darwin':
|
||||
return 'Mac OS X'
|
||||
case 'freebsd':
|
||||
return 'FreeBSD'
|
||||
case 'openbsd':
|
||||
return 'OpenBSD'
|
||||
case 'netbsd':
|
||||
return 'NetBSD'
|
||||
case 'archlinux':
|
||||
return 'Arch Linux'
|
||||
case 'linux':
|
||||
return 'Linux'
|
||||
case 'windows':
|
||||
return 'Windows'
|
||||
default:
|
||||
return os.charAt(0).toUpperCase() + os.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
const platformMap = {
|
||||
Mac: 'darwin',
|
||||
Win: 'windows',
|
||||
Linux: 'linux',
|
||||
}
|
||||
|
||||
export function sortPlatforms(releaseData) {
|
||||
// first we pull the platforms out of the release data object and format it the way we want
|
||||
const platforms = releaseData.builds.reduce((acc, build) => {
|
||||
if (!acc[build.os]) acc[build.os] = {}
|
||||
acc[build.os][build.arch] = build.url
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const platformKeys = Object.keys(platforms)
|
||||
|
||||
// create array of sorted values to base the order on
|
||||
const sortedValues = Object.keys(platformMap)
|
||||
.map((e) => platformMap[e])
|
||||
// join the lists together to make sure
|
||||
// all items are accounted for when sorting
|
||||
.concat(platformKeys)
|
||||
// filter our any duplicates and unneeded items
|
||||
.filter((elem, pos, arr) => {
|
||||
return arr.indexOf(elem) == pos && platformKeys.indexOf(elem) > -1
|
||||
})
|
||||
|
||||
return (
|
||||
platformKeys
|
||||
// sort items based on platformMap order
|
||||
.sort((a, b) => {
|
||||
return sortedValues.indexOf(a) - sortedValues.indexOf(b)
|
||||
})
|
||||
// create new sorted object to return
|
||||
.reduce((result, key) => {
|
||||
result[key] = platforms[key]
|
||||
return result
|
||||
}, {})
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
.homepageHero {
|
||||
padding: 128px 0;
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--red-l3);
|
||||
background-image: url(/img/hero-pattern.svg);
|
||||
background-position: bottom right;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
padding: 88px 0;
|
||||
background-position: bottom 0 right -130px;
|
||||
}
|
||||
|
||||
& :global(.g-hero) {
|
||||
background: none;
|
||||
padding: 0;
|
||||
|
||||
& :global(.g-btn) {
|
||||
border-color: var(--red);
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
& :global(.carousel) {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
& :global(.controls) {
|
||||
padding-top: 0;
|
||||
margin-bottom: 32px;
|
||||
|
||||
& :global(.progress-bar) {
|
||||
background-color: var(--gray-6);
|
||||
|
||||
& > span {
|
||||
background: var(--red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,35 +1,51 @@
|
||||
import s from './style.module.css'
|
||||
import Button from '@hashicorp/react-button'
|
||||
import Hero from '@hashicorp/react-hero'
|
||||
import styles from './HomepageHero.module.css'
|
||||
|
||||
export default function HomepageHero({ title, description, links }) {
|
||||
/**
|
||||
* A simple Facade around our react-hero to make the interface a little more straightforward
|
||||
* for the end-user updating this on our Homepage, while also allowing us to shim in some
|
||||
* additional styles and encapsulate that logic.
|
||||
*/
|
||||
export default function HomepageHero({
|
||||
title,
|
||||
description,
|
||||
links,
|
||||
uiVideo,
|
||||
cliVideo,
|
||||
}) {
|
||||
return (
|
||||
<div className={s.root}>
|
||||
<div className="g-grid-container">
|
||||
<span className={s.eyebrow}>
|
||||
Welcome to the <span className={s.red}>INTERNAL BETA</span> for
|
||||
HashiCorp Boundary! This is a confidential internal only beta. No
|
||||
details should be shared externally.
|
||||
</span>
|
||||
<h1 className="g-type-display-1">{title}</h1>
|
||||
<div className={s.contentAndLinks}>
|
||||
<p className="g-type-body-large">{description}</p>
|
||||
<div className={s.links}>
|
||||
{links.map((link, index) => {
|
||||
const brand = index === 0 ? 'hashicorp' : 'neutral'
|
||||
const variant = index === 0 ? 'primary' : 'secondary'
|
||||
return (
|
||||
<Button
|
||||
key={link.text}
|
||||
title={link.text}
|
||||
linkType={link.type}
|
||||
url={link.url}
|
||||
theme={{ variant, brand }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.homepageHero}>
|
||||
<Hero
|
||||
data={{
|
||||
title: title,
|
||||
description: description,
|
||||
buttons: links,
|
||||
backgroundTheme: 'light',
|
||||
centered: false,
|
||||
videos: [
|
||||
{
|
||||
name: 'UI',
|
||||
playbackRate: uiVideo.playbackRate,
|
||||
src: [
|
||||
{
|
||||
srcType: uiVideo.srcType,
|
||||
url: uiVideo.url,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'CLI',
|
||||
playbackRate: cliVideo.playbackRate,
|
||||
src: [
|
||||
{
|
||||
srcType: cliVideo.srcType,
|
||||
url: cliVideo.url,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
.root {
|
||||
/* 100% height - nav - footer */
|
||||
min-height: calc(100vh - (64px + 77px));
|
||||
width: 100%;
|
||||
padding: 150px 0;
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--gray-7);
|
||||
background-image: url(/img/hero-pattern.svg);
|
||||
background-size: contain;
|
||||
|
||||
@media (max-width: 758px) {
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
& .eyebrow {
|
||||
font-family: var(--font-body);
|
||||
text-align: center;
|
||||
display: block;
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: 1.1rem;
|
||||
|
||||
& .red {
|
||||
color: #ba2226;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 758px) {
|
||||
background-image: url(/img/mobile-hero-pattern.svg);
|
||||
}
|
||||
|
||||
& h1 {
|
||||
text-align: center;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin: 0 auto 0 auto;
|
||||
text-align: center;
|
||||
max-width: 40em;
|
||||
}
|
||||
|
||||
& :global(.g-grid-container) {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
& .links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-top: calc(32px - 8px);
|
||||
margin-bottom: -8px;
|
||||
|
||||
& a {
|
||||
margin: 8px;
|
||||
|
||||
&:global(.variant-primary) {
|
||||
border-color: var(--boundary);
|
||||
background-color: var(--boundary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
.root {
|
||||
& > h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import LogoList from './logo-list'
|
||||
import s from './feature.module.css'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Feature({
|
||||
title,
|
||||
description,
|
||||
logos,
|
||||
onInViewStatusChanged,
|
||||
}) {
|
||||
const [ref, inView] = useInView({ threshold: 0.8 })
|
||||
const [inViewStatus, setInViewStatus] = useState(false)
|
||||
if (inView != inViewStatus) {
|
||||
setInViewStatus(inView)
|
||||
onInViewStatusChanged(inView)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.root} ref={ref}>
|
||||
<h4 className="g-type-display-4">{title}</h4>
|
||||
<p className="g-type-body">{description}</p>
|
||||
{logos ? <LogoList logos={logos} /> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import s from './logo-list.module.css'
|
||||
|
||||
export default function LogoList({ logos }) {
|
||||
return (
|
||||
<div className={s.root}>
|
||||
<div className={s.logos}>
|
||||
{logos.map((logo, stableIdx) => (
|
||||
<div
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={stableIdx}
|
||||
>
|
||||
<img src={logo.url} alt={logo.alt} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className={`g-type-tag-label ${s.footerText}`}>
|
||||
Integrations coming soon
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
.logos {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
max-width: 440px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > div {
|
||||
margin-right: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.footerText {
|
||||
color: var(--gray-5);
|
||||
}
|
||||
@ -0,0 +1,190 @@
|
||||
.root {
|
||||
--transition-time: 0.7s;
|
||||
|
||||
@media (--medium-up) {
|
||||
position: sticky;
|
||||
top: calc(50vh - (249px));
|
||||
}
|
||||
|
||||
& > svg {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& .boundaryLetter,
|
||||
& .spacer,
|
||||
& .iconBg {
|
||||
fill: var(--white);
|
||||
}
|
||||
|
||||
& .iconLines,
|
||||
& .iconBg {
|
||||
stroke-linecap: round;
|
||||
stroke-width: 1.5px;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
& .boundaryFill,
|
||||
& .arrowHead {
|
||||
fill: #f04e54;
|
||||
}
|
||||
|
||||
& .arrowHead,
|
||||
& .arrowSegment,
|
||||
& .spacer {
|
||||
transition: var(--transition-time) ease;
|
||||
transition-property: opacity;
|
||||
}
|
||||
|
||||
& .arrowSegment {
|
||||
stroke-width: 2px;
|
||||
stroke: #f04e54;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-linecap: round;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
& .dropShadow {
|
||||
opacity: 0.08;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
& .iconLines {
|
||||
stroke: var(--white);
|
||||
fill: none;
|
||||
transition: var(--transition-time) ease;
|
||||
transition-property: stroke;
|
||||
}
|
||||
|
||||
& .subtitle {
|
||||
fill: #b6b8c3;
|
||||
}
|
||||
|
||||
& .iconBg {
|
||||
stroke: #e5e6ec;
|
||||
stroke-width: 1px;
|
||||
transition: var(--transition-time) ease;
|
||||
transition-property: fill, stroke;
|
||||
}
|
||||
|
||||
& .authenticate {
|
||||
& .iconBg {
|
||||
fill: var(--white);
|
||||
}
|
||||
|
||||
& .iconLines {
|
||||
stroke: #e5e6ec;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
& .authorize {
|
||||
& .spacer {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
& .iconBg {
|
||||
fill: var(--black);
|
||||
stroke: var(--black);
|
||||
}
|
||||
|
||||
& .iconLines {
|
||||
stroke: #e5e6ec;
|
||||
}
|
||||
}
|
||||
|
||||
& .access {
|
||||
& .spacer {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
& .iconBg {
|
||||
fill: var(--black);
|
||||
stroke: var(--black);
|
||||
}
|
||||
|
||||
& .vaultIcon {
|
||||
fill: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
& .hostsAndServices {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
transition: var(--transition-time) ease;
|
||||
transition-property: opacity, transform;
|
||||
|
||||
& .leadingLine {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: 1.5px;
|
||||
stroke: var(--black);
|
||||
}
|
||||
}
|
||||
|
||||
& .arrowOne .arrowHead,
|
||||
& .arrowTwo .arrowHead {
|
||||
@media (--small) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& .inactive {
|
||||
& .arrowHead,
|
||||
& .arrowSegment,
|
||||
& .spacer {
|
||||
@media (--medium-up) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.authorize {
|
||||
& .spacer {
|
||||
@media (--medium-up) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
& .iconBg {
|
||||
@media (--medium-up) {
|
||||
fill: var(--white);
|
||||
stroke: #e5e6ec;
|
||||
}
|
||||
}
|
||||
|
||||
& .iconLines {
|
||||
@media (--medium-up) {
|
||||
stroke: #b6b8c3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.access {
|
||||
& .spacer {
|
||||
@media (--medium-up) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
& .iconBg {
|
||||
@media (--medium-up) {
|
||||
fill: var(--white);
|
||||
stroke: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
& .vaultIcon {
|
||||
@media (--medium-up) {
|
||||
fill: var(--black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hostsAndServices {
|
||||
@media (--medium-up) {
|
||||
opacity: 0;
|
||||
transform: translate(0, 10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
.root {
|
||||
padding: 88px 0;
|
||||
|
||||
& .headerWrapper {
|
||||
& h2 {
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 32px;
|
||||
@media (--small) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& p {
|
||||
max-width: 818px;
|
||||
text-align: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
||||
@media (--small) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& > ul {
|
||||
max-width: 470px;
|
||||
margin-left: 128px;
|
||||
padding: 0;
|
||||
|
||||
@media (width < 1120px) {
|
||||
margin-left: 64px;
|
||||
}
|
||||
|
||||
@media (width < 1024px) {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
@media (--small) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diagram {
|
||||
width: 591px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.features {
|
||||
& > li {
|
||||
list-style: none;
|
||||
|
||||
& > div {
|
||||
list-style: none;
|
||||
padding-bottom: 285px;
|
||||
|
||||
@media (--small) {
|
||||
padding-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
& > div {
|
||||
padding-bottom: 355px;
|
||||
margin-bottom: -200px;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
& > div {
|
||||
padding-top: 108px;
|
||||
@media (width < 1120px) {
|
||||
padding-top: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import s from './how-it-works.module.css'
|
||||
import classNames from 'classnames'
|
||||
import HowBoundaryWorksDiagram from './how-boundary-works-diagram'
|
||||
import Feature from './feature'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function HowItWorks({ title, description, features }) {
|
||||
const [activeExampleIndex, setActiveExampleIndex] = useState(0)
|
||||
const [viewportStatus, setViewportStatus] = useState(
|
||||
new Array(features.length).fill(false)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={s.root}>
|
||||
<div className={classNames('g-grid-container', s.headerWrapper)}>
|
||||
<h2 className="g-type-display-2">{title}</h2>
|
||||
<p className="g-type-body-large">{description}</p>
|
||||
</div>
|
||||
<div className={`g-grid-container ${s.contentContainer}`}>
|
||||
<div className={s.diagram}>
|
||||
<HowBoundaryWorksDiagram activeExampleIndex={activeExampleIndex} />
|
||||
</div>
|
||||
<ul className={s.features}>
|
||||
{features.map((feature, index) => (
|
||||
<li key={feature.title}>
|
||||
<Feature
|
||||
{...feature}
|
||||
onInViewStatusChanged={(state) => {
|
||||
const newStatusArray = [...viewportStatus]
|
||||
newStatusArray[index] = state
|
||||
setViewportStatus(newStatusArray)
|
||||
// Calculate the first element in focus, set that as
|
||||
// our new activeExampleIndex. If it's been updated
|
||||
// notify the subscriber.
|
||||
const newExampleIndex = newStatusArray.lastIndexOf(true)
|
||||
if (
|
||||
activeExampleIndex != newExampleIndex &&
|
||||
newExampleIndex != -1
|
||||
) {
|
||||
setActiveExampleIndex(newExampleIndex)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import styles from './section-break-cta.module.css'
|
||||
import Button from '@hashicorp/react-button'
|
||||
|
||||
export default function SectionBreakCta({ heading, content, link }) {
|
||||
return (
|
||||
<div className={styles.sectionBreakCta}>
|
||||
<hr />
|
||||
<h4 className="g-type-display-4">{heading}</h4>
|
||||
<p className="g-type-body">{content}</p>
|
||||
<Button
|
||||
title={link.text}
|
||||
url={link.url}
|
||||
theme={{
|
||||
brand: 'neutral',
|
||||
variant: 'tertiary-neutral',
|
||||
background: 'light',
|
||||
}}
|
||||
linkType="outbound"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
.sectionBreakCta {
|
||||
padding: 60px 30px;
|
||||
max-width: 800px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 12px rgba(37, 38, 45, 0.08);
|
||||
margin: 0 auto;
|
||||
background-color: var(--white);
|
||||
|
||||
@media (width <= 880px) {
|
||||
margin: 0 40px;
|
||||
}
|
||||
|
||||
& > h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
& hr {
|
||||
width: 64px;
|
||||
background-color: var(--red);
|
||||
margin-top: 0;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import styles from './signin-error.module.css'
|
||||
import Button from '@hashicorp/react-button'
|
||||
import { useAuthProviders } from 'components/auth-gate'
|
||||
|
||||
export default function SigninErrorPage() {
|
||||
const authProviders = useAuthProviders()
|
||||
return (
|
||||
<div className={styles.signinErrorWrapper}>
|
||||
<h1 className="g-type-display-3">
|
||||
Sorry! <br />
|
||||
It seems you do not have appropriate permissions to view this content.
|
||||
</h1>
|
||||
{authProviders && (
|
||||
<div className={styles.logoutCard}>
|
||||
<div className={styles.authProviderGoBack}>
|
||||
<h4>{`If you'd like to try again with another Okta account, please log out of Okta`}</h4>
|
||||
{process.env.NEXT_PUBLIC_OKTA_DOMAIN && (
|
||||
<Button
|
||||
url={`https://${process.env.NEXT_PUBLIC_OKTA_DOMAIN}`}
|
||||
title={`Go to Okta`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.authProviderGoBack}>
|
||||
<h4>{`If you'd like to try again with another Auth0 account, please go back`}</h4>
|
||||
<Button url="/" title="Go back" />
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
.signinErrorWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.logoutCard {
|
||||
margin-top: 3rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
border: 1px solid var(--gray-6);
|
||||
}
|
||||
|
||||
.authProviderGoBack {
|
||||
display: flex;
|
||||
flex: 0 1 600px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
export const ALERT_BANNER_ACTIVE = false
|
||||
|
||||
export default {
|
||||
tag: 'ANNOUNCING',
|
||||
text: 'Some details about an announcement here',
|
||||
url: 'https://hashicorp.com',
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
export const packageManagers = {
|
||||
homebrew: {
|
||||
label: 'Homebrew',
|
||||
url: '',
|
||||
commands: ['brew tap hashicorp/tap', 'brew install hashicorp/tap/boundary'],
|
||||
},
|
||||
ubuntu: {
|
||||
label: 'Ubuntu/Debian',
|
||||
commands: [
|
||||
'curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -',
|
||||
'sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"',
|
||||
'sudo apt-get update && sudo apt-get install boundary',
|
||||
],
|
||||
},
|
||||
centos: {
|
||||
label: 'CentOS/RHEL',
|
||||
commands: [
|
||||
'sudo yum install -y yum-utils',
|
||||
'sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo',
|
||||
'sudo yum -y install boundary',
|
||||
],
|
||||
},
|
||||
fedora: {
|
||||
label: 'Fedora',
|
||||
commands: [
|
||||
'sudo dnf install -y dnf-plugins-core',
|
||||
'sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/fedora/hashicorp.repo',
|
||||
'sudo dnf -y install boundary',
|
||||
],
|
||||
},
|
||||
amazonLinux: {
|
||||
label: 'Amazon Linux',
|
||||
commands: [
|
||||
'sudo yum install -y yum-utils',
|
||||
'sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo',
|
||||
'sudo yum -y install boundary',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const packageManagersByOs = {
|
||||
darwin: packageManagers.homebrew,
|
||||
linux: [
|
||||
packageManagers.ubuntu,
|
||||
packageManagers.centos,
|
||||
packageManagers.fedora,
|
||||
packageManagers.amazonLinux,
|
||||
],
|
||||
}
|
||||
|
||||
export const getStartedLinks = [
|
||||
{
|
||||
label: 'Install Boundary',
|
||||
href:
|
||||
'https://learn.hashicorp.com/tutorials/boundary/getting-started-install',
|
||||
},
|
||||
{
|
||||
label: 'Introduction to Boundary',
|
||||
href:
|
||||
'https://learn.hashicorp.com/tutorials/boundary/getting-started-intro',
|
||||
},
|
||||
{
|
||||
label: 'Start a Development Environment',
|
||||
href: 'https://learn.hashicorp.com/tutorials/boundary/getting-started-dev',
|
||||
},
|
||||
]
|
||||
@ -1,10 +1,28 @@
|
||||
export default [
|
||||
{
|
||||
text: 'Overview',
|
||||
url: '/',
|
||||
type: 'inbound',
|
||||
},
|
||||
'divider',
|
||||
{
|
||||
text: 'Tutorials',
|
||||
url: 'https://learn.hashicorp.com/boundary',
|
||||
type: 'inbound',
|
||||
},
|
||||
{
|
||||
text: 'Docs',
|
||||
url: '/docs/getting-started',
|
||||
url: '/docs',
|
||||
type: 'inbound',
|
||||
},
|
||||
{
|
||||
text: 'API',
|
||||
url: '/api-docs',
|
||||
type: 'inbound',
|
||||
},
|
||||
{
|
||||
text: 'Community',
|
||||
url: '/community',
|
||||
type: 'inbound',
|
||||
},
|
||||
]
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import NextAuthProviders from 'next-auth/providers'
|
||||
|
||||
function formatProviderConfig(ap) {
|
||||
const apName = ap.toUpperCase()
|
||||
const config = {
|
||||
clientId: process.env[`${apName}_CLIENT_ID`],
|
||||
clientSecret: process.env[`${apName}_CLIENT_SECRET`],
|
||||
domain: process.env[`${apName}_DOMAIN`],
|
||||
}
|
||||
return NextAuthProviders[ap](config)
|
||||
}
|
||||
|
||||
export default (req, res) => ({ environments, pages }) =>
|
||||
NextAuth(req, res, {
|
||||
providers:
|
||||
environments[process.env.HASHI_ENV]?.map(formatProviderConfig) || [],
|
||||
pages,
|
||||
})
|
||||
@ -1,12 +0,0 @@
|
||||
import nextAuthApiRoute from 'lib/next-auth-utils/config'
|
||||
|
||||
export default (req, res) =>
|
||||
nextAuthApiRoute(
|
||||
req,
|
||||
res
|
||||
)({
|
||||
environments: { production: ['Okta', 'Auth0'], preview: ['Okta', 'Auth0'] },
|
||||
pages: {
|
||||
error: '/signin-error', // Error code passed in query string as ?error=
|
||||
},
|
||||
})
|
||||
@ -0,0 +1,21 @@
|
||||
#p-community {
|
||||
max-width: var(--site-max-width);
|
||||
margin: 72px auto;
|
||||
|
||||
& .g-section-header {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
& .g-vertical-text-block-list {
|
||||
& .list {
|
||||
& .body-text {
|
||||
& a {
|
||||
color: var(--red);
|
||||
&::after {
|
||||
background-color: var(--red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@ -1,4 +0,0 @@
|
||||
#p-downloads {
|
||||
margin-top: 72px;
|
||||
margin-bottom: 72px;
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
.root {
|
||||
margin-top: 72px;
|
||||
|
||||
& a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
--brand: var(--red);
|
||||
--brand-d1: #c03e43;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.hosting {
|
||||
border: 1px solid var(--gray-6);
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--gray-2);
|
||||
}
|
||||
|
||||
.gettingStarted {
|
||||
max-width: 680px;
|
||||
margin: 40px auto 64px;
|
||||
text-align: center;
|
||||
|
||||
& p {
|
||||
color: var(--gray-3);
|
||||
}
|
||||
|
||||
& a {
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
& h2 {
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
@media (--medium-up) {
|
||||
margin: 88px auto 128px;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-top: 32px;
|
||||
& a {
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.signUpButton {
|
||||
margin-left: 26px;
|
||||
}
|
||||
|
After Width: | Height: | Size: 573 B |
|
After Width: | Height: | Size: 769 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 625 B |
|
After Width: | Height: | Size: 701 B |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 714 B |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 913 B |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 270 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 377 B |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 31 KiB |
@ -1,3 +1,49 @@
|
||||
p {
|
||||
color: black;
|
||||
.p-home {
|
||||
& .break-section {
|
||||
background: linear-gradient(
|
||||
var(--gray-7) 0%,
|
||||
var(--gray-7) 50%,
|
||||
var(--white) 50%,
|
||||
var(--white) 100%
|
||||
);
|
||||
}
|
||||
|
||||
& .features-section {
|
||||
padding: 128px 0;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 88px 0;
|
||||
}
|
||||
|
||||
& > * {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& .use-cases-section {
|
||||
padding-top: 128px;
|
||||
padding-bottom: 128px;
|
||||
background-color: var(--gray-7);
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding-top: 88px;
|
||||
}
|
||||
|
||||
& .g-use-cases {
|
||||
& .icon {
|
||||
min-height: 140px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
& h2 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
margin-bottom: 64px;
|
||||
@media (max-width: 800px) {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import styles from './styles.module.css'
|
||||
|
||||
export default function SecurityPage() {
|
||||
return (
|
||||
<div className={styles.securityPage}>
|
||||
<div className="g-container">
|
||||
<div className={styles.longformWrapper}>
|
||||
<h2>Security</h2>
|
||||
<p>
|
||||
We understand that many users place a high level of trust in
|
||||
HashiCorp and the tools we build. We apply best practices and focus
|
||||
on security to make sure we can maintain the trust of the community.
|
||||
</p>
|
||||
<p>
|
||||
We deeply appreciate any effort to disclose vulnerabilities
|
||||
responsibly.
|
||||
</p>
|
||||
<p>
|
||||
If you would like to report a vulnerability, please see the{' '}
|
||||
<a href="https://www.hashicorp.com/security">
|
||||
HashiCorp security page
|
||||
</a>{' '}
|
||||
which has the proper email to communicate with as well as our PGP
|
||||
key.
|
||||
</p>
|
||||
<p>
|
||||
{' '}
|
||||
If you aren't reporting a security sensitive vulnerability,
|
||||
please open an issue on the standard{' '}
|
||||
<a href="https://github.com/hashicorp/boundary">GitHub</a>{' '}
|
||||
repository.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
.securityPage {
|
||||
& .longformWrapper {
|
||||
@media (min-width: 800px) {
|
||||
margin-top: 80px;
|
||||
margin-bottom: 80px;
|
||||
max-width: 70%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
& a {
|
||||
color: var(--red);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import PageComponent from 'components/signin-error-page'
|
||||
|
||||
export default function SigninErrorPage() {
|
||||
return <PageComponent />
|
||||
}
|
||||
|
After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 176 KiB |