Skip to content

Commit dc65f98

Browse files
feat(relocation): Get Started and Encrypt Backup pages (#61322)
This PR includes the get started page and encrypt backup page. If `relocation:enabled` is not set, the page will not load <img width="1174" alt="Screenshot 2023-12-07 at 1 20 51 PM" src="https://github.com/getsentry/sentry/assets/25517925/4c43601c-4452-48d2-bc2b-03348d7dfffd"> <img width="1172" alt="Screenshot 2023-12-07 at 1 21 00 PM" src="https://github.com/getsentry/sentry/assets/25517925/18aee989-dbbc-4663-8c9c-d6d58b097274">
1 parent 7f5e7fd commit dc65f98

File tree

10 files changed

+797
-0
lines changed

10 files changed

+797
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {createContext, useCallback} from 'react';
2+
3+
import {useSessionStorage} from 'sentry/utils/useSessionStorage';
4+
5+
type Data = {
6+
orgSlugs: string;
7+
region: string;
8+
file?: File;
9+
};
10+
11+
export type RelocationOnboardingContextProps = {
12+
data: Data;
13+
setData: (data: Data) => void;
14+
};
15+
16+
export const RelocationOnboardingContext =
17+
createContext<RelocationOnboardingContextProps>({
18+
data: {
19+
orgSlugs: '',
20+
region: '',
21+
file: undefined,
22+
},
23+
setData: () => {},
24+
});
25+
26+
type ProviderProps = {
27+
children: React.ReactNode;
28+
value?: Data;
29+
};
30+
31+
export function RelocationOnboardingContextProvider({children, value}: ProviderProps) {
32+
const [sessionStorage, setSessionStorage] = useSessionStorage<Data>(
33+
'relocationOnboarding',
34+
{
35+
orgSlugs: value?.orgSlugs || '',
36+
region: value?.region || '',
37+
file: value?.file || undefined,
38+
}
39+
);
40+
41+
const setData = useCallback(
42+
(data: Data) => {
43+
setSessionStorage(data);
44+
},
45+
[setSessionStorage]
46+
);
47+
48+
return (
49+
<RelocationOnboardingContext.Provider
50+
value={{
51+
data: sessionStorage,
52+
setData,
53+
}}
54+
>
55+
{children}
56+
</RelocationOnboardingContext.Provider>
57+
);
58+
}

static/app/routes.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,27 @@ function buildRoutes() {
279279
)}
280280
key="org-join-request"
281281
/>
282+
{usingCustomerDomain && (
283+
<Route
284+
path="/relocation/"
285+
component={errorHandler(withDomainRequired(OrganizationContextContainer))}
286+
key="orgless-relocation"
287+
>
288+
<IndexRedirect to="welcome/" />
289+
<Route
290+
path=":step/"
291+
component={make(() => import('sentry/views/relocation'))}
292+
/>
293+
</Route>
294+
)}
295+
<Route
296+
path="/relocation/:orgId/"
297+
component={withDomainRedirect(errorHandler(OrganizationContextContainer))}
298+
key="org-relocation"
299+
>
300+
<IndexRedirect to="welcome/" />
301+
<Route path=":step/" component={make(() => import('sentry/views/relocation'))} />
302+
</Route>
282303
{usingCustomerDomain && (
283304
<Route
284305
path="/onboarding/"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import styled from '@emotion/styled';
2+
import {motion} from 'framer-motion';
3+
4+
import {space} from 'sentry/styles/space';
5+
import testableTransition from 'sentry/utils/testableTransition';
6+
7+
const StepHeading = styled(motion.h2)<{step: number}>`
8+
position: relative;
9+
display: inline-grid;
10+
grid-template-columns: max-content auto;
11+
gap: ${space(2)};
12+
align-items: center;
13+
font-size: 21px;
14+
15+
&:before {
16+
content: '${p => p.step}';
17+
display: flex;
18+
align-items: center;
19+
justify-content: center;
20+
width: 30px;
21+
height: 30px;
22+
background-color: ${p => p.theme.yellow300};
23+
border-radius: 50%;
24+
color: ${p => p.theme.textColor};
25+
font-size: 1rem;
26+
}
27+
`;
28+
29+
StepHeading.defaultProps = {
30+
variants: {
31+
initial: {clipPath: 'inset(0% 100% 0% 0%)', opacity: 1},
32+
animate: {clipPath: 'inset(0% 0% 0% 0%)', opacity: 1},
33+
exit: {opacity: 0},
34+
},
35+
transition: testableTransition({
36+
duration: 0.3,
37+
}),
38+
};
39+
40+
export default StepHeading;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import styled from '@emotion/styled';
2+
import {motion} from 'framer-motion';
3+
4+
import {Button} from 'sentry/components/button';
5+
import {CodeSnippet} from 'sentry/components/codeSnippet';
6+
import {t} from 'sentry/locale';
7+
import {space} from 'sentry/styles/space';
8+
import testableTransition from 'sentry/utils/testableTransition';
9+
import StepHeading from 'sentry/views/relocation/components/stepHeading';
10+
11+
import {StepProps} from './types';
12+
13+
export function EncryptBackup(props: StepProps) {
14+
const code =
15+
'./sentry-admin.sh export global --encrypt-with /path/to/public_key.pub\n/path/to/encrypted/backup/file.tar';
16+
return (
17+
<Wrapper>
18+
<StepHeading step={3}>
19+
{t('Create an encrypted backup of current self-hosted instance')}
20+
</StepHeading>
21+
<motion.div
22+
transition={testableTransition()}
23+
variants={{
24+
initial: {y: 30, opacity: 0},
25+
animate: {y: 0, opacity: 1},
26+
exit: {opacity: 0},
27+
}}
28+
>
29+
<p>
30+
{t(
31+
'You’ll need to have the public key saved in the previous step accessible when you run the following command in your terminal. Make sure your current working directory is the root of your `self-hosted` install when you execute it.'
32+
)}
33+
</p>
34+
<EncryptCodeSnippet
35+
dark
36+
language="bash"
37+
filename=">_ TERMINAL"
38+
hideCopyButton={false}
39+
>
40+
{code}
41+
</EncryptCodeSnippet>
42+
<p className="encrypt-help">
43+
<b>{t('Understanding the command:')}</b>
44+
</p>
45+
<p>
46+
<mark>{'./sentry-admin.sh'}</mark>
47+
{t('this is a script present in your self-hosted installation')}
48+
</p>
49+
<p>
50+
<mark>{'/path/to/public/key/file.pub'}</mark>
51+
{t('path to file you created in the previous step')}
52+
</p>
53+
<p>
54+
<mark>{'/path/to/encrypted/backup/output/file.tar'}</mark>
55+
{t('file that will be uploaded in the next step')}
56+
</p>
57+
<ContinueButton size="md" priority="primary" onClick={() => props.onComplete()}>
58+
{t('Continue')}
59+
</ContinueButton>
60+
</motion.div>
61+
</Wrapper>
62+
);
63+
}
64+
65+
export default EncryptBackup;
66+
67+
const EncryptCodeSnippet = styled(CodeSnippet)`
68+
margin: ${space(2)} 0 ${space(4)};
69+
padding: 4px;
70+
`;
71+
72+
const Wrapper = styled('div')`
73+
max-width: 769px;
74+
max-height: 525px;
75+
margin-left: auto;
76+
margin-right: auto;
77+
padding: ${space(4)};
78+
background-color: ${p => p.theme.surface400};
79+
z-index: 100;
80+
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.05);
81+
border-radius: 10px;
82+
width: 100%;
83+
color: ${p => p.theme.gray300};
84+
mark {
85+
border-radius: 8px;
86+
padding: ${space(0.25)} ${space(0.5)} ${space(0.25)} ${space(0.5)};
87+
background: ${p => p.theme.gray100};
88+
margin-right: ${space(1)};
89+
}
90+
h2 {
91+
color: ${p => p.theme.gray500};
92+
}
93+
p {
94+
margin-bottom: ${space(1)};
95+
}
96+
.encrypt-help {
97+
color: ${p => p.theme.gray500};
98+
}
99+
`;
100+
101+
const ContinueButton = styled(Button)`
102+
margin-top: ${space(1.5)};
103+
`;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import {useContext, useState} from 'react';
2+
import styled from '@emotion/styled';
3+
import {motion} from 'framer-motion';
4+
5+
import {Button} from 'sentry/components/button';
6+
import SelectControl from 'sentry/components/forms/controls/selectControl';
7+
import Input from 'sentry/components/input';
8+
import {RelocationOnboardingContext} from 'sentry/components/onboarding/relocationOnboardingContext';
9+
import {t} from 'sentry/locale';
10+
import ConfigStore from 'sentry/stores/configStore';
11+
import {space} from 'sentry/styles/space';
12+
import testableTransition from 'sentry/utils/testableTransition';
13+
import StepHeading from 'sentry/views/relocation/components/stepHeading';
14+
15+
import {StepProps} from './types';
16+
17+
function GetStarted(props: StepProps) {
18+
const regions = ConfigStore.get('regions');
19+
const [region, setRegion] = useState('');
20+
const [orgSlugs, setOrgSlugs] = useState('');
21+
const relocationOnboardingContext = useContext(RelocationOnboardingContext);
22+
23+
const handleContinue = (event: any) => {
24+
event.preventDefault();
25+
relocationOnboardingContext.setData({orgSlugs, region});
26+
props.onComplete();
27+
};
28+
// TODO(getsentry/team-ospo#214): Make a popup to warn users about data region selection
29+
return (
30+
<Wrapper>
31+
<StepHeading step={1}>{t('Basic information needed to get started')}</StepHeading>
32+
<motion.div
33+
transition={testableTransition()}
34+
variants={{
35+
initial: {y: 30, opacity: 0},
36+
animate: {y: 0, opacity: 1},
37+
exit: {opacity: 0},
38+
}}
39+
>
40+
<Form onSubmit={handleContinue}>
41+
<p>
42+
{t(
43+
'In order to best facilitate the process some basic information will be required to ensure sucess with the relocation process of you self-hosted instance'
44+
)}
45+
</p>
46+
<RequiredLabel>{t('Organization slugs being relocated')}</RequiredLabel>
47+
<Input
48+
type="text"
49+
name="orgs"
50+
aria-label="org-slugs"
51+
onChange={evt => setOrgSlugs(evt.target.value)}
52+
required
53+
minLength={3}
54+
placeholder="org-slug-1, org-slug-2, ..."
55+
/>
56+
<Label>{t('Choose a datacenter region')}</Label>
57+
<RegionSelect
58+
value={region}
59+
name="region"
60+
aria-label="region"
61+
placeholder="Select Region"
62+
options={regions.map(r => ({label: r.name, value: r.name}))}
63+
onChange={opt => setRegion(opt.value)}
64+
/>
65+
{region && <p>{t('This is an important decision and cannot be changed.')}</p>}
66+
<ContinueButton
67+
disabled={!orgSlugs || !region}
68+
size="md"
69+
priority="primary"
70+
type="submit"
71+
>
72+
{t('Continue')}
73+
</ContinueButton>
74+
</Form>
75+
</motion.div>
76+
</Wrapper>
77+
);
78+
}
79+
80+
export default GetStarted;
81+
82+
const AnimatedContentWrapper = styled(motion.div)`
83+
overflow: hidden;
84+
`;
85+
86+
AnimatedContentWrapper.defaultProps = {
87+
initial: {
88+
height: 0,
89+
},
90+
animate: {
91+
height: 'auto',
92+
},
93+
exit: {
94+
height: 0,
95+
},
96+
};
97+
98+
const DocsWrapper = styled(motion.div)``;
99+
100+
DocsWrapper.defaultProps = {
101+
initial: {opacity: 0, y: 40},
102+
animate: {opacity: 1, y: 0},
103+
exit: {opacity: 0},
104+
};
105+
106+
const Wrapper = styled('div')`
107+
margin-left: auto;
108+
margin-right: auto;
109+
padding: ${space(4)};
110+
background-color: ${p => p.theme.surface400};
111+
z-index: 100;
112+
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.05);
113+
border-radius: 10px;
114+
max-width: 769px;
115+
max-height: 525px;
116+
color: ${p => p.theme.gray300};
117+
h2 {
118+
color: ${p => p.theme.gray500};
119+
}
120+
`;
121+
122+
const ContinueButton = styled(Button)`
123+
margin-top: ${space(4)};
124+
`;
125+
126+
const Form = styled('form')`
127+
position: relative;
128+
`;
129+
130+
const Label = styled('label')`
131+
display: block;
132+
text-transform: uppercase;
133+
color: ${p => p.theme.gray500};
134+
margin-top: ${space(2)};
135+
`;
136+
137+
const RequiredLabel = styled('label')`
138+
display: block;
139+
text-transform: uppercase;
140+
color: ${p => p.theme.gray500};
141+
margin-top: ${space(2)};
142+
&:after {
143+
content: '•';
144+
width: 6px;
145+
color: ${p => p.theme.red300};
146+
}
147+
`;
148+
149+
const RegionSelect = styled(SelectControl)`
150+
padding-bottom: ${space(2)};
151+
button {
152+
width: 709px;
153+
}
154+
`;

0 commit comments

Comments
 (0)