mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02:00
chore: adapt integrations layout for incoming webhooks (#5828)
https://linear.app/unleash/issue/2-1823/adapt-integrations-page-to-incoming-webhooks-tab-layout Adapts the current integrations page to the incoming webhooks feature, which includes things like: - Displaying both configured and available integrations in a single "page block" - Implement tabs - Add "Incoming Webhooks" integration card - Adapt the existing `IntegrationCard` component to support `onClick` This also includes a small girl scouting fix: Some tabs (like on the roles page) did not correctly reflect the active tab. ### `incomingWebhooks` disabled  ### `incomingWebhooks` enabled Notice the new "Incoming webhooks" tab and integration card. 
This commit is contained in:
parent
336eab9c5a
commit
10c3acd27d
@ -15,6 +15,7 @@ import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|||||||
import { Add } from '@mui/icons-material';
|
import { Add } from '@mui/icons-material';
|
||||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||||
import { IRole } from 'interfaces/role';
|
import { IRole } from 'interfaces/role';
|
||||||
|
import { TabLink } from 'component/common/TabNav/TabLink';
|
||||||
|
|
||||||
const StyledHeader = styled('div')(() => ({
|
const StyledHeader = styled('div')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -91,11 +92,9 @@ export const RolesPage = () => {
|
|||||||
key={label}
|
key={label}
|
||||||
value={path}
|
value={path}
|
||||||
label={
|
label={
|
||||||
<CenteredNavLink to={path}>
|
<TabLink to={path}>
|
||||||
<span>
|
{label} ({total})
|
||||||
{label} ({total})
|
</TabLink>
|
||||||
</span>
|
|
||||||
</CenteredNavLink>
|
|
||||||
}
|
}
|
||||||
sx={{ padding: 0 }}
|
sx={{ padding: 0 }}
|
||||||
/>
|
/>
|
||||||
|
25
frontend/src/component/common/TabNav/TabLink.tsx
Normal file
25
frontend/src/component/common/TabNav/TabLink.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const StyledTabLink = styled(Link)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
padding: theme.spacing(0, 5),
|
||||||
|
'&.active': {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface ICenteredTabLinkProps {
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabLink: FC<ICenteredTabLinkProps> = ({ to, children }) => (
|
||||||
|
<StyledTabLink to={to}>{children}</StyledTabLink>
|
||||||
|
);
|
@ -1,17 +1,17 @@
|
|||||||
import { type VFC } from 'react';
|
import { type VFC } from 'react';
|
||||||
import { Box, Typography, styled } from '@mui/material';
|
import { Box, Typography, styled } from '@mui/material';
|
||||||
import type { AddonTypeSchema } from 'openapi';
|
import type { AddonTypeSchema } from 'openapi';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import { IntegrationCard } from '../IntegrationCard/IntegrationCard';
|
import { IntegrationCard } from '../IntegrationCard/IntegrationCard';
|
||||||
import { JIRA_INFO } from '../../ViewIntegration/JiraIntegration/JiraIntegration';
|
import { JIRA_INFO } from '../../ViewIntegration/JiraIntegration/JiraIntegration';
|
||||||
import { StyledCardsGrid } from '../IntegrationList.styles';
|
import { StyledCardsGrid } from '../IntegrationList.styles';
|
||||||
import { RequestIntegrationCard } from '../RequestIntegrationCard/RequestIntegrationCard';
|
import { RequestIntegrationCard } from '../RequestIntegrationCard/RequestIntegrationCard';
|
||||||
import { OFFICIAL_SDKS } from './SDKs';
|
import { OFFICIAL_SDKS } from './SDKs';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
interface IAvailableIntegrationsProps {
|
interface IAvailableIntegrationsProps {
|
||||||
providers: AddonTypeSchema[];
|
providers: AddonTypeSchema[];
|
||||||
loading?: boolean;
|
onNewIncomingWebhook: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
@ -53,219 +53,228 @@ const StyledGrayContainer = styled('div')(({ theme }) => ({
|
|||||||
|
|
||||||
export const AvailableIntegrations: VFC<IAvailableIntegrationsProps> = ({
|
export const AvailableIntegrations: VFC<IAvailableIntegrationsProps> = ({
|
||||||
providers,
|
providers,
|
||||||
loading,
|
onNewIncomingWebhook,
|
||||||
}) => {
|
}) => {
|
||||||
|
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks');
|
||||||
|
|
||||||
const customProviders = [JIRA_INFO];
|
const customProviders = [JIRA_INFO];
|
||||||
const serverSdks = OFFICIAL_SDKS.filter((sdk) => sdk.type === 'server');
|
const serverSdks = OFFICIAL_SDKS.filter((sdk) => sdk.type === 'server');
|
||||||
const clientSdks = OFFICIAL_SDKS.filter((sdk) => sdk.type === 'client');
|
const clientSdks = OFFICIAL_SDKS.filter((sdk) => sdk.type === 'client');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<StyledContainer>
|
||||||
header={<PageHeader title='Integrations' secondary />}
|
<StyledSection>
|
||||||
isLoading={loading}
|
<div>
|
||||||
>
|
<Typography component='h3' variant='h2'>
|
||||||
<StyledContainer>
|
Unleash crafted
|
||||||
<StyledSection>
|
</Typography>
|
||||||
<StyledCardsGrid>
|
<Typography variant='body2' color='text.secondary'>
|
||||||
{providers
|
Unleash is built to be extended. We have crafted
|
||||||
?.sort(
|
integrations to make it easier for you to get started.
|
||||||
(a, b) =>
|
</Typography>
|
||||||
a.displayName?.localeCompare(
|
</div>
|
||||||
b.displayName,
|
<StyledCardsGrid>
|
||||||
) || 0,
|
{providers
|
||||||
)
|
?.sort(
|
||||||
.map(
|
(a, b) =>
|
||||||
|
a.displayName?.localeCompare(b.displayName) ||
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
deprecated,
|
||||||
|
}) => (
|
||||||
|
<IntegrationCard
|
||||||
|
key={name}
|
||||||
|
icon={name}
|
||||||
|
title={displayName || name}
|
||||||
|
description={description}
|
||||||
|
link={`/integrations/create/${name}`}
|
||||||
|
deprecated={deprecated}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={incomingWebhooksEnabled}
|
||||||
|
show={
|
||||||
|
<IntegrationCard
|
||||||
|
icon='webhook'
|
||||||
|
title='Incoming Webhooks'
|
||||||
|
description='Incoming Webhooks allow third party services to send observable events to Unleash.'
|
||||||
|
onClick={onNewIncomingWebhook}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* TODO: sort providers from backend with custom providers */}
|
||||||
|
{customProviders?.map(
|
||||||
|
({ name, displayName, description }) => (
|
||||||
|
<IntegrationCard
|
||||||
|
key={name}
|
||||||
|
icon={name}
|
||||||
|
title={displayName || name}
|
||||||
|
description={description}
|
||||||
|
link={`/integrations/view/${name}`}
|
||||||
|
configureActionText='Learn more'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<RequestIntegrationCard />
|
||||||
|
</StyledCardsGrid>
|
||||||
|
</StyledSection>
|
||||||
|
<StyledSection>
|
||||||
|
<div>
|
||||||
|
<Typography component='h3' variant='h2'>
|
||||||
|
Performance and security
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Connect Unleash to private, scalable, and distributed
|
||||||
|
relays.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<StyledCardsGrid>
|
||||||
|
<IntegrationCard
|
||||||
|
icon='unleash'
|
||||||
|
title='Unleash Edge'
|
||||||
|
description="Unleash Edge is built to help you scale Unleash. As a successor of Unleash Proxy it's even faster and more versatile."
|
||||||
|
link='/integrations/view/edge'
|
||||||
|
configureActionText='Learn more'
|
||||||
|
/>
|
||||||
|
<IntegrationCard
|
||||||
|
icon='unleash'
|
||||||
|
title='Unleash Proxy'
|
||||||
|
description='The Unleash Proxy is a lightweight, stateless proxy that sits between your Unleash client SDKs and the Unleash API.'
|
||||||
|
link='https://docs.getunleash.io/reference/unleash-proxy'
|
||||||
|
configureActionText='View documentation'
|
||||||
|
deprecated='Try Unleash Edge instead. It has all the features of Unleash Proxy and more.'
|
||||||
|
isExternal
|
||||||
|
/>
|
||||||
|
</StyledCardsGrid>
|
||||||
|
</StyledSection>
|
||||||
|
<StyledSection>
|
||||||
|
<div>
|
||||||
|
<Typography component='h3' variant='h2'>
|
||||||
|
Official SDKs
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
In order to connect your application to Unleash you will
|
||||||
|
need a client SDK (software developer kit) for your
|
||||||
|
programming language and an{' '}
|
||||||
|
<a
|
||||||
|
href='https://docs.getunleash.io/how-to/how-to-create-api-tokens'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
API token
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<StyledSdksSection>
|
||||||
|
<StyledSdksGroup>
|
||||||
|
<Box>
|
||||||
|
<Typography component='h4' variant='h4'>
|
||||||
|
Server-side SDKs
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Server-side clients run on your server and
|
||||||
|
communicate directly with your Unleash instance.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<StyledCardsGrid small>
|
||||||
|
{serverSdks?.map(
|
||||||
({
|
({
|
||||||
name,
|
name,
|
||||||
displayName,
|
displayName,
|
||||||
description,
|
description,
|
||||||
deprecated,
|
documentationUrl,
|
||||||
}) => (
|
}) => (
|
||||||
<IntegrationCard
|
<IntegrationCard
|
||||||
key={name}
|
key={name}
|
||||||
icon={name}
|
icon={name}
|
||||||
title={displayName || name}
|
title={displayName || name}
|
||||||
description={description}
|
description={description}
|
||||||
link={`/integrations/create/${name}`}
|
link={documentationUrl}
|
||||||
deprecated={deprecated}
|
configureActionText={
|
||||||
|
'View documentation'
|
||||||
|
}
|
||||||
|
isExternal
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
{/* TODO: sort providers from backend with custom providers */}
|
</StyledCardsGrid>
|
||||||
{customProviders?.map(
|
</StyledSdksGroup>
|
||||||
({ name, displayName, description }) => (
|
<StyledSdksGroup>
|
||||||
<IntegrationCard
|
<Box>
|
||||||
key={name}
|
<Typography component='h4' variant='h4'>
|
||||||
icon={name}
|
Client-side SDKs
|
||||||
title={displayName || name}
|
</Typography>
|
||||||
description={description}
|
<Typography variant='body2' color='text.secondary'>
|
||||||
link={`/integrations/view/${name}`}
|
Client-side SDKs can connect to the{' '}
|
||||||
configureActionText='Learn more'
|
<a
|
||||||
/>
|
href='https://docs.getunleash.io/reference/unleash-edge'
|
||||||
),
|
target='_blank'
|
||||||
)}
|
rel='noopener noreferrer'
|
||||||
<RequestIntegrationCard />
|
|
||||||
</StyledCardsGrid>
|
|
||||||
</StyledSection>
|
|
||||||
<StyledSection>
|
|
||||||
<div>
|
|
||||||
<Typography component='h3' variant='h2'>
|
|
||||||
Performance and security
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
Connect Unleash to private, scalable, and
|
|
||||||
distributed relays.
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<StyledCardsGrid>
|
|
||||||
<IntegrationCard
|
|
||||||
icon='unleash'
|
|
||||||
title='Unleash Edge'
|
|
||||||
description="Unleash Edge is built to help you scale Unleash. As a successor of Unleash Proxy it's even faster and more versatile."
|
|
||||||
link='/integrations/view/edge'
|
|
||||||
configureActionText='Learn more'
|
|
||||||
/>
|
|
||||||
<IntegrationCard
|
|
||||||
icon='unleash'
|
|
||||||
title='Unleash Proxy'
|
|
||||||
description='The Unleash Proxy is a lightweight, stateless proxy that sits between your Unleash client SDKs and the Unleash API.'
|
|
||||||
link='https://docs.getunleash.io/reference/unleash-proxy'
|
|
||||||
configureActionText='View documentation'
|
|
||||||
deprecated='Try Unleash Edge instead. It has all the features of Unleash Proxy and more.'
|
|
||||||
isExternal
|
|
||||||
/>
|
|
||||||
</StyledCardsGrid>
|
|
||||||
</StyledSection>
|
|
||||||
<StyledSection>
|
|
||||||
<div>
|
|
||||||
<Typography component='h3' variant='h2'>
|
|
||||||
Official SDKs
|
|
||||||
</Typography>
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
In order to connect your application to Unleash you
|
|
||||||
will need a client SDK (software developer kit) for
|
|
||||||
your programming language and an{' '}
|
|
||||||
<a
|
|
||||||
href='https://docs.getunleash.io/how-to/how-to-create-api-tokens'
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
API token
|
|
||||||
</a>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<StyledSdksSection>
|
|
||||||
<StyledSdksGroup>
|
|
||||||
<Box>
|
|
||||||
<Typography component='h4' variant='h4'>
|
|
||||||
Server-side SDKs
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant='body2'
|
|
||||||
color='text.secondary'
|
|
||||||
>
|
>
|
||||||
Server-side clients run on your server and
|
Unleash Edge
|
||||||
communicate directly with your Unleash
|
</a>{' '}
|
||||||
instance.
|
or to the{' '}
|
||||||
</Typography>
|
<a
|
||||||
</Box>
|
href='https://docs.getunleash.io/reference/front-end-api'
|
||||||
<StyledCardsGrid small>
|
target='_blank'
|
||||||
{serverSdks?.map(
|
rel='noopener noreferrer'
|
||||||
({
|
|
||||||
name,
|
|
||||||
displayName,
|
|
||||||
description,
|
|
||||||
documentationUrl,
|
|
||||||
}) => (
|
|
||||||
<IntegrationCard
|
|
||||||
key={name}
|
|
||||||
icon={name}
|
|
||||||
title={displayName || name}
|
|
||||||
description={description}
|
|
||||||
link={documentationUrl}
|
|
||||||
configureActionText={
|
|
||||||
'View documentation'
|
|
||||||
}
|
|
||||||
isExternal
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</StyledCardsGrid>
|
|
||||||
</StyledSdksGroup>
|
|
||||||
<StyledSdksGroup>
|
|
||||||
<Box>
|
|
||||||
<Typography component='h4' variant='h4'>
|
|
||||||
Client-side SDKs
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant='body2'
|
|
||||||
color='text.secondary'
|
|
||||||
>
|
>
|
||||||
Client-side SDKs can connect to the{' '}
|
Unleash front-end API
|
||||||
|
</a>
|
||||||
|
, but not to the regular Unleash client API.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<StyledCardsGrid small>
|
||||||
|
{clientSdks?.map(
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
documentationUrl,
|
||||||
|
}) => (
|
||||||
|
<IntegrationCard
|
||||||
|
key={name}
|
||||||
|
icon={name}
|
||||||
|
title={displayName || name}
|
||||||
|
description={description}
|
||||||
|
link={documentationUrl}
|
||||||
|
configureActionText={
|
||||||
|
'View documentation'
|
||||||
|
}
|
||||||
|
isExternal
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</StyledCardsGrid>
|
||||||
|
</StyledSdksGroup>
|
||||||
|
<StyledSdksGroup>
|
||||||
|
<StyledGrayContainer>
|
||||||
|
<div>
|
||||||
|
<Typography component='h4' variant='h3'>
|
||||||
|
Community SDKs
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
<a
|
<a
|
||||||
href='https://docs.getunleash.io/reference/unleash-edge'
|
href='https://docs.getunleash.io/reference/sdks#community-sdks'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
Unleash Edge
|
Here's some of the fantastic work
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
or to the{' '}
|
our community has built to make Unleash work
|
||||||
<a
|
in even more contexts.
|
||||||
href='https://docs.getunleash.io/reference/front-end-api'
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
Unleash front-end API
|
|
||||||
</a>
|
|
||||||
, but not to the regular Unleash client API.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</div>
|
||||||
<StyledCardsGrid small>
|
</StyledGrayContainer>
|
||||||
{clientSdks?.map(
|
</StyledSdksGroup>
|
||||||
({
|
</StyledSdksSection>
|
||||||
name,
|
</StyledSection>
|
||||||
displayName,
|
</StyledContainer>
|
||||||
description,
|
|
||||||
documentationUrl,
|
|
||||||
}) => (
|
|
||||||
<IntegrationCard
|
|
||||||
key={name}
|
|
||||||
icon={name}
|
|
||||||
title={displayName || name}
|
|
||||||
description={description}
|
|
||||||
link={documentationUrl}
|
|
||||||
configureActionText={
|
|
||||||
'View documentation'
|
|
||||||
}
|
|
||||||
isExternal
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</StyledCardsGrid>
|
|
||||||
</StyledSdksGroup>
|
|
||||||
<StyledSdksGroup>
|
|
||||||
<StyledGrayContainer>
|
|
||||||
<div>
|
|
||||||
<Typography component='h4' variant='h3'>
|
|
||||||
Community SDKs
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
<a
|
|
||||||
href='https://docs.getunleash.io/reference/sdks#community-sdks'
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
Here's some of the fantastic work
|
|
||||||
</a>{' '}
|
|
||||||
our community has built to make Unleash
|
|
||||||
work in even more contexts.
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</StyledGrayContainer>
|
|
||||||
</StyledSdksGroup>
|
|
||||||
</StyledSdksSection>
|
|
||||||
</StyledSection>
|
|
||||||
</StyledContainer>
|
|
||||||
</PageContent>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { AddonSchema, AddonTypeSchema } from 'openapi';
|
import { AddonSchema, AddonTypeSchema } from 'openapi';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import { StyledCardsGrid } from '../IntegrationList.styles';
|
import { StyledCardsGrid } from '../IntegrationList.styles';
|
||||||
import { IntegrationCard } from '../IntegrationCard/IntegrationCard';
|
import { IntegrationCard } from '../IntegrationCard/IntegrationCard';
|
||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
|
import { Typography, styled } from '@mui/material';
|
||||||
|
|
||||||
|
const StyledConfiguredSection = styled('section')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
marginBottom: theme.spacing(8),
|
||||||
|
}));
|
||||||
|
|
||||||
type ConfiguredIntegrationsProps = {
|
type ConfiguredIntegrationsProps = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@ -17,15 +23,19 @@ export const ConfiguredIntegrations: VFC<ConfiguredIntegrationsProps> = ({
|
|||||||
addons,
|
addons,
|
||||||
providers,
|
providers,
|
||||||
}) => {
|
}) => {
|
||||||
const counter = addons.length ? `(${addons.length})` : '';
|
|
||||||
const ref = useLoading(loading || false);
|
const ref = useLoading(loading || false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<StyledConfiguredSection>
|
||||||
header={<PageHeader title={`Configured integrations ${counter}`} />}
|
<div>
|
||||||
sx={(theme) => ({ marginBottom: theme.spacing(2) })}
|
<Typography component='h3' variant='h2'>
|
||||||
isLoading={loading}
|
Configured integrations
|
||||||
>
|
</Typography>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
These are the integrations that are currently configured for
|
||||||
|
your Unleash instance.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
<StyledCardsGrid ref={ref}>
|
<StyledCardsGrid ref={ref}>
|
||||||
{addons
|
{addons
|
||||||
?.sort(({ id: a }, { id: b }) => a - b)
|
?.sort(({ id: a }, { id: b }) => a - b)
|
||||||
@ -56,6 +66,6 @@ export const ConfiguredIntegrations: VFC<ConfiguredIntegrationsProps> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</StyledCardsGrid>
|
</StyledCardsGrid>
|
||||||
</PageContent>
|
</StyledConfiguredSection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import { styled, Tooltip, Typography } from '@mui/material';
|
import { Link, styled, Tooltip, Typography } from '@mui/material';
|
||||||
import { IntegrationIcon } from '../IntegrationIcon/IntegrationIcon';
|
import { IntegrationIcon } from '../IntegrationIcon/IntegrationIcon';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
@ -10,46 +10,56 @@ import type { AddonSchema } from 'openapi';
|
|||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
|
||||||
interface IIntegrationCardProps {
|
interface IIntegrationCardBaseProps {
|
||||||
id?: string | number;
|
id?: string | number;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
configureActionText?: string;
|
configureActionText?: string;
|
||||||
link: string;
|
|
||||||
isExternal?: boolean;
|
|
||||||
addon?: AddonSchema;
|
addon?: AddonSchema;
|
||||||
deprecated?: string;
|
deprecated?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledLink = styled(Link)(({ theme }) => ({
|
interface IIntegrationCardWithLinkProps extends IIntegrationCardBaseProps {
|
||||||
|
link: string;
|
||||||
|
isExternal?: boolean;
|
||||||
|
onClick?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IIntegrationCardWithOnClickProps extends IIntegrationCardBaseProps {
|
||||||
|
link?: never;
|
||||||
|
isExternal?: never;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IIntegrationCardProps =
|
||||||
|
| IIntegrationCardWithLinkProps
|
||||||
|
| IIntegrationCardWithOnClickProps;
|
||||||
|
|
||||||
|
const StyledCard = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: theme.spacing(3),
|
padding: theme.spacing(3),
|
||||||
|
height: '100%',
|
||||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
textDecoration: 'none',
|
|
||||||
color: 'inherit',
|
|
||||||
boxShadow: theme.boxShadows.card,
|
boxShadow: theme.boxShadows.card,
|
||||||
':hover': {
|
':hover': {
|
||||||
backgroundColor: theme.palette.action.hover,
|
backgroundColor: theme.palette.action.hover,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledAnchor = styled('a')(({ theme }) => ({
|
const StyledLink = styled(Link)({
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
padding: theme.spacing(3),
|
|
||||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
boxShadow: theme.boxShadows.card,
|
textAlign: 'left',
|
||||||
':hover': {
|
}) as typeof Link;
|
||||||
backgroundColor: theme.palette.action.hover,
|
|
||||||
},
|
const StyledRouterLink = styled(RouterLink)({
|
||||||
}));
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
const StyledHeader = styled('div')(({ theme }) => ({
|
const StyledHeader = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -85,6 +95,7 @@ export const IntegrationCard: VFC<IIntegrationCardProps> = ({
|
|||||||
isEnabled,
|
isEnabled,
|
||||||
configureActionText = 'Configure',
|
configureActionText = 'Configure',
|
||||||
link,
|
link,
|
||||||
|
onClick,
|
||||||
addon,
|
addon,
|
||||||
deprecated,
|
deprecated,
|
||||||
isExternal = false,
|
isExternal = false,
|
||||||
@ -102,7 +113,7 @@ export const IntegrationCard: VFC<IIntegrationCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<StyledCard>
|
||||||
<StyledHeader>
|
<StyledHeader>
|
||||||
<StyledTitle variant='h3' data-loading>
|
<StyledTitle variant='h3' data-loading>
|
||||||
<IntegrationIcon name={icon as string} /> {title}
|
<IntegrationIcon name={icon as string} /> {title}
|
||||||
@ -143,25 +154,37 @@ export const IntegrationCard: VFC<IIntegrationCardProps> = ({
|
|||||||
elseShow={<ChevronRightIcon />}
|
elseShow={<ChevronRightIcon />}
|
||||||
/>
|
/>
|
||||||
</StyledAction>
|
</StyledAction>
|
||||||
</>
|
</StyledCard>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isExternal) {
|
if (onClick) {
|
||||||
return (
|
return (
|
||||||
<StyledAnchor
|
<StyledLink
|
||||||
|
component='button'
|
||||||
|
onClick={() => {
|
||||||
|
handleClick();
|
||||||
|
onClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</StyledLink>
|
||||||
|
);
|
||||||
|
} else if (isExternal) {
|
||||||
|
return (
|
||||||
|
<StyledLink
|
||||||
href={link}
|
href={link}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</StyledAnchor>
|
</StyledLink>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<StyledLink to={link} onClick={handleClick}>
|
<StyledRouterLink to={link} onClick={handleClick}>
|
||||||
{content}
|
{content}
|
||||||
</StyledLink>
|
</StyledRouterLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,38 +1,143 @@
|
|||||||
import { VFC } from 'react';
|
import { VFC } from 'react';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import useAddons from 'hooks/api/getters/useAddons/useAddons';
|
import useAddons from 'hooks/api/getters/useAddons/useAddons';
|
||||||
import { AvailableIntegrations } from './AvailableIntegrations/AvailableIntegrations';
|
import { AvailableIntegrations } from './AvailableIntegrations/AvailableIntegrations';
|
||||||
import { ConfiguredIntegrations } from './ConfiguredIntegrations/ConfiguredIntegrations';
|
import { ConfiguredIntegrations } from './ConfiguredIntegrations/ConfiguredIntegrations';
|
||||||
import { AddonSchema } from 'openapi';
|
import { Tab, Tabs, styled, useTheme } from '@mui/material';
|
||||||
|
import { Add } from '@mui/icons-material';
|
||||||
|
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { useIncomingWebhooks } from 'hooks/api/getters/useIncomingWebhooks/useIncomingWebhooks';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { TabLink } from 'component/common/TabNav/TabLink';
|
||||||
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||||
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
|
|
||||||
|
const StyledHeader = styled('div')(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledTabsContainer = styled('div')({
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledActions = styled('div')({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
export const IntegrationList: VFC = () => {
|
export const IntegrationList: VFC = () => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const theme = useTheme();
|
||||||
|
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks');
|
||||||
const { providers, addons, loading } = useAddons();
|
const { providers, addons, loading } = useAddons();
|
||||||
|
const { incomingWebhooks } = useIncomingWebhooks();
|
||||||
|
|
||||||
const loadingPlaceholderAddons: AddonSchema[] = Array.from({ length: 4 })
|
const onNewIncomingWebhook = () => {
|
||||||
.fill({})
|
navigate('/integrations/incoming-webhooks');
|
||||||
.map((_, id) => ({
|
// TODO: Implement:
|
||||||
id,
|
// setSelectedIncomingWebhook(undefined);
|
||||||
provider: 'mock',
|
// setIncomingWebhookModalOpen(true);
|
||||||
description: 'mock integratino',
|
};
|
||||||
events: [],
|
|
||||||
projects: [],
|
const tabs = [
|
||||||
parameters: {},
|
{
|
||||||
enabled: false,
|
label: 'Integrations',
|
||||||
}));
|
path: '/integrations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Incoming webhooks (${incomingWebhooks.length})`,
|
||||||
|
path: '/integrations/incoming-webhooks',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageContent
|
||||||
<ConditionallyRender
|
header={
|
||||||
condition={addons.length > 0}
|
<ConditionallyRender
|
||||||
show={
|
condition={incomingWebhooksEnabled}
|
||||||
<ConfiguredIntegrations
|
show={
|
||||||
addons={loading ? loadingPlaceholderAddons : addons}
|
<StyledHeader>
|
||||||
providers={providers}
|
<StyledTabsContainer>
|
||||||
loading={loading}
|
<Tabs
|
||||||
/>
|
value={pathname}
|
||||||
}
|
indicatorColor='primary'
|
||||||
/>
|
textColor='primary'
|
||||||
<AvailableIntegrations providers={providers} loading={loading} />
|
variant='scrollable'
|
||||||
</>
|
allowScrollButtonsMobile
|
||||||
|
>
|
||||||
|
{tabs.map(({ label, path }) => (
|
||||||
|
<Tab
|
||||||
|
key={label}
|
||||||
|
value={path}
|
||||||
|
label={
|
||||||
|
<TabLink to={path}>
|
||||||
|
{label}
|
||||||
|
</TabLink>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</StyledTabsContainer>
|
||||||
|
<StyledActions>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={pathname.includes(
|
||||||
|
'incoming-webhooks',
|
||||||
|
)}
|
||||||
|
show={
|
||||||
|
<ResponsiveButton
|
||||||
|
onClick={onNewIncomingWebhook}
|
||||||
|
maxWidth={`${theme.breakpoints.values.sm}px`}
|
||||||
|
Icon={Add}
|
||||||
|
permission={ADMIN}
|
||||||
|
>
|
||||||
|
New incoming webhook
|
||||||
|
</ResponsiveButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StyledActions>
|
||||||
|
</StyledHeader>
|
||||||
|
}
|
||||||
|
elseShow={<PageHeader title='Integrations' />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
isLoading={loading}
|
||||||
|
withTabs={incomingWebhooksEnabled}
|
||||||
|
>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path='incoming-webhooks'
|
||||||
|
element={<span>TODO: Implement</span>}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path='*'
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={addons.length > 0}
|
||||||
|
show={
|
||||||
|
<ConfiguredIntegrations
|
||||||
|
addons={addons}
|
||||||
|
providers={providers}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AvailableIntegrations
|
||||||
|
providers={providers}
|
||||||
|
onNewIncomingWebhook={onNewIncomingWebhook}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -346,7 +346,7 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"advanced": true,
|
"advanced": true,
|
||||||
"mobile": true,
|
"mobile": true,
|
||||||
},
|
},
|
||||||
"path": "/integrations",
|
"path": "/integrations/*",
|
||||||
"title": "Integrations",
|
"title": "Integrations",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
@ -350,7 +350,7 @@ export const routes: IRoute[] = [
|
|||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/integrations',
|
path: '/integrations/*',
|
||||||
title: 'Integrations',
|
title: 'Integrations',
|
||||||
component: IntegrationList,
|
component: IntegrationList,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
@ -4,14 +4,16 @@ import handleErrorResponses from '../httpErrorResponseHandler';
|
|||||||
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||||
import useUiConfig from '../useUiConfig/useUiConfig';
|
import useUiConfig from '../useUiConfig/useUiConfig';
|
||||||
import { IIncomingWebhook } from 'interfaces/incomingWebhook';
|
import { IIncomingWebhook } from 'interfaces/incomingWebhook';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
const ENDPOINT = 'api/admin/incoming-webhooks';
|
const ENDPOINT = 'api/admin/incoming-webhooks';
|
||||||
|
|
||||||
export const useIncomingWebhooks = () => {
|
export const useIncomingWebhooks = () => {
|
||||||
const { isEnterprise } = useUiConfig();
|
const { isEnterprise } = useUiConfig();
|
||||||
|
const incomingWebhooksEnabled = useUiFlag('incomingWebhooks');
|
||||||
|
|
||||||
const { data, error, mutate } = useConditionalSWR(
|
const { data, error, mutate } = useConditionalSWR(
|
||||||
isEnterprise(),
|
isEnterprise() && incomingWebhooksEnabled,
|
||||||
{ incomingWebhooks: [] },
|
{ incomingWebhooks: [] },
|
||||||
formatApiPath(ENDPOINT),
|
formatApiPath(ENDPOINT),
|
||||||
fetcher,
|
fetcher,
|
||||||
|
Loading…
Reference in New Issue
Block a user