fix: gsheet preview data UI using tabs for preview and configuration (#28318)

This commit is contained in:
Aman Agarwal 2023-11-03 19:15:00 +05:30 committed by GitHub
parent 8dab2c84e6
commit 72c4b4d996
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 887 additions and 524 deletions

View File

@ -1903,6 +1903,11 @@ export const ERR_FETCHING_DATASOURCE_PREVIEW_DATA = () =>
export const FETCHING_DATASOURCE_PREVIEW_DATA = () => "Loading data"; export const FETCHING_DATASOURCE_PREVIEW_DATA = () => "Loading data";
export const SCHEMA_PREVIEW_NO_DATA = () => export const SCHEMA_PREVIEW_NO_DATA = () =>
"No data records to show or the table header begins with an index other than 1"; "No data records to show or the table header begins with an index other than 1";
export const GSHEET_SPREADSHEET_LABEL = () => "Spreadsheets";
export const GSHEET_SPREADSHEET_LOADING = () => "Loading Spreadsheets";
export const GSHEET_SHEET_LOADING = () => "Loading Sheets";
export const GSHEET_DATA_LOADING = () => "Loading attributes";
export const GSHEET_SEARCH_PLACEHOLDER = () => "Search for spreadsheet";
//Layout Conversion flow //Layout Conversion flow
export const CONVERT = () => "Convert layout"; export const CONVERT = () => "Convert layout";
@ -2161,6 +2166,8 @@ export const COMMUNITY_TEMPLATES = {
export const EMPTY_TABLE_TITLE_TEXT = () => "Empty table"; export const EMPTY_TABLE_TITLE_TEXT = () => "Empty table";
export const EMPTY_TABLE_MESSAGE_TEXT = () => export const EMPTY_TABLE_MESSAGE_TEXT = () =>
"There are no data records to show"; "There are no data records to show";
export const LOADING_RECORDS_TITLE_TEXT = () => "Loading records";
export const LOADING_RECORDS_MESSAGE_TEXT = () => "This may take a few seconds";
export const EMPTY_TABLE_SVG_ALT_TEXT = () => "Empty table image"; export const EMPTY_TABLE_SVG_ALT_TEXT = () => "Empty table image";
export const DATA_PANE_TITLE = () => "Datasources in your Workspace"; export const DATA_PANE_TITLE = () => "Datasources in your Workspace";

View File

@ -350,7 +350,8 @@ export type DATASOURCE_SCHEMA_EVENTS =
| "GSHEET_PREVIEW_DATA_SHOWN" | "GSHEET_PREVIEW_DATA_SHOWN"
| "DATASOURCE_GENERATE_PAGE_BUTTON_CLICKED" | "DATASOURCE_GENERATE_PAGE_BUTTON_CLICKED"
| "DATASOURCE_PREVIEW_TABLE_CHANGE" | "DATASOURCE_PREVIEW_TABLE_CHANGE"
| "DATASOURCE_PREVIEW_DATA_SHOWN"; | "DATASOURCE_PREVIEW_DATA_SHOWN"
| "GSHEET_SPREADSHEET_SEARCH";
type WALKTHROUGH_EVENTS = "WALKTHROUGH_DISMISSED" | "WALKTHROUGH_SHOWN"; type WALKTHROUGH_EVENTS = "WALKTHROUGH_DISMISSED" | "WALKTHROUGH_SHOWN";

View File

@ -51,6 +51,7 @@ import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { getHasManagePagePermission } from "@appsmith/utils/BusinessFeatures/permissionPageHelpers"; import { getHasManagePagePermission } from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import type { Datasource } from "entities/Datasource";
import { DatasourceStructureContext } from "entities/Datasource"; import { DatasourceStructureContext } from "entities/Datasource";
const SCHEMA_GUIDE_GIF = `${ASSETS_CDN_URL}/schema.gif`; const SCHEMA_GUIDE_GIF = `${ASSETS_CDN_URL}/schema.gif`;
@ -152,7 +153,7 @@ const Placeholder = styled.div`
`; `;
const DataStructureListWrapper = styled.div` const DataStructureListWrapper = styled.div`
overflow-y: auto; overflow-y: hidden;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -176,7 +177,7 @@ interface CollapsibleProps {
label: string; label: string;
CustomLabelComponent?: (props: any) => JSX.Element; CustomLabelComponent?: (props: any) => JSX.Element;
isDisabled?: boolean; isDisabled?: boolean;
datasourceId?: string; datasource?: Partial<Datasource>;
containerRef?: MutableRefObject<HTMLDivElement | null>; containerRef?: MutableRefObject<HTMLDivElement | null>;
} }
@ -189,7 +190,7 @@ export function Collapsible({
children, children,
containerRef, containerRef,
CustomLabelComponent, CustomLabelComponent,
datasourceId, datasource,
expand = true, expand = true,
label, label,
}: CollapsibleProps) { }: CollapsibleProps) {
@ -220,7 +221,7 @@ export function Collapsible({
/> />
{!!CustomLabelComponent ? ( {!!CustomLabelComponent ? (
<CustomLabelComponent <CustomLabelComponent
datasourceId={datasourceId} datasource={datasource}
onRefreshCallback={() => handleCollapse(true)} onRefreshCallback={() => handleCollapse(true)}
/> />
) : ( ) : (
@ -427,7 +428,7 @@ function ActionSidebar({
<Collapsible <Collapsible
CustomLabelComponent={DatasourceStructureHeader} CustomLabelComponent={DatasourceStructureHeader}
containerRef={schemaRef} containerRef={schemaRef}
datasourceId={datasourceId} datasource={{ id: datasourceId }}
expand={!showSuggestedWidgets} expand={!showSuggestedWidgets}
label="Schema" label="Schema"
> >

View File

@ -181,9 +181,11 @@ export function renderDatasourceSection(
if ( if (
!value && !value &&
!!viewMode && !!viewMode &&
!!section.hidden && (!section.hidden ||
(!!section.hidden &&
"comparison" in section.hidden && "comparison" in section.hidden &&
section.hidden.comparison === ComparisonOperationsEnum.VIEW_MODE section.hidden.comparison ===
ComparisonOperationsEnum.VIEW_MODE))
) { ) {
value = section.initialValue; value = section.initialValue;
} }

View File

@ -78,12 +78,16 @@ export const ResizerContentContainer = styled.div`
border-bottom: 1px solid var(--ads-v2-color-border); border-bottom: 1px solid var(--ads-v2-color-border);
} }
} }
&.db-form-resizer-content.db-form-resizer-content-show-tabs { &.db-form-resizer-content.db-form-resizer-content-show-tabs,
&.saas-form-resizer-content.saas-form-resizer-content-show-tabs {
padding: 0; padding: 0;
& .t--ds-form-header { & .t--ds-form-header {
border-bottom: none; border-bottom: none;
} }
} }
&.saas-form-resizer-content.saas-form-resizer-content-show-tabs form {
padding-bottom: 0;
}
border-top: none; border-top: none;
.db-form-content-container { .db-form-content-container {
display: flex; display: flex;

View File

@ -871,16 +871,9 @@ class DatasourceEditorRouter extends React.Component<Props, State> {
}; };
shouldShowTabs = () => { shouldShowTabs = () => {
const { const { isEnabledForDSViewModeSchema, isPluginAllowedToPreviewData } =
isEnabledForDSViewModeSchema, this.props;
isPluginAllowedToPreviewData, return isEnabledForDSViewModeSchema && isPluginAllowedToPreviewData;
pluginDatasourceForm,
} = this.props;
const isRestAPI =
pluginDatasourceForm === DatasourceComponentTypes.RestAPIDatasourceForm;
return (
isEnabledForDSViewModeSchema && isPluginAllowedToPreviewData && !isRestAPI
);
}; };
renderTabsForViewMode = () => { renderTabsForViewMode = () => {

View File

@ -13,8 +13,11 @@ import DatasourceViewModeSchema from "./DatasourceViewModeSchema";
import { getCurrentEnvironmentId } from "@appsmith/selectors/environmentSelectors"; import { getCurrentEnvironmentId } from "@appsmith/selectors/environmentSelectors";
import { isEnvironmentValid } from "@appsmith/utils/Environments"; import { isEnvironmentValid } from "@appsmith/utils/Environments";
import type { Datasource } from "entities/Datasource"; import type { Datasource } from "entities/Datasource";
import { isGoogleSheetPluginDS } from "utils/editorContextUtils"; import {
import { getPluginNameFromId } from "@appsmith/selectors/entitiesSelector"; isDatasourceAuthorizedForQueryCreation,
isGoogleSheetPluginDS,
} from "utils/editorContextUtils";
import { getPlugin } from "@appsmith/selectors/entitiesSelector";
import GoogleSheetSchema from "./GoogleSheetSchema"; import GoogleSheetSchema from "./GoogleSheetSchema";
const TabsContainer = styled(Tabs)` const TabsContainer = styled(Tabs)`
@ -55,14 +58,22 @@ const DatasourceTabs = (props: DatasourceTabProps) => {
const setDatasourceViewModeFlagClick = (value: boolean) => { const setDatasourceViewModeFlagClick = (value: boolean) => {
dispatch(setDatasourceViewModeFlag(value)); dispatch(setDatasourceViewModeFlag(value));
}; };
const pluginName = useSelector((state) => const plugin = useSelector((state) =>
getPluginNameFromId(state, props.datasource.pluginId), getPlugin(state, props.datasource.pluginId),
); );
const isGoogleSheetPlugin = isGoogleSheetPluginDS(pluginName); const isGoogleSheetPlugin = isGoogleSheetPluginDS(plugin?.packageName);
const isPluginAuthorized =
!!plugin && !!props.datasource
? isDatasourceAuthorizedForQueryCreation(
props.datasource,
plugin,
currentEnvironmentId,
)
: false;
return ( return (
<TabsContainer <TabsContainer
defaultValue={ defaultValue={
isDatasourceValid isDatasourceValid || isPluginAuthorized
? VIEW_MODE_TABS.VIEW_DATA ? VIEW_MODE_TABS.VIEW_DATA
: VIEW_MODE_TABS.CONFIGURATIONS : VIEW_MODE_TABS.CONFIGURATIONS
} }

View File

@ -11,11 +11,10 @@ import React, { memo, useEffect, useState, useContext } from "react";
import EntityPlaceholder from "../Explorer/Entity/Placeholder"; import EntityPlaceholder from "../Explorer/Entity/Placeholder";
import DatasourceStructure from "./DatasourceStructure"; import DatasourceStructure from "./DatasourceStructure";
import { SearchInput, Text } from "design-system"; import { SearchInput, Text } from "design-system";
import styled from "styled-components";
import { getIsFetchingDatasourceStructure } from "@appsmith/selectors/entitiesSelector"; import { getIsFetchingDatasourceStructure } from "@appsmith/selectors/entitiesSelector";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import type { AppState } from "@appsmith/reducers"; import type { AppState } from "@appsmith/reducers";
import DatasourceStructureLoadingContainer from "./DatasourceStructureLoadingContainer"; import ItemLoadingIndicator from "./ItemLoadingIndicator";
import DatasourceStructureNotFound from "./DatasourceStructureNotFound"; import DatasourceStructureNotFound from "./DatasourceStructureNotFound";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import { PluginName } from "entities/Action"; import { PluginName } from "entities/Action";
@ -23,6 +22,7 @@ import WalkthroughContext from "components/featureWalkthrough/walkthroughContext
import { setFeatureWalkthroughShown } from "utils/storage"; import { setFeatureWalkthroughShown } from "utils/storage";
import { FEATURE_WALKTHROUGH_KEYS } from "constants/WalkthroughConstants"; import { FEATURE_WALKTHROUGH_KEYS } from "constants/WalkthroughConstants";
import { SCHEMA_SECTION_ID } from "entities/Action"; import { SCHEMA_SECTION_ID } from "entities/Action";
import { DatasourceStructureSearchContainer } from "./SchemaViewModeCSS";
interface Props { interface Props {
datasourceId: string; datasourceId: string;
@ -50,15 +50,6 @@ export const SCHEMALESS_PLUGINS: Array<string> = [
PluginName.OPEN_AI, PluginName.OPEN_AI,
]; ];
const DatasourceStructureSearchContainer = styled.div`
margin-bottom: 8px;
position: sticky;
top: 0;
overflow: hidden;
z-index: 10;
background: white;
`;
const Container = (props: Props) => { const Container = (props: Props) => {
const isLoading = useSelector((state: AppState) => const isLoading = useSelector((state: AppState) =>
getIsFetchingDatasourceStructure(state, props.datasourceId), getIsFetchingDatasourceStructure(state, props.datasourceId),
@ -113,7 +104,7 @@ const Container = (props: Props) => {
const filteredDastasourceStructure = const filteredDastasourceStructure =
props.datasourceStructure.tables.filter((table) => props.datasourceStructure.tables.filter((table) =>
table.name.includes(value), table.name.toLowerCase().includes(value.toLowerCase()),
); );
setDatasourceStructure({ tables: filteredDastasourceStructure }); setDatasourceStructure({ tables: filteredDastasourceStructure });
@ -129,7 +120,9 @@ const Container = (props: Props) => {
view = ( view = (
<> <>
{props.context !== DatasourceStructureContext.EXPLORER && ( {props.context !== DatasourceStructureContext.EXPLORER && (
<DatasourceStructureSearchContainer> <DatasourceStructureSearchContainer
className={`t--search-container--${props.context.toLowerCase()}`}
>
<SearchInput <SearchInput
className="datasourceStructure-search" className="datasourceStructure-search"
endIcon="close" endIcon="close"
@ -137,7 +130,7 @@ const Container = (props: Props) => {
placeholder={createMessage( placeholder={createMessage(
DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT, DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT,
)} )}
size={"md"} size={"sm"}
startIcon="search" startIcon="search"
type="text" type="text"
/> />
@ -199,7 +192,7 @@ const Container = (props: Props) => {
props.context !== DatasourceStructureContext.EXPLORER && props.context !== DatasourceStructureContext.EXPLORER &&
isLoading isLoading
) { ) {
view = <DatasourceStructureLoadingContainer />; view = <ItemLoadingIndicator type="SCHEMA" />;
} }
return view; return view;

View File

@ -1,15 +1,23 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { Button, Text } from "design-system"; import { Button, Text } from "design-system";
import styled from "styled-components"; import styled from "styled-components";
import { refreshDatasourceStructure } from "actions/datasourceActions"; import { refreshDatasourceStructure } from "actions/datasourceActions";
import { SCHEMA_LABEL, createMessage } from "@appsmith/constants/messages"; import {
GSHEET_SPREADSHEET_LABEL,
SCHEMA_LABEL,
createMessage,
} from "@appsmith/constants/messages";
import type { Datasource } from "entities/Datasource";
import { DatasourceStructureContext } from "entities/Datasource"; import { DatasourceStructureContext } from "entities/Datasource";
import { getPluginPackageNameFromId } from "@appsmith/selectors/entitiesSelector";
import { isGoogleSheetPluginDS } from "utils/editorContextUtils";
interface Props { interface Props {
datasourceId: string; datasource?: Datasource;
onRefreshCallback?: () => void; onRefreshCallback?: () => void;
paddingBottom?: boolean; paddingBottom?: boolean;
refetchFn?: () => void;
} }
const HeaderWrapper = styled.div<{ paddingBottom: boolean }>` const HeaderWrapper = styled.div<{ paddingBottom: boolean }>`
@ -24,27 +32,41 @@ export default function DatasourceStructureHeader(props: Props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const dispatchRefresh = useCallback( const dispatchRefresh = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => { (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (props.datasource?.id) {
event.stopPropagation(); event.stopPropagation();
if (props.refetchFn) {
props.refetchFn();
} else {
dispatch( dispatch(
refreshDatasourceStructure( refreshDatasourceStructure(
props.datasourceId, props.datasource?.id,
DatasourceStructureContext.QUERY_EDITOR, DatasourceStructureContext.QUERY_EDITOR,
), ),
); );
}
!!props.onRefreshCallback && props.onRefreshCallback(); !!props.onRefreshCallback && props.onRefreshCallback();
}
}, },
[dispatch, props.datasourceId], [dispatch, props.datasource?.id],
); );
const pluginPackageName = useSelector((state) =>
getPluginPackageNameFromId(state, props.datasource?.pluginId || ""),
);
const isGoogleSheetPlugin = isGoogleSheetPluginDS(pluginPackageName);
return ( return (
<HeaderWrapper <HeaderWrapper
className="datasourceStructure-header" className="datasourceStructure-header"
paddingBottom={!!props.paddingBottom} paddingBottom={!!props.paddingBottom}
> >
<Text kind="heading-xs" renderAs="h3"> <Text kind="heading-xs" renderAs="h3">
{createMessage(SCHEMA_LABEL)} {createMessage(
isGoogleSheetPlugin ? GSHEET_SPREADSHEET_LABEL : SCHEMA_LABEL,
)}
</Text> </Text>
<Button <Button
className="datasourceStructure-refresh" className="datasourceStructure-refresh"

View File

@ -1,36 +0,0 @@
import React from "react";
import { createMessage, LOADING_SCHEMA } from "@appsmith/constants/messages";
import { Spinner, Text } from "design-system";
import styled from "styled-components";
const LoadingContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
& > p {
margin-left: 0.5rem;
}
`;
const SpinnerWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
const DatasourceStructureLoadingContainer = () => {
return (
<LoadingContainer>
<SpinnerWrapper>
<Spinner size={"sm"} />
</SpinnerWrapper>
<Text kind="body-m" renderAs="p">
{createMessage(LOADING_SCHEMA)}
</Text>
</LoadingContainer>
);
};
export default DatasourceStructureLoadingContainer;

View File

@ -1,5 +1,4 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { DatasourceStructureContainer as DatasourceStructureList } from "./DatasourceStructureContainer"; import { DatasourceStructureContainer as DatasourceStructureList } from "./DatasourceStructureContainer";
import { import {
@ -8,16 +7,10 @@ import {
getNumberOfEntitiesInCurrentPage, getNumberOfEntitiesInCurrentPage,
} from "@appsmith/selectors/entitiesSelector"; } from "@appsmith/selectors/entitiesSelector";
import DatasourceStructureHeader from "./DatasourceStructureHeader"; import DatasourceStructureHeader from "./DatasourceStructureHeader";
import { MessageWrapper, TableWrapper } from "./GoogleSheetSchema"; import { Button } from "design-system";
import { Spinner, Text, Button } from "design-system";
import { import {
ERR_FETCHING_DATASOURCE_PREVIEW_DATA,
FETCHING_DATASOURCE_PREVIEW_DATA,
DATASOURCE_GENERATE_PAGE_BUTTON, DATASOURCE_GENERATE_PAGE_BUTTON,
EMPTY_TABLE_TITLE_TEXT,
EMPTY_TABLE_MESSAGE_TEXT,
createMessage, createMessage,
EMPTY_TABLE_SVG_ALT_TEXT,
} from "@appsmith/constants/messages"; } from "@appsmith/constants/messages";
import Table from "pages/Editor/QueryEditor/Table"; import Table from "pages/Editor/QueryEditor/Table";
import { generateTemplateToUpdatePage } from "actions/pageActions"; import { generateTemplateToUpdatePage } from "actions/pageActions";
@ -44,98 +37,22 @@ import {
getHasCreatePagePermission, getHasCreatePagePermission,
hasCreateDSActionPermissionInApp, hasCreateDSActionPermissionInApp,
} from "@appsmith/utils/BusinessFeatures/permissionPageHelpers"; } from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import EmptyTableSVG from "assets/images/empty-table-in-display-preview.svg"; import RenderInterimDataState from "./RenderInterimDataState";
import {
const ViewModeSchemaContainer = styled.div` ButtonContainer,
height: 100%; DataWrapperContainer,
width: 100%; DatasourceDataContainer,
display: flex; DatasourceListContainer,
flex-direction: column; StructureContainer,
`; TableWrapper,
ViewModeSchemaContainer,
const DataWrapperContainer = styled.div` } from "./SchemaViewModeCSS";
display: flex;
flex: 1;
overflow: hidden;
`;
const StructureContainer = styled.div`
height: 100%;
width: 25%;
padding: var(--ads-v2-spaces-4) var(--ads-v2-spaces-5);
padding-left: var(--ads-v2-spaces-7);
display: flex;
flex-direction: column;
overflow: hidden;
border-right: 1px solid var(--ads-v2-color-gray-300);
flex-shrink: 0;
`;
const DatasourceDataContainer = styled.div`
height: 100%;
width: 75%;
display: flex;
flex-direction: column;
`;
const DatasourceListContainer = styled.div`
height: 100%;
display: flex;
flex-direction: column;
div {
flex-shrink: 0;
}
div ~ div {
flex: 1;
}
.t--schema-virtuoso-container {
height: 100%;
}
`;
const SchemaStateMessageWrapper = styled.div`
width: auto;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
img {
padding-bottom: var(--ads-v2-spaces-7);
}
span:first-child {
padding-bottom: var(--ads-v2-spaces-2);
}
`;
const ButtonContainer = styled.div`
display: flex;
flex-shrink: 0;
justify-content: flex-end;
border-top: 1px solid var(--ads-v2-color-gray-300);
padding: var(--ads-v2-spaces-4);
`;
interface Props { interface Props {
datasource: Datasource; datasource: Datasource;
setDatasourceViewModeFlag: (viewMode: boolean) => void; setDatasourceViewModeFlag: (viewMode: boolean) => void;
} }
const renderEmptyTablePage = () => {
return (
<SchemaStateMessageWrapper>
{/* Render empty table image */}
<img alt={createMessage(EMPTY_TABLE_SVG_ALT_TEXT)} src={EmptyTableSVG} />
{/* Show description below the image */}
{/* Show title */}
<Text style={{ fontWeight: "bold" }}>
{createMessage(EMPTY_TABLE_TITLE_TEXT)}
</Text>
{/* Show description */}
<Text>{createMessage(EMPTY_TABLE_MESSAGE_TEXT)}</Text>
</SchemaStateMessageWrapper>
);
};
const DatasourceViewModeSchema = (props: Props) => { const DatasourceViewModeSchema = (props: Props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -200,6 +117,7 @@ const DatasourceViewModeSchema = (props: Props) => {
if ( if (
isDatasourceStructureLoading || isDatasourceStructureLoading ||
!datasourceStructure || !datasourceStructure ||
!datasourceStructure.tables ||
(datasourceStructure && datasourceStructure?.error) (datasourceStructure && datasourceStructure?.error)
) { ) {
setPreviewData([]); setPreviewData([]);
@ -248,6 +166,10 @@ const DatasourceViewModeSchema = (props: Props) => {
} }
}, [previewData]); }, [previewData]);
useEffect(() => {
setPreviewData([]);
}, [props.datasource.id]);
const onEntityTableClick = (table: string) => { const onEntityTableClick = (table: string) => {
AnalyticsUtil.logEvent("DATASOURCE_PREVIEW_TABLE_CHANGE", { AnalyticsUtil.logEvent("DATASOURCE_PREVIEW_TABLE_CHANGE", {
datasourceId: props.datasource.id, datasourceId: props.datasource.id,
@ -304,10 +226,12 @@ const DatasourceViewModeSchema = (props: Props) => {
<ViewModeSchemaContainer> <ViewModeSchemaContainer>
<DataWrapperContainer> <DataWrapperContainer>
<StructureContainer> <StructureContainer>
{props.datasource && (
<DatasourceStructureHeader <DatasourceStructureHeader
datasourceId={props.datasource.id} datasource={props.datasource}
paddingBottom paddingBottom
/> />
)}
<DatasourceListContainer> <DatasourceListContainer>
<DatasourceStructureList <DatasourceStructureList
context={DatasourceStructureContext.DATASOURCE_VIEW_MODE} context={DatasourceStructureContext.DATASOURCE_VIEW_MODE}
@ -322,31 +246,25 @@ const DatasourceViewModeSchema = (props: Props) => {
</StructureContainer> </StructureContainer>
<DatasourceDataContainer> <DatasourceDataContainer>
<TableWrapper> <TableWrapper>
{isLoading && ( {(isLoading || isDatasourceStructureLoading) && (
<MessageWrapper> <RenderInterimDataState state="LOADING" />
<Spinner size="md" />
<Text style={{ marginLeft: "8px" }}>
{createMessage(FETCHING_DATASOURCE_PREVIEW_DATA)}
</Text>
</MessageWrapper>
)} )}
{!isLoading && failedFetchingPreviewData && ( {(!isLoading || !isDatasourceStructureLoading) &&
<MessageWrapper> (failedFetchingPreviewData || previewDataError) && (
<Text color="var(--ads-color-red-500)"> <RenderInterimDataState state="FAILED" />
{createMessage(ERR_FETCHING_DATASOURCE_PREVIEW_DATA)}
</Text>
</MessageWrapper>
)} )}
{!isLoading && {!isLoading &&
!isDatasourceStructureLoading &&
!failedFetchingPreviewData && !failedFetchingPreviewData &&
!previewDataError && !previewDataError &&
previewData?.length > 0 && <Table data={previewData} />} previewData?.length > 0 && (
{!isLoading && <Table data={previewData} shouldResize={false} />
!failedFetchingPreviewData &&
!previewDataError &&
previewData?.length < 1 && (
<MessageWrapper>{renderEmptyTablePage()}</MessageWrapper>
)} )}
{!isLoading &&
!isDatasourceStructureLoading &&
!failedFetchingPreviewData &&
!previewDataError &&
!previewData?.length && <RenderInterimDataState state="NODATA" />}
</TableWrapper> </TableWrapper>
</DatasourceDataContainer> </DatasourceDataContainer>
</DataWrapperContainer> </DataWrapperContainer>

View File

@ -1,24 +1,16 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import type { DropdownOption } from "design-system-old"; import type { DropdownOption } from "design-system-old";
import { Button, Option, Select, Spinner, Text } from "design-system"; import { Button, SearchInput } from "design-system";
import { import {
useSheetData, useSheetData,
useSheetsList, useSheetsList,
useSpreadSheets, useSpreadSheets,
} from "../GeneratePage/components/GeneratePageForm/hooks"; } from "../GeneratePage/components/GeneratePageForm/hooks";
import type { import type { DropdownOptions } from "../GeneratePage/components/constants";
DatasourceTableDropdownOption, import { DEFAULT_DROPDOWN_OPTION } from "../GeneratePage/components/constants";
DropdownOptions,
} from "../GeneratePage/components/constants";
import {
DEFAULT_DROPDOWN_OPTION,
DROPDOWN_DIMENSION,
} from "../GeneratePage/components/constants";
import { SelectWrapper } from "../GeneratePage/components/GeneratePageForm/styles";
import { isEmpty } from "lodash"; import { isEmpty } from "lodash";
import Table from "pages/Editor/QueryEditor/Table"; import Table from "pages/Editor/QueryEditor/Table";
import styled from "styled-components";
import { import {
getCurrentApplicationId, getCurrentApplicationId,
getPagePermissions, getPagePermissions,
@ -26,10 +18,8 @@ import {
import { generateTemplateToUpdatePage } from "actions/pageActions"; import { generateTemplateToUpdatePage } from "actions/pageActions";
import { import {
createMessage, createMessage,
ERR_FETCHING_DATASOURCE_PREVIEW_DATA,
FETCHING_DATASOURCE_PREVIEW_DATA,
DATASOURCE_GENERATE_PAGE_BUTTON, DATASOURCE_GENERATE_PAGE_BUTTON,
SCHEMA_PREVIEW_NO_DATA, GSHEET_SEARCH_PLACEHOLDER,
} from "@appsmith/constants/messages"; } from "@appsmith/constants/messages";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors"; import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors";
@ -41,76 +31,23 @@ import {
getHasCreatePagePermission, getHasCreatePagePermission,
hasCreateDSActionPermissionInApp, hasCreateDSActionPermissionInApp,
} from "@appsmith/utils/BusinessFeatures/permissionPageHelpers"; } from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import RenderInterimDataState from "./RenderInterimDataState";
export const MessageWrapper = styled.div` import {
display: flex; ButtonContainer,
align-items: center; DataWrapperContainer,
justify-content: center; DatasourceAttributesWrapper,
height: 200px; DatasourceDataContainer,
`; DatasourceListContainer,
DatasourceStructureSearchContainer,
export const TableWrapper = styled.div` StructureContainer,
overflow-x: auto; TableWrapper,
height: 100%; ViewModeSchemaContainer,
&& > div { } from "./SchemaViewModeCSS";
width: 100%; import DatasourceStructureHeader from "./DatasourceStructureHeader";
} import Entity from "../Explorer/Entity";
import DatasourceField from "./DatasourceField";
&& > ${MessageWrapper} { import { setEntityCollapsibleState } from "actions/editorContextActions";
width: 100%; import ItemLoadingIndicator from "./ItemLoadingIndicator";
height: 100%;
}
&& .t--table-response {
border: none;
height: 100%;
overflow: hidden;
}
&& .tableWrap {
overflow: auto;
}
& .table {
background: none;
}
& .table div:first-of-type .tr {
background: var(--ads-v2-color-black-5);
border-right: none;
border-bottom: 1px solid var(--ads-v2-color-black-75);
}
&& .table div.tbody .tr {
background: var(--ads-v2-color-white);
border-bottom: 1px solid var(--ads-v2-color-black-75);
}
&& .table .td,
&& .table .th {
border-right: none;
border-bottom: none;
}
button {
margin-right: 24px;
}
&& .tableWrap {
}
`;
const SelectContainer = styled.div`
display: flex;
margin-top: 16px;
margin-bottom: 16px;
.t--datasource-generate-page {
align-self: flex-end;
margin-left: auto;
}
`;
const SelectListWrapper = styled.div`
display: flex;
flex-direction: column;
width: 278px;
margin-right: 16px;
& div {
margin-bottom: 0px;
}
`;
interface Props { interface Props {
datasourceId: string; datasourceId: string;
@ -122,25 +59,47 @@ const MAX_SHEET_ROWS_LENGTH = 12;
// ---------- GoogleSheetSchema Component ------- // ---------- GoogleSheetSchema Component -------
function GoogleSheetSchema(props: Props) { function GoogleSheetSchema(props: Props) {
const [datasourceTableOptions, setSelectedDatasourceTableOptions] = const [spreadsheetOptions, setSpreadsheetOptions] = useState<DropdownOptions>(
useState<DropdownOptions>([]); [],
const [selectedDatasourceIsInvalid, setSelectedDatasourceIsInvalid] = );
useState(false); const [sheetOptions, setSheetOptions] = useState<DropdownOptions>([]);
const { fetchAllSpreadsheets, isFetchingSpreadsheets } = useSpreadSheets({ const [sheetData, setSheetData] = useState<any>([]);
setSelectedDatasourceTableOptions,
setSelectedDatasourceIsInvalid,
});
const {
failedFetchingSheetsList,
fetchSheetsList,
isFetchingSheetsList,
sheetsList,
} = useSheetsList();
const { fetchSheetData, isFetchingSheetData, sheetData } = useSheetData();
const [selectedSpreadsheet, setSelectedSpreadsheet] = const [selectedSpreadsheet, setSelectedSpreadsheet] =
useState<DropdownOption>({}); useState<DropdownOption>({});
const [selectedSheet, setSelectedSheet] = useState<DropdownOption>({}); const [selectedSheet, setSelectedSheet] = useState<DropdownOption>({});
const [currentSheetData, setCurrentSheetData] = useState<any>(); const [searchString, setSearchString] = useState<string>("");
const [selectedDatasourceIsInvalid, setSelectedDatasourceIsInvalid] =
useState(false);
const { fetchAllSpreadsheets, isFetchingSpreadsheets } = useSpreadSheets({
setSelectedDatasourceTableOptions: setSpreadsheetOptions,
setSelectedDatasourceIsInvalid,
});
const handleSearch = (value: string) => {
setSearchString(value.toLowerCase());
AnalyticsUtil.logEvent("GSHEET_SPREADSHEET_SEARCH", {
datasourceId: props.datasourceId,
pluginId: props.pluginId,
});
};
const setSlicedSheetData = (response: DropdownOptions) => {
// Getting the top 12 rows as for experimentation we need to keep this number fixed for preview
AnalyticsUtil.logEvent("GSHEET_PREVIEW_DATA_SHOWN", {
datasourceId: props.datasourceId,
pluginId: props.pluginId,
});
setSheetData(response.slice(0, MAX_SHEET_ROWS_LENGTH));
};
const { failedFetchingSheetsList, fetchSheetsList, isFetchingSheetsList } =
useSheetsList({ setSheetOptions });
const { failedFetchingSheetData, fetchSheetData, isFetchingSheetData } =
useSheetData({
setSheetData: setSlicedSheetData,
});
const applicationId: string = useSelector(getCurrentApplicationId); const applicationId: string = useSelector(getCurrentApplicationId);
const datasource = useSelector((state) => const datasource = useSelector((state) =>
getDatasource(state, props.datasourceId), getDatasource(state, props.datasourceId),
@ -148,101 +107,176 @@ function GoogleSheetSchema(props: Props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
// Fetch spreadsheets if datasourceId present const scrollIntoView = (
useEffect(() => { elementId: string,
containerId: string,
offset: number = 0,
) => {
try {
const element = document.querySelector(elementId);
const container = document.querySelector(containerId);
if (element && container) {
const elementRect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const scrollTop = container.scrollTop;
const scrollAmount =
elementRect.top - containerRect.top + scrollTop + offset;
container.scrollTop = scrollAmount;
}
} catch {}
};
const collapseAccordions = (
datasourceId: string,
spreadSheet?: string,
sheet?: string,
collapseSpreadsheet: boolean = true,
) => {
if (!isEmpty(spreadSheet) && !isEmpty(sheet)) {
dispatch(
setEntityCollapsibleState(
`${datasourceId}-${spreadSheet}-${sheet}`,
false,
),
);
}
if (!isEmpty(spreadSheet) && collapseSpreadsheet) {
dispatch(
setEntityCollapsibleState(`${datasourceId}-${spreadSheet}`, false),
);
}
};
const selectSpreadsheetAndToggle = (option: DropdownOption) => {
collapseAccordions(
datasource?.id || "",
selectedSpreadsheet.value,
selectedSheet.value,
);
setSelectedSheet(DEFAULT_DROPDOWN_OPTION);
setSheetOptions([]);
setSheetData(undefined);
dispatch(
setEntityCollapsibleState(`${datasource?.id}-${option.value}`, true),
);
scrollIntoView(
`#${CSS.escape(`entity-${datasource?.id}-${option.value}`)}`,
".t--gsheet-structure",
);
setSelectedSpreadsheet(option);
fetchSheetsList({
requestObject: {},
selectedDatasourceId: props.datasourceId,
selectedSpreadsheetUrl: option.value || "",
pluginId: props.pluginId || "",
});
};
const selectSheetAndToggle = (option: DropdownOption) => {
collapseAccordions(
datasource?.id || "",
selectedSpreadsheet.value,
selectedSheet.value,
false,
);
dispatch(
setEntityCollapsibleState(
`${datasource?.id}-${selectedSpreadsheet.value}-${option.value}`,
true,
),
);
scrollIntoView(
`#${CSS.escape(
`entity-${datasource?.id}-${selectedSpreadsheet.value}-${option.value}`,
)}`,
".t--gsheet-structure",
-30,
);
setSelectedSheet(option);
setSheetData(undefined);
fetchSheetData({
selectedDatasourceId: datasource?.id || "",
selectedSpreadsheetUrl: selectedSpreadsheet.value || "",
selectedSheetName: option.value || "",
pluginId: props.pluginId || "",
});
};
const refetchAllSpreadsheets = () => {
if (!!props.datasourceId && !!props.pluginId) { if (!!props.datasourceId && !!props.pluginId) {
fetchAllSpreadsheets({ fetchAllSpreadsheets({
selectedDatasourceId: props.datasourceId, selectedDatasourceId: props.datasourceId,
pluginId: props.pluginId || "", pluginId: props.pluginId || "",
requestObject: {}, requestObject: {},
}); });
} setSpreadsheetOptions([]);
}, [props.datasourceId, props.pluginId, dispatch]); setSheetOptions([]);
setSheetData(undefined);
// When user selects a spreadsheet setSelectedSpreadsheet((ss) => {
// Fetch all sheets inside that spreadsheet setSelectedSheet((s) => {
useEffect(() => { collapseAccordions(datasource?.id || "", ss.value, s.value);
if (!!props.datasourceId && !!props.pluginId && selectedSpreadsheet.value) { return {};
setSelectedSheet(DEFAULT_DROPDOWN_OPTION); });
setCurrentSheetData(undefined); return {};
fetchSheetsList({
requestObject: {},
selectedDatasourceId: props.datasourceId,
selectedSpreadsheetUrl: selectedSpreadsheet.value,
pluginId: props.pluginId,
}); });
} }
}, [ };
selectedSpreadsheet.value,
props.datasourceId,
props.pluginId,
dispatch,
fetchSheetsList,
]);
// When user selects a sheet name // Fetch spreadsheets if datasourceId present
// Fetch all sheet data inside that sheet
useEffect(() => { useEffect(() => {
if (!!props.datasourceId && !!props.pluginId && selectedSheet.value) { fetchAllSpreadsheets({
setCurrentSheetData(undefined);
fetchSheetData({
selectedDatasourceId: props.datasourceId, selectedDatasourceId: props.datasourceId,
selectedSpreadsheetUrl: selectedSpreadsheet.value || "",
selectedSheetName: selectedSheet.value,
pluginId: props.pluginId || "", pluginId: props.pluginId || "",
requestObject: {},
}); });
} }, [props.datasourceId, props.pluginId, dispatch]);
}, [
selectedSheet.value,
props.datasourceId,
props.pluginId,
dispatch,
fetchSheetData,
]);
// Set first spreadsheet as default option in the dropdown // Set first spreadsheet as default option in the dropdown
useEffect(() => { useEffect(() => {
if ( if (spreadsheetOptions?.length > 0 && isEmpty(selectedSpreadsheet.value)) {
datasourceTableOptions?.length > 0 && selectSpreadsheetAndToggle(spreadsheetOptions[0]);
isEmpty(selectedSpreadsheet.value)
) {
setSelectedSpreadsheet(datasourceTableOptions[0]);
} }
}, [selectedSpreadsheet, datasourceTableOptions]); }, [selectedSpreadsheet, spreadsheetOptions]);
// Set first sheet as default option in the dropdown // Set first sheet as default option in the dropdown
useEffect(() => { useEffect(() => {
if ( if (
sheetsList?.length > 0 && sheetOptions?.length > 0 &&
isEmpty(selectedSheet.value) && isEmpty(selectedSheet.value) &&
!isFetchingSheetsList !isFetchingSheetsList &&
!isFetchingSpreadsheets
) { ) {
setSelectedSheet(sheetsList[0]); selectSheetAndToggle(sheetOptions[0]);
} }
}, [selectedSheet, sheetsList, isFetchingSheetsList]); }, [
selectedSheet,
sheetOptions,
isFetchingSheetsList,
isFetchingSpreadsheets,
]);
// Set current sheet data
useEffect(() => { useEffect(() => {
if (sheetData) { return () => {
// Getting the top 12 rows as for experimentation we need to keep this number fixed for preview collapseAccordions(
AnalyticsUtil.logEvent("GSHEET_PREVIEW_DATA_SHOWN", { datasource?.id || "",
datasourceId: props.datasourceId, selectedSpreadsheet.value,
pluginId: props.pluginId, selectedSheet.value,
}); );
setCurrentSheetData(sheetData.slice(0, MAX_SHEET_ROWS_LENGTH)); };
} }, [datasource?.id]);
}, [sheetData]);
const onSelectSpreadsheet = ( const onSelectSpreadsheet = (
table: string | undefined, table: string | undefined,
tableObj: DatasourceTableDropdownOption | undefined, tableObj: DropdownOption | undefined,
) => { ) => {
if (table && tableObj) { if (table && tableObj) {
AnalyticsUtil.logEvent("GSHEET_PREVIEW_SPREADSHEET_CHANGE", { AnalyticsUtil.logEvent("GSHEET_PREVIEW_SPREADSHEET_CHANGE", {
datasourceId: props.datasourceId, datasourceId: props.datasourceId,
pluginId: props.pluginId, pluginId: props.pluginId,
}); });
setSelectedSpreadsheet(tableObj); selectSpreadsheetAndToggle(tableObj);
} }
}; };
@ -255,25 +289,22 @@ function GoogleSheetSchema(props: Props) {
datasourceId: props.datasourceId, datasourceId: props.datasourceId,
pluginId: props.pluginId, pluginId: props.pluginId,
}); });
setSelectedSheet(sheetObj); selectSheetAndToggle(sheetObj);
} }
}; };
const isError = selectedDatasourceIsInvalid || failedFetchingSheetsList; const isError =
selectedDatasourceIsInvalid ||
failedFetchingSheetsList ||
failedFetchingSheetData;
const isLoading = const isLoading =
isFetchingSpreadsheets || isFetchingSpreadsheets || isFetchingSheetsList || isFetchingSheetData;
isFetchingSheetsList ||
isFetchingSheetData ||
(!isError && !currentSheetData);
const onGsheetGeneratePage = () => { const onGsheetGeneratePage = () => {
const payload = { const payload = {
applicationId: applicationId || "", applicationId: applicationId || "",
pageId: "", pageId: "",
columns: columns: sheetData?.length > 0 ? Object.keys(sheetData[0]) : [],
!!currentSheetData && currentSheetData.length > 0
? Object.keys(currentSheetData[0])
: [],
searchColumn: "", searchColumn: "",
tableName: selectedSheet?.value || "", tableName: selectedSheet?.value || "",
datasourceId: props.datasourceId || "", datasourceId: props.datasourceId || "",
@ -312,76 +343,144 @@ function GoogleSheetSchema(props: Props) {
pagePermissions, pagePermissions,
); );
const refreshSpreadSheetButton = (option: DropdownOption) => (
<Button
isIconButton
kind="tertiary"
onClick={() => selectSpreadsheetAndToggle(option)}
startIcon="refresh"
/>
);
const showGeneratePageBtn = const showGeneratePageBtn =
!isLoading && !isLoading &&
!isError && !isError &&
currentSheetData && sheetData?.length &&
canCreateDatasourceActions && canCreateDatasourceActions &&
canCreatePages; canCreatePages;
return ( const filteredSpreadsheets = spreadsheetOptions.filter(
<> (option) => (option.label || "").toLowerCase()?.includes(searchString),
<SelectContainer>
{!!props.datasourceId ? (
<SelectListWrapper>
<Text>Spreadsheet</Text>
<SelectWrapper width={DROPDOWN_DIMENSION.WIDTH}>
<Select
data-testid="t--table-dropdown"
isLoading={isFetchingSpreadsheets}
onChange={(value: any) =>
onSelectSpreadsheet(
value,
datasourceTableOptions.find(
(table) => table.value === value,
) as DatasourceTableDropdownOption,
)
}
value={selectedSpreadsheet}
>
{datasourceTableOptions.map((table) => {
return (
<Option key={table.value} value={table.value}>
{table.label}
</Option>
); );
})}
</Select> return (
</SelectWrapper> <ViewModeSchemaContainer>
</SelectListWrapper> <DataWrapperContainer>
) : null} <StructureContainer>
{selectedSpreadsheet.value ? ( {datasource && (
<SelectListWrapper> <DatasourceStructureHeader
<Text>Sheet</Text> datasource={datasource}
<SelectWrapper width={DROPDOWN_DIMENSION.WIDTH}> paddingBottom
<Select refetchFn={refetchAllSpreadsheets}
data-testid="t--sheetName-dropdown" />
isLoading={isFetchingSheetsList} )}
onChange={(value: any) => <DatasourceListContainer className="t--gsheet-structure">
onSelectSheetOption( {!isFetchingSpreadsheets && (
value, <DatasourceStructureSearchContainer className="t--gsheet-search-container">
sheetsList.find( <SearchInput
(sheet: DropdownOption) => sheet.value === value, className="datasourceStructure-search"
endIcon="close"
onChange={(value) => handleSearch(value)}
placeholder={createMessage(GSHEET_SEARCH_PLACEHOLDER)}
size={"sm"}
startIcon="search"
type="text"
/>
</DatasourceStructureSearchContainer>
)}
<div className="t--gsheet-structure-list">
{isFetchingSpreadsheets ? (
<ItemLoadingIndicator type="SPREADSHEET" />
) : (
filteredSpreadsheets.map((spreadsheet) => {
return (
<Entity
className="t--spreadsheet-structure"
customAddButton={refreshSpreadSheetButton(spreadsheet)}
entityId={`${datasource?.id}-${spreadsheet.value}`}
icon={null}
key={`${datasource?.id}-${spreadsheet.value}`}
name={spreadsheet.label as string}
onToggle={(isOpen) => {
isOpen &&
onSelectSpreadsheet(spreadsheet.value, spreadsheet);
}}
showAddButton={
spreadsheet.value === selectedSpreadsheet.value
}
step={0}
>
{isFetchingSheetsList ? (
<ItemLoadingIndicator type="SHEET" />
) : sheetOptions.length > 0 ? (
sheetOptions.map((sheet) => (
<Entity
className={`t--sheet-structure ${
sheet.value === selectedSheet.value
? "t--sheet-structure-active"
: ""
}`}
entityId={`${datasource?.id}-${selectedSpreadsheet.value}-${sheet.value}`}
icon={null}
key={`${datasource?.id}-${selectedSpreadsheet.value}-${sheet.value}`}
name={sheet.label as string}
onToggle={(isOpen) => {
isOpen && onSelectSheetOption(sheet.value, sheet);
}}
step={1}
>
{selectedSheet.value === sheet.value ? (
isFetchingSheetData ? (
<ItemLoadingIndicator type="DATA" />
) : sheetData?.length > 0 ? (
<DatasourceAttributesWrapper>
{Object.keys(sheetData[0]).map(
(fieldValue, index) => (
<DatasourceField
field={{
name: fieldValue,
type: "string",
}}
key={`${fieldValue}${index}`}
step={2}
/>
), ),
) )}
} </DatasourceAttributesWrapper>
value={selectedSheet} ) : null
> ) : (
{sheetsList.map((sheet) => { <ItemLoadingIndicator type="DATA" />
return ( )}
<Option key={sheet.label} value={sheet.label}> </Entity>
{sheet?.label} ))
</Option> ) : (
<ItemLoadingIndicator type="SPREADSHEET" />
)}
</Entity>
); );
})} })
</Select> )}
</SelectWrapper> </div>
</SelectListWrapper> </DatasourceListContainer>
) : null} </StructureContainer>
<DatasourceDataContainer>
<TableWrapper>
{isLoading ? (
<RenderInterimDataState state="LOADING" />
) : isError ? (
<RenderInterimDataState state="FAILED" />
) : sheetData?.length > 0 ? (
<Table data={sheetData} shouldResize={false} />
) : (
<RenderInterimDataState state="NODATA" />
)}
</TableWrapper>
</DatasourceDataContainer>
</DataWrapperContainer>
{showGeneratePageBtn && ( {showGeneratePageBtn && (
<ButtonContainer>
<Button <Button
className="t--datasource-generate-page" className="t--datasource-generate-page"
isDisabled={!currentSheetData || currentSheetData?.length == 0}
key="datasource-generate-page" key="datasource-generate-page"
kind="secondary" kind="secondary"
onClick={onGsheetGeneratePage} onClick={onGsheetGeneratePage}
@ -389,29 +488,9 @@ function GoogleSheetSchema(props: Props) {
> >
{createMessage(DATASOURCE_GENERATE_PAGE_BUTTON)} {createMessage(DATASOURCE_GENERATE_PAGE_BUTTON)}
</Button> </Button>
</ButtonContainer>
)} )}
</SelectContainer> </ViewModeSchemaContainer>
<TableWrapper>
{isLoading ? (
<MessageWrapper>
<Spinner size="md" />
<Text style={{ marginLeft: "8px" }}>
{createMessage(FETCHING_DATASOURCE_PREVIEW_DATA)}
</Text>
</MessageWrapper>
) : isError ? (
<Text color="var(--ads-color-red-500)">
{createMessage(ERR_FETCHING_DATASOURCE_PREVIEW_DATA)}
</Text>
) : currentSheetData?.length > 0 ? (
<Table data={currentSheetData} />
) : (
<MessageWrapper>
<Text>{createMessage(SCHEMA_PREVIEW_NO_DATA)}</Text>
</MessageWrapper>
)}
</TableWrapper>
</>
); );
} }

View File

@ -0,0 +1,35 @@
import React from "react";
import {
createMessage,
GSHEET_DATA_LOADING,
GSHEET_SHEET_LOADING,
GSHEET_SPREADSHEET_LOADING,
LOADING_SCHEMA,
} from "@appsmith/constants/messages";
import { Spinner, Text } from "design-system";
import { MessageWrapper } from "./SchemaViewModeCSS";
type LoadingItemType = "SPREADSHEET" | "SHEET" | "DATA" | "SCHEMA";
const ItemLoadingIndicator = ({ type }: { type: LoadingItemType }) => {
return (
<MessageWrapper
className={`t--item-loading-indicator t--item-loading-indicator--${type.toLowerCase()}`}
>
<Spinner size="md" />
<Text style={{ marginLeft: "8px" }}>
{createMessage(
type === "SPREADSHEET"
? GSHEET_SPREADSHEET_LOADING
: type === "SHEET"
? GSHEET_SHEET_LOADING
: type === "SCHEMA"
? LOADING_SCHEMA
: GSHEET_DATA_LOADING,
)}
</Text>
</MessageWrapper>
);
};
export default ItemLoadingIndicator;

View File

@ -0,0 +1,61 @@
import React from "react";
import EmptyTableSVG from "assets/images/empty-table-in-display-preview.svg";
import { Spinner, Text } from "design-system";
import {
EMPTY_TABLE_TITLE_TEXT,
EMPTY_TABLE_MESSAGE_TEXT,
createMessage,
EMPTY_TABLE_SVG_ALT_TEXT,
LOADING_RECORDS_MESSAGE_TEXT,
LOADING_RECORDS_TITLE_TEXT,
ERR_FETCHING_DATASOURCE_PREVIEW_DATA,
} from "@appsmith/constants/messages";
import { MessageWrapper, SchemaStateMessageWrapper } from "./SchemaViewModeCSS";
type InterimState = "LOADING" | "NODATA" | "FAILED";
interface RenderInterimDataStateProps {
state: InterimState;
}
const RenderInterimDataState = ({ state }: RenderInterimDataStateProps) => {
return (
<MessageWrapper>
<SchemaStateMessageWrapper>
{state === "NODATA" ? (
<>
{/* Render empty table image */}
<img
alt={createMessage(EMPTY_TABLE_SVG_ALT_TEXT)}
src={EmptyTableSVG}
/>
{/* Show description below the image */}
{/* Show title */}
<Text style={{ fontWeight: "bold" }}>
{createMessage(EMPTY_TABLE_TITLE_TEXT)}
</Text>
{/* Show description */}
<Text>{createMessage(EMPTY_TABLE_MESSAGE_TEXT)}</Text>
</>
) : state === "FAILED" ? (
<Text color="var(--ads-color-red-500)">
{createMessage(ERR_FETCHING_DATASOURCE_PREVIEW_DATA)}
</Text>
) : state === "LOADING" ? (
<>
{/* Show spinner */}
<Spinner size="md" />
{/* Show title */}
<Text style={{ fontWeight: "bold" }}>
{createMessage(LOADING_RECORDS_TITLE_TEXT)}
</Text>
{/* Show description */}
<Text>{createMessage(LOADING_RECORDS_MESSAGE_TEXT)}</Text>
</>
) : null}
</SchemaStateMessageWrapper>
</MessageWrapper>
);
};
export default RenderInterimDataState;

View File

@ -0,0 +1,212 @@
import styled from "styled-components";
export const ViewModeSchemaContainer = styled.div`
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
`;
export const DataWrapperContainer = styled.div`
display: flex;
flex: 1;
overflow: hidden;
.t--datasource-column:hover {
background: none;
cursor: default;
}
`;
export const StructureContainer = styled.div`
height: 100%;
width: 25%;
padding: var(--ads-v2-spaces-4) 0 var(--ads-v2-spaces-4)
var(--ads-v2-spaces-5);
display: flex;
flex-direction: column;
overflow: hidden;
border-right: 1px solid var(--ads-v2-color-gray-300);
flex-shrink: 0;
& > .datasourceStructure-header {
padding: 0 var(--ads-v2-spaces-5);
}
`;
export const DatasourceDataContainer = styled.div`
height: 100%;
width: 75%;
display: flex;
flex-direction: column;
`;
export const DatasourceListContainer = styled.div`
height: 100%;
display: flex;
flex-direction: column;
padding-left: var(--ads-v2-spaces-5);
.t--entity.datasourceStructure-datasource-view-mode {
padding-right: var(--ads-v2-spaces-5);
}
&.t--gsheet-structure {
padding-bottom: var(--ads-v2-spaces-8);
.t--gsheet-structure-list {
overflow-y: auto;
flex-shrink: 1;
flex-grow: 0;
scrollbar-gutter: stable;
}
.t--spreadsheet-structure > .t--entity-item {
font-weight: var(--ads-v2-font-weight-bold);
height: 36px;
padding-left: 0;
& .ads-v2-button__content:hover {
background: none;
}
}
.t--spreadsheet-structure {
padding-right: var(--ads-spaces-3);
flex-grow: 0;
flex-shrink: 0;
}
.t--sheet-structure:not(:last-of-type) {
padding-bottom: var(--ads-spaces-3);
}
.t--sheet-structure .t--datasource-column {
padding-top: var(--ads-spaces-1);
padding-bottom: var(--ads-spaces-1);
}
.t--sheet-structure > .t--entity-item {
color: var(--ads-v2-color-gray-600);
height: 36px;
padding-left: var(--ads-spaces-10);
}
.t--sheet-structure .t--datasource-column {
color: var(--ads-v2-color-gray-600);
height: 30px;
padding-left: var(--ads-spaces-10);
}
.t--sheet-structure.t--sheet-structure-active > .t--entity-item {
background: var(--ads-v2-color-gray-100);
}
.t--sheet-structure .t--entity-name {
padding: var(--ads-spaces-2) 0;
}
}
.t--schema-virtuoso-container {
height: 100%;
}
`;
export const DatasourceAttributesWrapper = styled.div`
padding: var(--ads-spaces-1) 0 var(--ads-spaces-1) var(--ads-spaces-10);
.t--datasource-column > div > div:first-of-type {
padding-right: var(--ads-spaces-3);
}
.t--datasource-column > div > div:last-of-type {
flex-shrink: 0;
flex-grow: 0;
}
`;
export const ButtonContainer = styled.div`
display: flex;
flex-shrink: 0;
justify-content: flex-end;
border-top: 1px solid var(--ads-v2-color-gray-300);
padding: var(--ads-v2-spaces-4);
`;
export const MessageWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: auto;
&.t--item-loading-indicator {
justify-content: flex-start;
padding: var(--ads-spaces-1) 0 var(--ads-spaces-1) var(--ads-spaces-11);
}
&.t--item-loading-indicator--spreadsheet,
&.t--item-loading-indicator--schema {
padding-left: 0;
}
`;
export const SchemaStateMessageWrapper = styled.div`
width: auto;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
img {
padding-bottom: var(--ads-v2-spaces-7);
}
span:first-child {
padding-bottom: var(--ads-v2-spaces-2);
}
`;
export const TableWrapper = styled.div`
overflow-x: auto;
height: 100%;
&& > div {
width: 100%;
}
&& > ${MessageWrapper} {
width: 100%;
height: 100%;
}
&& .t--table-response {
border: none;
height: 100%;
overflow: hidden;
}
&& .tableWrap {
overflow: auto;
}
& .table {
background: none;
& > div:first-child {
position: sticky;
top: 0;
z-index: 1;
}
& .draggable-header {
cursor: default;
}
}
& .table div:first-of-type .tr {
background: var(--ads-v2-color-black-5);
border-right: none;
border-bottom: 1px solid var(--ads-v2-color-black-75);
}
&& .table div.tbody .tr {
background: var(--ads-v2-color-white);
border-bottom: 1px solid var(--ads-v2-color-black-75);
}
&& .table .td,
&& .table .th {
border-right: none;
border-bottom: none;
}
button {
margin-right: 24px;
}
&& .tableWrap {
}
`;
export const DatasourceStructureSearchContainer = styled.div`
margin: var(--ads-v2-spaces-3) 0 var(--ads-v2-spaces-4) 0;
background: white;
flex-shrink: 0;
padding-right: var(--ads-v2-spaces-5);
&.t--gsheet-search-container {
margin-top: var(--ads-v2-spaces-4);
margin-bottom: var(--ads-v2-spaces-3);
}
&.t--search-container--query-editor {
padding-right: 0;
}
`;

View File

@ -272,7 +272,17 @@ export interface UseSheetDataReturn {
}: FetchSheetData) => void; }: FetchSheetData) => void;
} }
export const useSheetsList = (): UseSheetListReturn => { export interface UseSheetListProps {
setSheetOptions?: (tableOptions: DropdownOptions) => void;
}
export interface UseSheetDataProps {
setSheetData?: (tableOptions: DropdownOptions) => void;
}
export const useSheetsList = (
props: UseSheetListProps = {},
): UseSheetListReturn => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [sheetsList, setSheetsList] = useState<DropdownOption[]>([]); const [sheetsList, setSheetsList] = useState<DropdownOption[]>([]);
@ -298,12 +308,13 @@ export const useSheetsList = (): UseSheetListReturn => {
const responseBody = payload.data.trigger; const responseBody = payload.data.trigger;
if (Array.isArray(responseBody)) { if (Array.isArray(responseBody)) {
setSheetsList(responseBody); setSheetsList(responseBody);
props.setSheetOptions && props.setSheetOptions(responseBody);
} else { } else {
// to handle error like "401 Unauthorized" // to handle error like "401 Unauthorized"
} }
} }
}, },
[setSheetsList, setIsFetchingSheetsList], [setSheetsList, setIsFetchingSheetsList, props.setSheetOptions],
); );
const fetchSheetsList = useCallback( const fetchSheetsList = useCallback(
@ -314,6 +325,7 @@ export const useSheetsList = (): UseSheetListReturn => {
selectedSpreadsheetUrl, selectedSpreadsheetUrl,
}: FetchSheetsList) => { }: FetchSheetsList) => {
setSheetsList([]); setSheetsList([]);
props.setSheetOptions && props.setSheetOptions([]);
setIsFetchingSheetsList(true); setIsFetchingSheetsList(true);
setFailedFetchingSheetsList(false); setFailedFetchingSheetsList(false);
const formattedRequestData = { const formattedRequestData = {
@ -343,6 +355,7 @@ export const useSheetsList = (): UseSheetListReturn => {
onFetchAllSheetFailure, onFetchAllSheetFailure,
setIsFetchingSheetsList, setIsFetchingSheetsList,
setFailedFetchingSheetsList, setFailedFetchingSheetsList,
props.setSheetOptions,
], ],
); );
@ -354,7 +367,9 @@ export const useSheetsList = (): UseSheetListReturn => {
}; };
}; };
export const useSheetData = (): UseSheetDataReturn => { export const useSheetData = (
props: UseSheetDataProps = {},
): UseSheetDataReturn => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [sheetData, setSheetData] = useState<any>([]); const [sheetData, setSheetData] = useState<any>([]);
@ -380,12 +395,13 @@ export const useSheetData = (): UseSheetDataReturn => {
const responseBody = payload.data.trigger; const responseBody = payload.data.trigger;
if (Array.isArray(responseBody)) { if (Array.isArray(responseBody)) {
setSheetData(responseBody); setSheetData(responseBody);
props.setSheetData && props.setSheetData(responseBody);
} else { } else {
// to handle error like "401 Unauthorized" // to handle error like "401 Unauthorized"
} }
} }
}, },
[setSheetData, setIsFetchingSheetData], [setSheetData, setIsFetchingSheetData, props.setSheetData],
); );
const fetchSheetData = useCallback( const fetchSheetData = useCallback(
@ -396,6 +412,7 @@ export const useSheetData = (): UseSheetDataReturn => {
selectedSpreadsheetUrl, selectedSpreadsheetUrl,
}: FetchSheetData) => { }: FetchSheetData) => {
setSheetData([]); setSheetData([]);
props.setSheetData && props.setSheetData([]);
setIsFetchingSheetData(true); setIsFetchingSheetData(true);
setFailedFetchingSheetData(false); setFailedFetchingSheetData(false);
const formattedRequestData = { const formattedRequestData = {
@ -428,6 +445,7 @@ export const useSheetData = (): UseSheetDataReturn => {
onFetchAllSheetFailure, onFetchAllSheetFailure,
setIsFetchingSheetData, setIsFetchingSheetData,
setFailedFetchingSheetData, setFailedFetchingSheetData,
props.setSheetData,
], ],
); );

View File

@ -17,6 +17,7 @@ import type { Theme } from "constants/DefaultTheme";
interface TableProps { interface TableProps {
data: Record<string, any>[]; data: Record<string, any>[];
tableBodyHeight?: number; tableBodyHeight?: number;
shouldResize?: boolean;
} }
const TABLE_SIZES = { const TABLE_SIZES = {
@ -208,6 +209,7 @@ export const getScrollBarWidth = (tableBodyEle: any, scrollBarW: number) => {
function Table(props: TableProps) { function Table(props: TableProps) {
const theme = useTheme() as Theme; const theme = useTheme() as Theme;
const tableBodyRef = React.useRef<HTMLElement>(); const tableBodyRef = React.useRef<HTMLElement>();
const { shouldResize = true } = props;
const data = React.useMemo(() => { const data = React.useMemo(() => {
/* Check for length greater than 0 of rows returned from the query for mappings keys */ /* Check for length greater than 0 of rows returned from the query for mappings keys */
@ -346,12 +348,14 @@ function Table(props: TableProps) {
> >
<AutoToolTipComponent title={column.render("Header")}> <AutoToolTipComponent title={column.render("Header")}>
{column.render("Header")} {column.render("Header")}
{shouldResize && (
<div <div
{...column.getResizerProps()} {...column.getResizerProps()}
className={`resizer ${ className={`resizer ${
column.isResizing ? "isResizing" : "" column.isResizing ? "isResizing" : ""
}`} }`}
/> />
)}
</AutoToolTipComponent> </AutoToolTipComponent>
</div> </div>
</div> </div>

View File

@ -90,7 +90,7 @@ import {
} from "@appsmith/utils/BusinessFeatures/permissionPageHelpers"; } from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import { selectFeatureFlagCheck } from "@appsmith/selectors/featureFlagsSelectors"; import { selectFeatureFlagCheck } from "@appsmith/selectors/featureFlagsSelectors";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import GoogleSheetSchema from "../DatasourceInfo/GoogleSheetSchema"; import DatasourceTabs from "../DatasourceInfo/DatasorceTabs";
const ViewModeContainer = styled.div` const ViewModeContainer = styled.div`
display: flex; display: flex;
@ -468,13 +468,77 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
} }
renderDatasourceInfo = () => { renderDatasourceInfo = () => {
const { datasource, formConfig, viewMode } = this.props; const {
datasource,
formConfig,
formData,
isPluginAuthFailed,
isPluginAuthorized,
pageId,
plugin,
pluginPackageName,
viewMode,
} = this.props;
const isGoogleSheetPlugin = isGoogleSheetPluginDS(pluginPackageName);
const authErrorMessage = getDatasourceErrorMessage(
formData,
plugin,
this.state.filterParams.id,
);
const hideDatasourceSection =
isGoogleSheetPlugin &&
!isPluginAuthorized &&
authErrorMessage == GSHEET_AUTHORIZATION_ERROR;
return ( return (
<ViewModeWrapper data-testid="t--ds-review-section">
{datasource &&
isGoogleSheetPlugin &&
isPluginAuthFailed &&
datasource.id !== TEMP_DATASOURCE_ID && (
<AuthMessage
actionType={ActionType.AUTHORIZE}
datasource={datasource}
description={authErrorMessage}
isInViewMode
pageId={pageId}
/>
)}
{!isNil(formConfig) && !isNil(datasource) && !hideDatasourceSection && (
<DatasourceInformation <DatasourceInformation
config={formConfig[0]} config={formConfig[0]}
datasource={datasource} datasource={datasource}
viewMode={viewMode} viewMode={viewMode}
/> />
)}
</ViewModeWrapper>
);
};
shouldShowTabs = () => {
const { datasource, isPluginAuthorized, pluginPackageName } = this.props;
const isGoogleSheetPlugin = isGoogleSheetPluginDS(pluginPackageName);
const isGoogleSheetSchemaAvailable =
isGoogleSheetPlugin && isPluginAuthorized;
return isGoogleSheetSchemaAvailable && datasource;
};
renderTabsForViewMode = () => {
const { datasource } = this.props;
return (
<ViewModeContainer>
{this.shouldShowTabs() ? (
<DatasourceTabs
configChild={this.renderDatasourceInfo()}
datasource={datasource as Datasource}
/>
) : (
this.renderDatasourceInfo()
)}
</ViewModeContainer>
); );
}; };
@ -486,7 +550,6 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
datasource, datasource,
datasourceButtonConfiguration, datasourceButtonConfiguration,
datasourceId, datasourceId,
formConfig,
formData, formData,
gsheetProjectID, gsheetProjectID,
gsheetToken, gsheetToken,
@ -526,13 +589,8 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
GOOGLE_SHEETS_INFO_BANNER_MESSAGE, GOOGLE_SHEETS_INFO_BANNER_MESSAGE,
); );
const hideDatasourceSection = const showingTabsOnViewMode =
isGoogleSheetPlugin && this.shouldShowTabs() && viewMode && !isInsideReconnectModal;
!isPluginAuthorized &&
authErrorMessage == GSHEET_AUTHORIZATION_ERROR;
const isGoogleSheetSchemaAvailable =
isGoogleSheetPlugin && isPluginAuthorized;
return ( return (
<> <>
@ -546,6 +604,7 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
isDeleting={isDeleting} isDeleting={isDeleting}
isNewDatasource={createFlow} isNewDatasource={createFlow}
isPluginAuthorized={isPluginAuthorized} isPluginAuthorized={isPluginAuthorized}
noBottomBorder={showingTabsOnViewMode}
pluginImage={pluginImage} pluginImage={pluginImage}
pluginName={plugin?.name || ""} pluginName={plugin?.name || ""}
pluginType={plugin?.type || ""} pluginType={plugin?.type || ""}
@ -554,7 +613,11 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
/> />
)} )}
<ResizerMainContainer> <ResizerMainContainer>
<ResizerContentContainer className="saas-form-resizer-content"> <ResizerContentContainer
className={`saas-form-resizer-content ${
showingTabsOnViewMode && "saas-form-resizer-content-show-tabs"
}`}
>
<DSEditorWrapper> <DSEditorWrapper>
<DSDataFilter <DSDataFilter
filterId={this.state.filterParams.id} filterId={this.state.filterParams.id}
@ -601,35 +664,9 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
{""} {""}
</> </>
)} )}
{viewMode && !isInsideReconnectModal && ( {viewMode &&
<ViewModeContainer> !isInsideReconnectModal &&
<ViewModeWrapper> this.renderTabsForViewMode()}
{datasource &&
isGoogleSheetPlugin &&
isPluginAuthFailed ? (
<AuthMessage
actionType={ActionType.AUTHORIZE}
datasource={datasource}
description={authErrorMessage}
isInViewMode
pageId={pageId}
/>
) : null}
{!isNil(formConfig) &&
!isNil(datasource) &&
!hideDatasourceSection
? this.renderDatasourceInfo()
: undefined}
</ViewModeWrapper>
{isGoogleSheetSchemaAvailable && datasource && (
<GoogleSheetSchema
datasourceId={datasourceId}
key={datasourceId}
pluginId={plugin?.id}
/>
)}
</ViewModeContainer>
)}
</Form> </Form>
{/* Render datasource form call-to-actions */} {/* Render datasource form call-to-actions */}
{datasource && ( {datasource && (

View File

@ -622,4 +622,5 @@ export const ColumnWrapper = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
height: 100%; height: 100%;
width: 100%;
`; `;