Skip to content
Open
9 changes: 7 additions & 2 deletions app/src/components/Home/home.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,19 @@

& .homepage-primary-cards-wrapper {
gap: 24px;
justify-content: center;
justify-content: flex-start;
align-items: flex-start;
margin-top: 40px;
margin-left: 90px;
padding-bottom: 12px;

& > .ant-col {
max-width: 49%;
}

> div{
width: 460px;
}
}

.ant-skeleton-paragraph li {
Expand All @@ -48,7 +53,7 @@
.homepage-emptycard {
width: 100%;
min-width: 325px;
max-width: 325px;
max-width: 500px;
padding-top: 20px;
height: 304px;
display: flex;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import React, { useCallback, useState } from "react";
import { Checkbox, Input, Modal, Radio, Tag } from "antd";
import { Checkbox, Input, Modal, Radio } from "antd";
import { useDispatch, useSelector } from "react-redux";
import * as Sentry from "@sentry/react";

import { getAppMode } from "store/selectors";
import { RQButton } from "lib/design-system-v2/components";
import { CreateTeamParams, LocalWorkspaceConfig, SharedOrPrivateWorkspaceConfig } from "types";
import { displayFolderSelector } from "components/mode-specific/desktop/misc/FileDialogButton";
import { CONSTANTS as GLOBAL_CONSTANTS } from "@requestly/requestly-core";
import { IoMdClose } from "@react-icons/all-files/io/IoMdClose";
import { CreateTeamParams, SharedOrPrivateWorkspaceConfig } from "types";
import { getFunctions, httpsCallable } from "firebase/functions";
import {
trackAddTeamMemberSuccess,
Expand All @@ -24,10 +21,8 @@ import { switchWorkspace } from "actions/TeamWorkspaceActions";
import { redirectToTeam } from "utils/RedirectionUtils";
import { useNavigate } from "react-router-dom";
import { getAvailableBillingTeams } from "store/features/billing/selectors";
import { createWorkspaceFolder } from "services/fsManagerServiceAdapter";
import { isActiveWorkspaceShared } from "store/slices/workspaces/selectors";
import { workspaceActions } from "store/slices/workspaces/slice";
import { Workspace, WorkspaceMemberRole, WorkspaceType } from "features/workspaces/types";
import { WorkspaceType } from "features/workspaces/types";
import "./createOldWorkspaceModal.scss";

interface Props {
Expand All @@ -45,17 +40,10 @@ export const CreateWorkspaceModalOld: React.FC<Props> = ({ isOpen, defaultWorksp
const isSharedWorkspaceMode = useSelector(isActiveWorkspaceShared);
const billingTeams = useSelector(getAvailableBillingTeams);
const [workspaceName, setWorkspaceName] = useState("");
const [workspaceType, setWorkspaceType] = useState(
defaultWorkspaceType || (user.loggedIn ? WorkspaceType.SHARED : WorkspaceType.LOCAL)
);
const [folderPath, setFolderPath] = useState("");
const [workspaceType, setWorkspaceType] = useState(defaultWorkspaceType || WorkspaceType.SHARED);
const [isLoading, setIsLoading] = useState(false);
const [isNotifyAllSelected, setIsNotifyAllSelected] = useState(false);

const folderSelectCallback = (path: string) => {
setFolderPath(path);
};

const handlePostTeamCreationStep = useCallback(
(teamId: string, newTeamName: string, hasMembersInSameDomain: boolean) => {
switchWorkspace(
Expand Down Expand Up @@ -138,43 +126,15 @@ export const CreateWorkspaceModalOld: React.FC<Props> = ({ isOpen, defaultWorksp
const createOrgTeamInvite = httpsCallable(functions, "invites-createOrganizationTeamInvite");
const upsertTeamCommonInvite = httpsCallable(functions, "invites-upsertTeamCommonInvite");

const config =
workspaceType === WorkspaceType.LOCAL
? ({ type: WorkspaceType.LOCAL, rootPath: folderPath } as LocalWorkspaceConfig)
: ({ type: WorkspaceType.SHARED } as SharedOrPrivateWorkspaceConfig);
const config = { type: WorkspaceType.SHARED } as SharedOrPrivateWorkspaceConfig;

try {
const teamId = await (async () => {
if (config.type === WorkspaceType.LOCAL) {
const workspaceCreationResult = await createWorkspaceFolder(workspaceName, config.rootPath);
if (workspaceCreationResult.type === "error") {
throw new Error(workspaceCreationResult.error.message);
}
const partialWorkspace = workspaceCreationResult.content;
const localWorkspace: Workspace = {
id: partialWorkspace.id,
name: partialWorkspace.name,
owner: user?.details?.profile?.uid,
accessCount: 1,
adminCount: 1,
members: {
[user?.details?.profile?.uid]: {
role: WorkspaceMemberRole.admin,
},
},
appsumo: null,
workspaceType: WorkspaceType.LOCAL,
rootPath: partialWorkspace.path,
};
dispatch(workspaceActions.upsertWorkspace(localWorkspace));
return partialWorkspace.id;
} else {
const response: any = await createTeam({
teamName: workspaceName,
config,
});
return response.data.teamId;
}
const response: any = await createTeam({
teamName: workspaceName,
config,
});
return response.data.teamId;
})();

trackNewTeamCreateSuccess(teamId, workspaceName, "create_workspace_modal", workspaceType);
Expand Down Expand Up @@ -216,16 +176,13 @@ export const CreateWorkspaceModalOld: React.FC<Props> = ({ isOpen, defaultWorksp
}, [
billingTeams,
callback,
dispatch,
isNotifyAllSelected,
toggleModal,
user?.details?.profile?.email,
workspaceName,
handlePostTeamCreationStep,
handleDomainInvitesCreation,
workspaceType,
folderPath,
user?.details?.profile?.uid,
]);

return (
Expand All @@ -245,7 +202,7 @@ export const CreateWorkspaceModalOld: React.FC<Props> = ({ isOpen, defaultWorksp
<RQButton onClick={toggleModal}>Cancel</RQButton>
<RQButton
type="primary"
disabled={!workspaceName.length || (workspaceType === WorkspaceType.LOCAL && !folderPath.length)}
disabled={!workspaceName.length}
loading={isLoading}
onClick={handleTeamWorkspaceCreation}
>
Expand Down Expand Up @@ -300,45 +257,6 @@ export const CreateWorkspaceModalOld: React.FC<Props> = ({ isOpen, defaultWorksp
</div>
),
},
{
value: WorkspaceType.LOCAL,
disabled: appMode !== GLOBAL_CONSTANTS.APP_MODES.DESKTOP,
label: (
<div className="workspace-type-content">
<div className="workspace-type-content_title">
Local workspace <Tag>BETA</Tag>
</div>
<div className="workspace-type-content_description">
In a Local Workspace, your files are stored on your device. Currently, only API client files are
supported.
</div>
{workspaceType === WorkspaceType.LOCAL ? (
<div className="folder-path-selector">
{folderPath.length ? (
<div className="selected-folder-container">
<div className="selected-folder-header">
<span>SELECTED LOCATION</span> <IoMdClose onClick={() => setFolderPath("")} />
</div>
<div className="selected-folder-path">{folderPath}</div>
</div>
) : (
<>
<RQButton
disabled={appMode !== GLOBAL_CONSTANTS.APP_MODES.DESKTOP}
block
type="primary"
onClick={() => displayFolderSelector(folderSelectCallback)}
>
Select a folder
</RQButton>
<div className="selector-description">A location to store your workspace data.</div>
</>
)}
</div>
) : null}
</div>
),
},
]}
></Radio.Group>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NativeError } from "errors/NativeError";

// Thrown by makeRequest when the migration block is active (RQ-4699). The
// legacy API Client stays mounted but request execution is stopped at the
// single choke point so every outbound path (HTTP/GraphQL send, response-panel
// Retry, Collection Runner, GraphQL introspection, WSDL/SOAP import fetch) is
// blocked. Pairs with the mandatory migration modal.
export class MigrationBlockedError extends NativeError {
constructor() {
super("API Client has moved to a dedicated app. Requests are disabled here.");
this.name = "MigrationBlockedError";
}
}
14 changes: 14 additions & 0 deletions app/src/features/apiClient/screens/apiClient/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { Request as HarRequest } from "har-format";
import { getDefaultAuth } from "./components/views/components/request/components/AuthorizationView/defaults";
import { ApiClientRecordsInterface } from "features/apiClient/helpers/modules/sync/interfaces";
import { UserAbortError } from "features/apiClient/errors/UserAbortError/UserAbortError";
import { MigrationBlockedError } from "features/apiClient/errors/MigrationBlockedError/MigrationBlockedError";
import { growthbook } from "utils/feature-flag/growthbook";
import { MIGRATION_BLOCK_FLAG } from "features/apiClient/screens/migrationBlock/constants";
import { Authorization } from "./components/views/components/request/components/AuthorizationView/types/AuthConfig";
import { inheritAuthFromParent } from "features/apiClient/helpers/auth";
import { AutogeneratedFieldsStore, parseAuth, parseContentType } from "features/apiClient/store/autogenerateStore";
Expand Down Expand Up @@ -52,6 +55,17 @@ export const makeRequest = async (
request: RQAPI.HttpRequest,
signal?: AbortSignal
): Promise<RQAPI.HttpResponse> => {
// RQ-4699: universal choke point. Every outbound path (HTTP/GraphQL send,
// response-panel Retry, Collection Runner, GraphQL introspection, WSDL/SOAP
// import fetch) funnels through makeRequest, so a single guard here is a full
// stop. Read the block state from the GrowthBook singleton (not a hook — this
// isn't a React context) so the guard can't itself throw; `isOn` returns a
// boolean even before features load. This mirrors the container's master
// kill-switch (MIGRATION_BLOCK_FLAG) that mounts the mandatory migration modal.
if (growthbook.isOn(MIGRATION_BLOCK_FLAG)) {
throw new MigrationBlockedError();
}

return new Promise((resolve, reject) => {
const abortListener = () => {
if (signal) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ import {
DOWNLOAD_URLS,
DOWNLOAD_LABELS,
REPORT_ISSUES_URL,
MIGRATION_BLOCK_DISMISSABLE_FLAG,
MIGRATION_BLOCK_CLOUD_FORCE_FLAG,
MIGRATION_BLOCK_LOCAL_STORAGE_FLAG,
MIGRATION_BLOCK_CLOUD_FLAG,
MIGRATION_BLOCK_LOCAL_FS_FLAG,
} from "./constants";
import {
trackMigrationBlockScreenShown,
Expand Down Expand Up @@ -80,14 +76,15 @@ function openExternalLink(url: string): void {
export const MigrationBlockModal: React.FC = () => {
const segment = useMigrationSegment();
const selectedWorkspaces = useGetAllSelectedWorkspaces();
const dismissable = useFeatureIsOn(MIGRATION_BLOCK_DISMISSABLE_FLAG);
// RQ-4699: the migration block is mandatory. The modal is non-dismissable and
// renders for every segment — the per-segment enable flags are no longer
// consulted here. The only gate is the container's master kill-switch
// (MIGRATION_BLOCK_FLAG), which is also what the makeRequest guard reads, so
// the modal and the request block turn on together.
const dismissable = false;
const forceCloudMigration = useFeatureIsOn(MIGRATION_BLOCK_CLOUD_FORCE_FLAG);
const isLocalStorageVariantOn = useFeatureIsOn(MIGRATION_BLOCK_LOCAL_STORAGE_FLAG);
const isCloudVariantOn = useFeatureIsOn(MIGRATION_BLOCK_CLOUD_FLAG);
const isLocalFsVariantOn = useFeatureIsOn(MIGRATION_BLOCK_LOCAL_FS_FLAG);

if (segment === "local-storage") {
if (!isLocalStorageVariantOn) return null;
// local-storage is always SINGLE view with exactly one selected workspace.
const workspace = selectedWorkspaces[0];
if (!workspace || workspace.status.loading) return null;
Expand All @@ -99,7 +96,6 @@ export const MigrationBlockModal: React.FC = () => {
}

if (segment === "auto-cloud") {
if (!isCloudVariantOn) return null;
const workspace = selectedWorkspaces[0];
if (!workspace || workspace.status.loading) return null;
return (
Expand All @@ -110,7 +106,6 @@ export const MigrationBlockModal: React.FC = () => {
}

if (segment === "auto-local-fs") {
if (!isLocalFsVariantOn) return null;
// LocalFS data lives on disk and the new app reads the same paths, so the
// copy is workspace-agnostic — no sign-in, no WorkspaceProvider at this
// level. Same modal renders for SINGLE (one LOCAL ws) and MULTI (N LOCAL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { FeatureSection, FeatureReleaseTag } from "./types";
import { CONSTANTS as GLOBAL_CONSTANTS } from "@requestly/requestly-core";
import { FiVideo } from "@react-icons/all-files/fi/FiVideo";
import { IoDocumentTextOutline } from "@react-icons/all-files/io5/IoDocumentTextOutline";
import { ApiOutlined } from "@ant-design/icons";

const { RULE_TYPES } = GLOBAL_CONSTANTS;
const { RULE_TYPES_CONFIG } = APP_CONSTANTS;
Expand Down Expand Up @@ -76,13 +75,6 @@ export const personaRecommendationData: FeatureSection[] = [
subTitle: "Create mocks for your APIs",
link: PATHS.MOCK_SERVER_V2.ABSOLUTE,
},
{
id: "test_api_endpoints",
title: "Test API endpoints",
icon: () => <ApiOutlined />,
subTitle: "Test responses quickly using API Client",
link: PATHS.API_CLIENT.ABSOLUTE,
},
],
},
];
Loading