Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ COPY public ./public

# Build application
RUN npm run build && \
chmod 644 /app/dist/favicon.ico || true
(chmod 644 /app/dist/favicon.ico || true)

# Production stage
FROM nginx:stable-alpine AS production
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added public/assets/logos/gradle-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/logos/maven-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 25 additions & 6 deletions public/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,47 @@ window.theiaCloudConfig = {
additionalApps: [
{
appId: "c-latest",
appName: "C"
appName: "C",
buildSystems: [
{ id: "makefile", label: "Makefile"}
]
},
{
appId: "java-17-latest",
appName: "Java",
image: "java-17"
image: "java-17",
buildSystems: [
{ id: "maven", label: "Maven" },
{ id: "gradle", label: "Gradle "},
]
},
{
appId: "javascript-latest",
appName: "Javascript"
appName: "Javascript",
buildSystems: [
{ id: "npm", label: "npm"}
]
},
{
appId: "ocaml-latest",
appName: "Ocaml"
appName: "Ocaml",
buildSystems: [
{ id: "dune", label: "Dune"}
]
},
{
appId: "python-latest",
appName: "Python"
appName: "Python",
buildSystems: [
{ id: "pip", label: "pip"}
]
},
{
appId: "rust-latest",
appName: "Rust"
appName: "Rust",
buildSystems: [
{ id: "cargo", label: "Cargo"}
]
}
],
disableInfo: true,
Expand Down
14 changes: 14 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@
margin: 0.5rem auto 1rem auto;
}

.Build-System__list {
display: flex;
justify-content: center;
gap: 20px;

max-width: 800px;
margin: 0.5rem auto 1rem auto;
padding: 24px;
}

.Build-System__list > * {
flex: 0 1 150px;
}

.App__grid-item {
background: var(--color-background-card);
color: var(--color-text-primary);
Expand Down
102 changes: 50 additions & 52 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Loading } from './components/Loading';
import { LoginButton } from './components/LoginButton';
import { Privacy } from './components/Privacy';
import { SelectApp } from './components/SelectApp';
import { SelectBuildSystem } from './components/SelectBuildSystem';
import { VantaBackground } from './components/VantaBackground';

