Skip to content

Commit f58cf05

Browse files
committed
feat: implement OAuth connection flow and update integration alerts (#4111)
1 parent c746e93 commit f58cf05

22 files changed

Lines changed: 901 additions & 115 deletions

client/eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ const jsRules = {
163163
"nteract",
164164
"ntlm",
165165
"nullable",
166+
"oauth",
166167
"oauth2",
167168
"objectstores",
168169
"oidc",

client/src/components/Alert.jsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ import {
3131
} from "react-bootstrap-icons";
3232
import { Alert } from "reactstrap";
3333

34-
import { ALERT_ICON_SIZE } from "./Alert.constants";
35-
3634
/**
3735
* Display a alert that can be dismissed.
3836
*
@@ -87,10 +85,10 @@ class RenkuAlert extends Component {
8785

8886
getIcon() {
8987
const icon = {
90-
danger: <ExclamationTriangle size={ALERT_ICON_SIZE} />,
91-
info: <InfoCircle size={ALERT_ICON_SIZE} />,
92-
warning: <ExclamationTriangle size={ALERT_ICON_SIZE} />,
93-
success: <CheckCircle size={ALERT_ICON_SIZE} />,
88+
danger: <ExclamationTriangle />,
89+
info: <InfoCircle />,
90+
warning: <ExclamationTriangle />,
91+
success: <CheckCircle />,
9492
}[this.props.color];
9593

9694
return icon;
@@ -110,7 +108,7 @@ class RenkuAlert extends Component {
110108
data-cy={this.props.dataCy || this.props["data-cy"]}
111109
>
112110
<div className={cx("d-flex", "gap-3")}>
113-
<div>{alertIcon}</div>
111+
<div className="fs-1">{alertIcon}</div>
114112
<div className={cx("my-auto", "overflow-auto", "w-100")}>
115113
{this.props.children}
116114
</div>

client/src/components/navbar/NavBarItems.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export function RenkuToolbarItemUser({ params }: RenkuToolbarItemUserProps) {
9595
User Secrets
9696
</DropdownItemTag>
9797

98-
<DropdownItemTag tag={Link} to={ABSOLUTE_ROUTES.v2.integrations}>
98+
<DropdownItemTag tag={Link} to={ABSOLUTE_ROUTES.v2.integrations.root}>
9999
Integrations
100100
</DropdownItemTag>
101101

client/src/features/ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay.tsx

Lines changed: 92 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* limitations under the License.
1717
*/
1818

19+
import { skipToken } from "@reduxjs/toolkit/query";
1920
import cx from "classnames";
2021
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
2122
import {
@@ -51,24 +52,33 @@ import { ErrorAlert, InfoAlert, WarnAlert } from "~/components/Alert";
5152
import { CommandCopy } from "~/components/commandCopy/CommandCopy";
5253
import ExternalLink from "~/components/ExternalLink";
5354
import RenkuBadge from "~/components/renkuBadge/RenkuBadge";
55+
import {
56+
useGetOauth2ProvidersQuery,
57+
type ConnectionStatus,
58+
} from "~/features/connectedServices/api/connectedServices.api";
5459
import {
5560
SEARCH_PARAM_ACTION_REQUIRED,
5661
SEARCH_PARAM_PROVIDER,
5762
SEARCH_PARAM_SOURCE,
5863
} from "~/features/connectedServices/connectedServices.constants";
5964
import RepositoryGitLabWarnBadge from "~/features/legacy/RepositoryGitLabWarnBadge";
60-
import { useGetRepositoryQuery } from "~/features/repositories/api/repositories.api";
65+
import {
66+
repositoriesApi,
67+
useGetRepositoryQuery,
68+
} from "~/features/repositories/api/repositories.api";
6169
import { useGetUserQueryState } from "~/features/usersV2/api/users.api";
6270
import { ABSOLUTE_ROUTES } from "~/routing/routes.constants";
6371
import AppContext from "~/utils/context/appContext";
6472
import { DEFAULT_APP_PARAMS } from "~/utils/context/appParams.constants";
73+
import useAppDispatch from "~/utils/customHooks/useAppDispatch.hook";
6574
import { ButtonWithMenuV2 } from "../../../../components/buttons/Button";
6675
import RtkOrDataServicesError from "../../../../components/errors/RtkOrDataServicesError";
6776
import { Loader } from "../../../../components/Loader";
6877
import PermissionsGuard from "../../../permissionsV2/PermissionsGuard";
6978
import { Project } from "../../../projectsV2/api/projectV2.api";
7079
import { usePatchProjectsByProjectIdMutation } from "../../../projectsV2/api/projectV2.enhanced-api";
7180
import useProjectPermissions from "../../utils/useProjectPermissions.hook";
81+
import { ConnectButton } from "./../../../connectedServices/ConnectedServicesPage";
7282
import { SshRepositoryUrlWarning } from "./AddCodeRepositoryModal";
7383
import {
7484
getRepositoryName,
@@ -658,6 +668,7 @@ function RepositoryView({
658668
<RepositoryCallToActionAlert
659669
hasWriteAccess={projectPermissions?.write}
660670
repositoryUrl={repositoryUrl}
671+
project={project}
661672
/>
662673
</div>
663674

@@ -697,7 +708,7 @@ function RepositoryView({
697708
(
698709
<Link
699710
to={{
700-
pathname: ABSOLUTE_ROUTES.v2.integrations,
711+
pathname: ABSOLUTE_ROUTES.v2.integrations.root,
701712
search,
702713
}}
703714
>
@@ -730,11 +741,14 @@ function LogInWarning() {
730741
interface RepositoryCallToActionAlertProps {
731742
hasWriteAccess: boolean;
732743
repositoryUrl: string;
744+
project: Project;
733745
}
734746
export function RepositoryCallToActionAlert({
735747
hasWriteAccess,
736748
repositoryUrl,
749+
project,
737750
}: RepositoryCallToActionAlertProps) {
751+
const dispatch = useAppDispatch();
738752
const { pathname, hash } = useLocation();
739753
const { params } = useContext(AppContext);
740754
const renkuContactEmail =
@@ -748,12 +762,24 @@ export function RepositoryCallToActionAlert({
748762
return userInfo && !userInfo?.isLoggedIn;
749763
}, [userInfo]);
750764

751-
const search = useMemo(() => {
752-
return `?${new URLSearchParams({
753-
[SEARCH_PARAM_PROVIDER]: data?.provider?.id ?? "",
754-
[SEARCH_PARAM_SOURCE]: `${pathname}${hash}`,
755-
}).toString()}`;
756-
}, [data, pathname, hash]);
765+
const { data: oauthProviders } = useGetOauth2ProvidersQuery(
766+
anonymousUser ? skipToken : undefined
767+
);
768+
const oauthProvider = useMemo(
769+
() => oauthProviders?.find((p) => p.id === data?.provider?.id) ?? undefined,
770+
[data?.provider?.id, oauthProviders]
771+
);
772+
773+
const onRepositoryOAuthConnected = useCallback(() => {
774+
project.repositories?.map((repoUrl) => {
775+
dispatch(
776+
repositoriesApi.util.invalidateTags([
777+
{ type: "Repository", id: repoUrl },
778+
])
779+
);
780+
});
781+
}, [dispatch, project]);
782+
757783
const searchActionRequired = useMemo(() => {
758784
return `?${new URLSearchParams({
759785
[SEARCH_PARAM_PROVIDER]: data?.provider?.id ?? "",
@@ -777,29 +803,35 @@ export function RepositoryCallToActionAlert({
777803
{data?.provider?.id ? (
778804
<>
779805
<p className="mb-2">
780-
Either the repository does not exist, or you do not have access to
781-
it.
806+
The repository is not accessible and your connection to{" "}
807+
<span className="fst-italic">{data.provider.name}</span> is
808+
invalid.
782809
</p>
783810
{anonymousUser ? (
784811
<LogInWarning />
785812
) : (
786813
<>
787-
<p className="mb-2">
788-
If you think you should have access, check your integration{" "}
789-
<span className="fst-italic">{data.provider.name}</span>.
790-
</p>
814+
<p className="mb-2">You can try to refresh it.</p>
815+
{oauthProvider && (
816+
<ConnectButton
817+
className="btn-sm"
818+
connectionStatus={
819+
data.connection?.status as ConnectionStatus | undefined
820+
}
821+
includeSource
822+
onConnected={onRepositoryOAuthConnected}
823+
provider={oauthProvider}
824+
withIcon
825+
/>
826+
)}
791827
<Link
792-
className={cx("btn", "btn-primary", "btn-sm")}
828+
className={cx("btn", "btn-outline-primary", "btn-sm", "ms-2")}
793829
to={{
794-
pathname: ABSOLUTE_ROUTES.v2.integrations,
795-
search:
796-
data?.connection?.status === "connected"
797-
? search
798-
: searchActionRequired,
830+
pathname: ABSOLUTE_ROUTES.v2.integrations.root,
831+
search: searchActionRequired,
799832
}}
800833
>
801-
<Plugin className={cx("bi", "me-1")} />
802-
View integration
834+
Check integration
803835
</Link>
804836
</>
805837
)}
@@ -816,7 +848,7 @@ export function RepositoryCallToActionAlert({
816848
currently supported{" "}
817849
<Link
818850
to={{
819-
pathname: ABSOLUTE_ROUTES.v2.integrations,
851+
pathname: ABSOLUTE_ROUTES.v2.integrations.root,
820852
}}
821853
>
822854
<Plugin className={cx("bi", "me-1")} />
@@ -892,19 +924,30 @@ export function RepositoryCallToActionAlert({
892924
data-cy="code-repository-alert"
893925
>
894926
<p className="mb-2">
895-
You can log in through the integration{" "}
927+
You can connect to{" "}
896928
<span className="fst-italic">{data.provider.name}</span> to enable
897929
pushing to repositories for which you have permissions.
898930
</p>
931+
{oauthProvider && (
932+
<ConnectButton
933+
className="btn-sm"
934+
connectionStatus={
935+
data.connection?.status as ConnectionStatus | undefined
936+
}
937+
includeSource
938+
onConnected={onRepositoryOAuthConnected}
939+
provider={oauthProvider}
940+
withIcon
941+
/>
942+
)}
899943
<Link
900-
className={cx("btn", "btn-primary", "btn-sm")}
944+
className={cx("btn", "btn-outline-primary", "btn-sm", "ms-2")}
901945
to={{
902-
pathname: ABSOLUTE_ROUTES.v2.integrations,
946+
pathname: ABSOLUTE_ROUTES.v2.integrations.root,
903947
search: searchActionRequired,
904948
}}
905949
>
906-
<Plugin className={cx("bi", "me-1")} />
907-
View integration
950+
Check integration
908951
</Link>
909952
</WarnAlert>
910953
);
@@ -931,17 +974,28 @@ export function RepositoryCallToActionAlert({
931974
{anonymousUser ? (
932975
<LogInWarning />
933976
) : (
934-
<Link
935-
className={cx("btn", "btn-primary", "btn-sm")}
936-
to={{
937-
pathname: ABSOLUTE_ROUTES.v2.integrations,
938-
search: searchActionRequired,
939-
}}
940-
>
941-
<Plugin className={cx("bi", "me-1")} />
942-
View integration
943-
</Link>
977+
oauthProvider && (
978+
<ConnectButton
979+
className="btn-sm"
980+
connectionStatus={
981+
data.connection?.status as ConnectionStatus | undefined
982+
}
983+
includeSource
984+
onConnected={onRepositoryOAuthConnected}
985+
provider={oauthProvider}
986+
withIcon
987+
/>
988+
)
944989
)}
990+
<Link
991+
className={cx("btn", "btn-outline-primary", "btn-sm", "ms-2")}
992+
to={{
993+
pathname: ABSOLUTE_ROUTES.v2.integrations.root,
994+
search: searchActionRequired,
995+
}}
996+
>
997+
Check integration
998+
</Link>
945999
</InfoAlert>
9461000
);
9471001
}

client/src/features/cloudStorage/AddOrEditCloudStorage.tsx

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import {
6464
} from "../connectedServices/connectedServices.constants";
6565
import type { DataConnectorSecret } from "../dataConnectorsV2/api/data-connectors.api";
6666
import { hasSchemaAccessMode } from "../dataConnectorsV2/components/dataConnector.utils";
67+
import { ConnectButton } from "./../connectedServices/ConnectedServicesPage";
6768
import {
6869
CLOUD_STORAGE_CONFIGURATION_PLACEHOLDER,
6970
CLOUD_STORAGE_INTEGRATION_KIND_MAP,
@@ -1034,8 +1035,6 @@ interface IntegrationAlertProps {
10341035
}
10351036

10361037
export function IntegrationAlert({ schema }: IntegrationAlertProps) {
1037-
const { pathname, hash } = useLocation();
1038-
10391038
const {
10401039
data: providers,
10411040
error: providersError,
@@ -1048,6 +1047,7 @@ export function IntegrationAlert({ schema }: IntegrationAlertProps) {
10481047
} = useGetOauth2ConnectionsQuery();
10491048
const error = providersError ?? connectionsError;
10501049
const isLoading = isLoadingProviders || isLoadingConnections;
1050+
const { pathname, hash } = useLocation();
10511051

10521052
const providerKind = useMemo(
10531053
() => CLOUD_STORAGE_INTEGRATION_KIND_MAP[schema.name],
@@ -1096,7 +1096,7 @@ export function IntegrationAlert({ schema }: IntegrationAlertProps) {
10961096
const link = singleProvider && (
10971097
<Link
10981098
to={{
1099-
pathname: ABSOLUTE_ROUTES.v2.integrations,
1099+
pathname: ABSOLUTE_ROUTES.v2.integrations.root,
11001100
search: new URLSearchParams({
11011101
[SEARCH_PARAM_PROVIDER]: singleProvider.id,
11021102
}).toString(),
@@ -1119,29 +1119,39 @@ export function IntegrationAlert({ schema }: IntegrationAlertProps) {
11191119
}
11201120

11211121
if (providersForSchema && providersForSchema.length > 0) {
1122-
// We don't know which provider to pick when there are multiple.
11231122
const provider = providersForSchema[0];
1124-
const link = (
1125-
<Link
1126-
to={{
1127-
pathname: ABSOLUTE_ROUTES.v2.integrations,
1128-
search: new URLSearchParams({
1129-
[SEARCH_PARAM_PROVIDER]: provider.id,
1130-
[SEARCH_PARAM_SOURCE]: `${pathname}${hash}`,
1131-
[SEARCH_PARAM_ACTION_REQUIRED]: "true",
1132-
}).toString(),
1133-
}}
1134-
>
1135-
{provider.display_name}
1136-
</Link>
1123+
const connection = connections?.find(
1124+
({ provider_id }) => provider_id === provider.id
11371125
);
11381126

11391127
return (
11401128
<WarnAlert dismissible={false}>
11411129
<h3>Action required</h3>
1142-
<p className="mb-0">
1143-
Please connect with the {link} Renku integration first.
1130+
<p className="mb-2">
1131+
Please connect the{" "}
1132+
<span className="fst-italic">{provider.display_name}</span> Renku
1133+
integration first.
11441134
</p>
1135+
<ConnectButton
1136+
className="btn-sm"
1137+
connectionStatus={connection?.status}
1138+
includeSource
1139+
provider={provider}
1140+
withIcon
1141+
/>
1142+
<Link
1143+
className={cx("btn", "btn-outline-primary", "btn-sm", "ms-2")}
1144+
to={{
1145+
pathname: ABSOLUTE_ROUTES.v2.integrations.root,
1146+
search: new URLSearchParams({
1147+
[SEARCH_PARAM_PROVIDER]: provider.id,
1148+
[SEARCH_PARAM_SOURCE]: `${pathname}${hash}`,
1149+
[SEARCH_PARAM_ACTION_REQUIRED]: "true",
1150+
}).toString(),
1151+
}}
1152+
>
1153+
Check integration
1154+
</Link>
11451155
</WarnAlert>
11461156
);
11471157
}

0 commit comments

Comments
 (0)