Skip to content

Commit

Permalink
feat: Add C3 config (#4881)
Browse files Browse the repository at this point in the history
Co-authored-by: marcosgvieira <marcos.vieira@camunda.com>
  • Loading branch information
vsgoulart and marcosgvieira committed Apr 15, 2024
1 parent a55d273 commit 3f884eb
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 117 deletions.
17 changes: 10 additions & 7 deletions tasklist/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {TrackPagination} from 'modules/tracking/TrackPagination';
import {ReactQueryProvider} from 'modules/react-query/ReactQueryProvider';
import {ErrorWithinLayout, FallbackErrorPage} from 'errorBoundaries';
import {tracking} from 'modules/tracking';
import {C3Provider} from 'C3Provider';

const Wrapper: React.FC = () => {
return (
Expand Down Expand Up @@ -92,13 +93,15 @@ const App: React.FC = () => {

return (
<ErrorBoundary FallbackComponent={FallbackErrorPage}>
<ThemeProvider>
<ReactQueryProvider>
<Notifications />
<NetworkStatusWatcher />
<RouterProvider router={router} />
</ReactQueryProvider>
</ThemeProvider>
<C3Provider>
<ThemeProvider>
<ReactQueryProvider>
<Notifications />
<NetworkStatusWatcher />
<RouterProvider router={router} />
</ReactQueryProvider>
</ThemeProvider>
</C3Provider>
</ErrorBoundary>
);
};
Expand Down
80 changes: 80 additions & 0 deletions tasklist/client/src/C3Provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright Camunda Services GmbH
*
* BY INSTALLING, DOWNLOADING, ACCESSING, USING, OR DISTRIBUTING THE SOFTWARE ("USE"), YOU INDICATE YOUR ACCEPTANCE TO AND ARE ENTERING INTO A CONTRACT WITH, THE LICENSOR ON THE TERMS SET OUT IN THIS AGREEMENT. IF YOU DO NOT AGREE TO THESE TERMS, YOU MUST NOT USE THE SOFTWARE. IF YOU ARE RECEIVING THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU HAVE THE ACTUAL AUTHORITY TO AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT ON BEHALF OF SUCH ENTITY.
* "Licensee" means you, an individual, or the entity on whose behalf you receive the Software.
*
* Permission is hereby granted, free of charge, to the Licensee obtaining a copy of this Software and associated documentation files to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject in each case to the following conditions:
* Condition 1: If the Licensee distributes the Software or any derivative works of the Software, the Licensee must attach this Agreement.
* Condition 2: Without limiting other conditions in this Agreement, the grant of rights is solely for non-production use as defined below.
* "Non-production use" means any use of the Software that is not directly related to creating products, services, or systems that generate revenue or other direct or indirect economic benefits. Examples of permitted non-production use include personal use, educational use, research, and development. Examples of prohibited production use include, without limitation, use for commercial, for-profit, or publicly accessible systems or use for commercial or revenue-generating purposes.
*
* If the Licensee is in breach of the Conditions, this Agreement, including the rights granted under it, will automatically terminate with immediate effect.
*
* SUBJECT AS SET OUT BELOW, THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* NOTHING IN THIS AGREEMENT EXCLUDES OR RESTRICTS A PARTY’S LIABILITY FOR (A) DEATH OR PERSONAL INJURY CAUSED BY THAT PARTY’S NEGLIGENCE, (B) FRAUD, OR (C) ANY OTHER LIABILITY TO THE EXTENT THAT IT CANNOT BE LAWFULLY EXCLUDED OR RESTRICTED.
*/

import {C3UserConfigurationProvider} from '@camunda/camunda-composite-components';
import {C3ThemePersister} from 'C3ThemePersister';
import {api} from 'modules/api';
import {getStage} from 'modules/utils/getStage';
import {useEffect, useState} from 'react';

const IS_SAAS = typeof window.clientConfig?.organizationId === 'string';
const STAGE = getStage(window.location.host);

async function fetchToken() {
try {
const response = await fetch(api.getSaasUserToken());

if (!response.ok) {
console.error('Failed to fetch user token', response);
return '';
}

const token = await response.json();
return token;
} catch (error) {
console.error('Failed to fetch user token', error);
return '';
}
}

type Props = {
children: React.ReactNode;
};

const C3Provider: React.FC<Props> = ({children}) => {
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
async function init() {
if (IS_SAAS) {
setToken(await fetchToken());
}
}

init();
}, []);

if (token === null) {
return children;
}

return (
<C3UserConfigurationProvider
activeOrganizationId={window.clientConfig?.organizationId ?? ''}
userToken={token}
getNewUserToken={fetchToken}
currentClusterUuid={window.clientConfig?.clusterId ?? ''}
currentApp="tasklist"
stage={STAGE === 'unknown' ? 'dev' : STAGE}
handleTheme
>
<C3ThemePersister />
{children}
</C3UserConfigurationProvider>
);
};

export {C3Provider};
32 changes: 32 additions & 0 deletions tasklist/client/src/C3ThemePersister.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Camunda Services GmbH
*
* BY INSTALLING, DOWNLOADING, ACCESSING, USING, OR DISTRIBUTING THE SOFTWARE ("USE"), YOU INDICATE YOUR ACCEPTANCE TO AND ARE ENTERING INTO A CONTRACT WITH, THE LICENSOR ON THE TERMS SET OUT IN THIS AGREEMENT. IF YOU DO NOT AGREE TO THESE TERMS, YOU MUST NOT USE THE SOFTWARE. IF YOU ARE RECEIVING THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU HAVE THE ACTUAL AUTHORITY TO AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT ON BEHALF OF SUCH ENTITY.
* "Licensee" means you, an individual, or the entity on whose behalf you receive the Software.
*
* Permission is hereby granted, free of charge, to the Licensee obtaining a copy of this Software and associated documentation files to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject in each case to the following conditions:
* Condition 1: If the Licensee distributes the Software or any derivative works of the Software, the Licensee must attach this Agreement.
* Condition 2: Without limiting other conditions in this Agreement, the grant of rights is solely for non-production use as defined below.
* "Non-production use" means any use of the Software that is not directly related to creating products, services, or systems that generate revenue or other direct or indirect economic benefits. Examples of permitted non-production use include personal use, educational use, research, and development. Examples of prohibited production use include, without limitation, use for commercial, for-profit, or publicly accessible systems or use for commercial or revenue-generating purposes.
*
* If the Licensee is in breach of the Conditions, this Agreement, including the rights granted under it, will automatically terminate with immediate effect.
*
* SUBJECT AS SET OUT BELOW, THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* NOTHING IN THIS AGREEMENT EXCLUDES OR RESTRICTS A PARTY’S LIABILITY FOR (A) DEATH OR PERSONAL INJURY CAUSED BY THAT PARTY’S NEGLIGENCE, (B) FRAUD, OR (C) ANY OTHER LIABILITY TO THE EXTENT THAT IT CANNOT BE LAWFULLY EXCLUDED OR RESTRICTED.
*/

import {useC3Profile} from '@camunda/camunda-composite-components';
import {themeStore} from 'modules/stores/theme';
import {useEffect} from 'react';

const C3ThemePersister: React.FC = () => {
const {theme} = useC3Profile();

useEffect(() => {
themeStore.changeTheme(theme);
}, [theme]);

return null;
};

export {C3ThemePersister};
59 changes: 13 additions & 46 deletions tasklist/client/src/Layout/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
*/

import {useEffect, useState} from 'react';
import capitalize from 'lodash/capitalize';
import {observer} from 'mobx-react-lite';
import {Link as RouterLink, matchPath, useLocation} from 'react-router-dom';
import {Link} from '@carbon/react';
Expand All @@ -31,14 +30,6 @@ import {useCurrentUser} from 'modules/queries/useCurrentUser';
import {getStateLocally} from 'modules/utils/localStorage';
import styles from './styles.module.scss';

const orderedApps = [
'console',
'modeler',
'tasklist',
'operate',
'optimize',
] as const;

function getInfoSidebarItems(isPaidPlan: boolean) {
const BASE_INFO_SIDEBAR_ITEMS = [
{
Expand Down Expand Up @@ -100,10 +91,6 @@ function getInfoSidebarItems(isPaidPlan: boolean) {
: [...BASE_INFO_SIDEBAR_ITEMS, COMMUNITY_FORUM_ITEM];
}

type AppSwitcherElementType = NonNullable<
React.ComponentProps<typeof C3Navigation>['appBar']['elements']
>[number];

const Header: React.FC = observer(() => {
const IS_SAAS = typeof window.clientConfig?.organizationId === 'string';
const IS_ENTERPRISE = window.clientConfig?.isEnterprise === true;
Expand All @@ -112,31 +99,10 @@ const Header: React.FC = observer(() => {
matchPath(pages.processes(), location.pathname) !== null;
const {data: currentUser} = useCurrentUser();
const {selectedTheme, changeTheme} = themeStore;
const {displayName, salesPlanType, c8Links} = currentUser ?? {
const {displayName, salesPlanType} = currentUser ?? {
displayName: null,
salesPlanType: null,
c8Links: [],
};
const parsedC8Links = c8Links
.filter(({name}) => orderedApps.includes(name))
.reduce((acc, {name, link}) => ({...acc, [name]: link}), {}) as Record<
(typeof orderedApps)[number],
string
>;
const switcherElements = orderedApps
.map<AppSwitcherElementType | undefined>((appName) =>
parsedC8Links[appName] === undefined
? undefined
: {
key: appName,
label: capitalize(appName),
href: parsedC8Links[appName],
active: appName === 'tasklist',
routeProps:
appName === 'tasklist' ? {to: pages.initial} : undefined,
},
)
.filter((entry): entry is AppSwitcherElementType => entry !== undefined);

const [isTermsConditionModalOpen, setTermsConditionModalOpen] =
useState(false);
Expand All @@ -150,6 +116,18 @@ const Header: React.FC = observer(() => {
return (
<>
<C3Navigation
notificationSideBar={IS_SAAS ? {} : undefined}
appBar={{
ariaLabel: 'App Panel',
isOpen: false,
elementClicked: (app: string) => {
tracking.track({
eventName: 'app-switcher-item-clicked',
app,
});
},
appTeaserRouteProps: IS_SAAS ? {} : undefined,
}}
app={{
ariaLabel: 'Camunda Tasklist',
name: 'Tasklist',
Expand Down Expand Up @@ -237,17 +215,6 @@ const Header: React.FC = observer(() => {
},
],
}}
appBar={{
ariaLabel: 'App Panel',
isOpen: false,
elements: IS_SAAS ? switcherElements : [],
elementClicked: (app: string) => {
tracking.track({
eventName: 'app-switcher-item-clicked',
app,
});
},
}}
infoSideBar={{
isOpen: false,
ariaLabel: 'Info',
Expand Down
53 changes: 0 additions & 53 deletions tasklist/client/src/Layout/Header/tests/appSwitcher.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,59 +28,6 @@ describe('App switcher', () => {
window.clientConfig = DEFAULT_MOCK_CLIENT_CONFIG;
});

it('should render with correct links', async () => {
window.clientConfig = {
...window.clientConfig,
isEnterprise: false,
organizationId: 'some-organization-id',
};

nodeMockServer.use(
http.get(
'/v1/internal/users/current',
() => {
return HttpResponse.json(userMocks.currentUserWithC8Links);
},
{
once: true,
},
),
);

const {user} = render(<Header />, {
wrapper: getWrapper(),
});

expect(await screen.findByText('Console')).toBeInTheDocument();

await user.click(
await screen.findByRole('button', {
name: /camunda components/i,
}),
);

expect(await screen.findByRole('link', {name: 'Console'})).toHaveAttribute(
'href',
'https://link-to-console',
);
expect(screen.getByRole('link', {name: 'Modeler'})).toHaveAttribute(
'href',
'https://link-to-modeler',
);
expect(screen.getByRole('link', {name: 'Operate'})).toHaveAttribute(
'href',
'https://link-to-operate',
);
expect(screen.getByRole('link', {name: 'Tasklist'})).toHaveAttribute(
'href',
'/',
);
expect(screen.getByRole('link', {name: 'Optimize'})).toHaveAttribute(
'href',
'https://link-to-optimize',
);
});

it('should not render links for CCSM', async () => {
window.clientConfig = {
...window.clientConfig,
Expand Down
8 changes: 6 additions & 2 deletions tasklist/client/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@
)
);

.cds--g10 {
:root {
@include theme.theme(themes.$g10);
}

.cds--g100 {
[data-carbon-theme='g10'] {
@include theme.theme(themes.$g10);
}

[data-carbon-theme='g100'] {
@include theme.theme(themes.$g100);
}

Expand Down
9 changes: 9 additions & 0 deletions tasklist/client/src/modules/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ const api = {
},
);
},
getSaasUserToken: () => {
return new Request(getFullURL('/v1/internal/users/token'), {
...BASE_REQUEST_OPTIONS,
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
},
} as const;

export {api};
8 changes: 1 addition & 7 deletions tasklist/client/src/modules/theme/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,7 @@ const ThemeProvider: React.FC<Props> = observer(({children}) => {
const prefix = usePrefix();

useLayoutEffect(() => {
const body = document.body;

Object.values(THEME_TOKENS).forEach((theme) => {
body.classList.remove(`${prefix}--${theme}`);
});

body.classList.add(`${prefix}--${THEME_TOKENS[actualTheme]}`);
document.documentElement.dataset.carbonTheme = THEME_TOKENS[actualTheme];
}, [actualTheme, prefix]);

return (
Expand Down
2 changes: 1 addition & 1 deletion tasklist/client/src/modules/tracking/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import {Mixpanel} from 'mixpanel-browser';
import {getStage} from './getStage';
import {getStage} from 'modules/utils/getStage';
import {CurrentUser} from 'modules/types';
import {TaskFilters} from 'modules/hooks/useTaskFilters';
import Hotjar from '@hotjar/browser';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.security.core.Authentication;
Expand Down Expand Up @@ -103,7 +104,9 @@ public List<UserDTO> getUsersByUsernames(List<String> usernames) {
@Override
public Optional<String> getUserToken(final Authentication authentication) {
if (authentication instanceof TokenAuthentication) {
return Optional.of(((TokenAuthentication) authentication).getNewTokenByRefreshToken());
return Optional.of(
JSONObject.valueToString(
((TokenAuthentication) authentication).getNewTokenByRefreshToken()));
} else {
throw new UnsupportedOperationException(
"Not supported for token class: " + authentication.getClass().getName());
Expand Down

0 comments on commit 3f884eb

Please sign in to comment.