// global state to be kept between render calls
Expand Down Expand Up @@ -122,6 +123,9 @@ function App(): JSX.Element {

const [autoStart, setAutoStart] = useState<boolean>(false);

const [standaloneWizardStep, setStandaloneWizardStep] = useState<'language' | 'buildSystem'>('language');
const [standaloneAppDef, setStandaloneAppDef] = useState<string>();

if (!initialized) {
const urlParams = new URLSearchParams(window.location.search);

Expand Down Expand Up @@ -238,11 +242,10 @@ function App(): JSX.Element {
}

const handleStartSession = useCallback(
(appDefinition: string): void => {
(appDefinition: string, buildSystemId?: string): void => {
setLoading(true);
setError(undefined);

// first check if the service is available. if not we are doing maintenance and should adapt the error message accordingly
TheiaCloud.ping(PingRequest.create(config.serviceUrl, getServiceAuthToken(config)))
.then(() => {
// ping successful continue with launch
Expand Down Expand Up @@ -283,51 +286,20 @@ function App(): JSX.Element {
accessToken: token
};

/*
const sessionStartRequest: SessionStartRequest = {
serviceUrl: config.serviceUrl,
appId: config.appId,
user: config.useKeycloak ? email! : user!,
appDefinition,
workspaceName: workspace,
timeout: 180,
env: {
fromMap: {
THEIA: 'true',
ARTEMIS_TOKEN: artemisToken!,
ARTEMIS_CLONE_URL: gitUri!
}
}
};

TheiaCloud.Session.startSession(
sessionStartRequest,
requestOptions
).catch((err: Error) => {
if (err && (err as any).status === 473) {
setError(
`The app definition '${appDefinition}' is not available in the cluster.\n` +
'Please try launching another application.'
);
return;
}
setError(err.message);
})
.finally(() => {
setLoading(false);
});
*/

const launchEnv = {
fromMap: {
THEIA: 'true',
ARTEMIS_TOKEN: artemisToken!,
ARTEMIS_URL: artemisUrl!,
GIT_URI: gitUri!,
GIT_USER: gitUser!,
GIT_MAIL: gitMail!
}
const envFromMap: Record<string, string> = {
THEIA: 'true',
ARTEMIS_TOKEN: artemisToken!,
ARTEMIS_URL: artemisUrl!,
GIT_URI: gitUri!,
GIT_USER: gitUser!,
GIT_MAIL: gitMail!
};
if (buildSystemId) {
envFromMap.STANDALONE_MODE = 'true';
envFromMap.BUILD_SYSTEM = buildSystemId;
}

const launchEnv = { fromMap: envFromMap };
const launchUser = config.useKeycloak ? email! : user!;
const serviceAuthToken = getServiceAuthToken(config);
const createWorkspaceLaunchRequest = (): LaunchRequest => ({
Expand Down Expand Up @@ -417,6 +389,22 @@ function App(): JSX.Element {
[config, gitUri, username, user, token, artemisToken, artemisUrl, gitUser, gitMail, email]
);

const handleAppSelected = (appId: string, _: string): void => {
const isStandaloneMode = !artemisToken && !gitUri;
if (isStandaloneMode) {
const appDef = config.additionalApps?.find(a => (a.serviceAuthToken || a.appId) === appId);
const buildSystems = appDef?.buildSystems ?? [];
if (buildSystems.length <= 1) {
handleStartSession(appId, buildSystems[0]?.id ?? 'none');
} else {
setStandaloneAppDef(appId);
setStandaloneWizardStep('buildSystem');
}
} else {
handleStartSession(appId);
}
};

useEffect(() => {
if (!initialized) {
return;
Expand All @@ -441,20 +429,20 @@ function App(): JSX.Element {

const authenticate: () => void = (): void => {
const keycloak = new Keycloak(keycloakConfig);
const redirectUri = window.location.origin + window.location.pathname + window.location.search;

keycloak
.init({
redirectUri: window.location.origin + window.location.pathname,
redirectUri,
checkLoginIframe: false
})
.then((authenticated: boolean) => {
if (!authenticated) {
keycloak.login({
redirectUri: window.location.origin + window.location.pathname,
redirectUri,
action: 'webauthn-register-passwordless:skip_if_exists'
});
} else {
// If we are already authenticated (e.g. session existed but UI wasn't updated), update state
const parsedToken = keycloak.idTokenParsed;
if (parsedToken) {
const userMail = parsedToken.email;
Expand All @@ -474,7 +462,6 @@ function App(): JSX.Element {
const needsLogin = config.useKeycloak && !token;
const logoFileExtension = config.logoFileExtension ?? 'svg';

// Render different pages based on currentPage state
if (currentPage === 'imprint') {
return (
<div className='App'>
Expand All @@ -495,6 +482,9 @@ function App(): JSX.Element {
);
}

const standaloneAppBuildSystems =
config.additionalApps?.find(a => (a.serviceAuthToken || a.appId) === standaloneAppDef)?.buildSystems ?? [];

return (
<div className='App'>
<VantaBackground>
Expand All @@ -511,7 +501,9 @@ function App(): JSX.Element {
<div>
<div style={{ marginTop: '2rem' }}></div>
<AppLogo fileExtension={logoFileExtension} />
<h2 className='App__title'>Choose your Online IDE</h2>
<h2 className='App__title'>
{standaloneWizardStep === 'buildSystem' ? 'Choose your build system' : 'Choose your Online IDE'}
</h2>
<div>
{needsLogin ? (
<LoginButton login={authenticate} />
Expand All @@ -521,8 +513,14 @@ function App(): JSX.Element {
appDefinition={selectedAppDefinition}
onStartSession={handleStartSession}
/>
) : standaloneWizardStep === 'buildSystem' ? (
<SelectBuildSystem
buildSystems={standaloneAppBuildSystems}
onSelect={buildSystemId => handleStartSession(standaloneAppDef!, buildSystemId)}
onBack={() => setStandaloneWizardStep('language')}
/>
) : (
<SelectApp appDefinitions={config.additionalApps} onStartSession={handleStartSession} />
<SelectApp appDefinitions={config.additionalApps} onSelectApp={handleAppSelected} />
)}
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions src/common-extensions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,21 @@ export interface FooterLinksConfig {
};
}

/**
* A single build system option for a language app
*/
export interface BuildSystemOption {
id: string;
label: string;
}

/**
* Extended AppDefinition with service authentication token
* Bridges the gap between the package's ServiceConfig and legacy usage
*/
export type ExtendedAppDefinition = AppDefinition & {
serviceAuthToken?: string;
buildSystems?: BuildSystemOption[]
image?: string;
Image?: string;
};
Expand Down
9 changes: 5 additions & 4 deletions src/components/SelectApp.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import type { ExtendedAppDefinition } from '../common-extensions/types';

interface SelectAppProps {
appDefinitions: ExtendedAppDefinition[] | undefined;
onStartSession: (appDefinition: string) => void;
onSelectApp: (appId: string, appName: string) => void;
}

function normalizeLogoName(value: string): string {
Expand All @@ -29,22 +30,22 @@ function getAppLogoSrc(app: ExtendedAppDefinition): string {
return `/assets/logos/${normalizeLogoName(trimmedImage)}-logo.png`;
}

export const SelectApp: React.FC<SelectAppProps> = ({ appDefinitions, onStartSession }: SelectAppProps) => (
export const SelectApp: React.FC<SelectAppProps> = ({ appDefinitions, onSelectApp }: SelectAppProps) => (
<div className='App__grid'>
{appDefinitions &&
appDefinitions.map((app, index) => (
<button
key={index}
className='App__grid-item'
onClick={() => onStartSession(app.serviceAuthToken || app.appId)}
onClick={() => onSelectApp(app.serviceAuthToken || app.appId, app.appName)}
data-testid={`launch-app-${app.serviceAuthToken || app.appId}`}
>
<img
src={getAppLogoSrc(app)}
alt={`${app.appName} logo`}
className='App__grid-item-logo'
/>
<div className='App__grid-item-launch'>Launch</div>
<div className='App__grid-item-launch'>Select</div>
<div className='App__grid-item-text'>{app.appName}</div>
</button>
))}
Expand Down
35 changes: 35 additions & 0 deletions src/components/SelectBuildSystem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import type { BuildSystemOption } from '../common-extensions/types';

interface SelectBuildSystemProps {
buildSystems: BuildSystemOption[];
onSelect: (buildSystemId: string) => void;
onBack: () => void;
}

export const SelectBuildSystem: React.FC<SelectBuildSystemProps> = ({ buildSystems, onSelect, onBack }) => (
<div>
<div className='Build-System__list'>
{buildSystems.map((option, index) => (
<button
key={index}
className='App__grid-item'
onClick={() => onSelect(option.id)}
data-testid={`build-system-${option.id}`}
>
<img
src={`/assets/logos/${option.id.toLowerCase().replace(/\s+/g, '-')}-logo.png`}
alt={`${option.id} logo`}
className='App__grid-item-logo'
/>
<div className='App__grid-item-launch'>Select</div>
<div className='App__grid-item-text'>{option.label}</div>
</button>
))}

</div>
<button className='App__try-now-button App__back-button' onClick={onBack}>
&larr; Back
</button>
</div>
);
Loading