feat(block): Add Google Sheets block

Update customnode.css and layout.tsx for Google Sign-In integration

This commit adds styles to customnode.css to accommodate the Google Sign-In button. It also updates layout.tsx to wrap the entire application in the GoogleOAuthProvider component, enabling Google Sign-In functionality.
This commit is contained in:
Toran Bruce Richards
2024-07-20 21:21:59 +01:00
parent f833fa3624
commit 401c92fe75
6 changed files with 159 additions and 27 deletions

View File

@@ -14,6 +14,8 @@ import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { GoogleOAuthProvider } from '@react-oauth/google';
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
@@ -67,14 +69,16 @@ export default function RootLayout({
defaultTheme="light"
disableTransitionOnChange
>
<div className="min-h-screen bg-gray-200 text-gray-900">
<NavBar />
<main className="mx-auto p-4">
{children}
</main>
</div>
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!}>
<div className="min-h-screen bg-gray-200 text-gray-900">
<NavBar />
<main className="mx-auto p-4">
{children}
</main>
</div>
</GoogleOAuthProvider>
</ThemeProvider>
</body>
</html>
);
}
}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, FC, memo } from 'react';
import React, { useState, useEffect, FC, memo, useCallback } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import 'reactflow/dist/style.css';
import './customnode.css';
@@ -8,6 +8,8 @@ import { Input } from './ui/input';
import { BlockSchema } from '@/lib/types';
import SchemaTooltip from './SchemaTooltip';
import { beautifyString } from '@/lib/utils';
import GoogleSignInButton from './GoogleSignInButton';
import { GoogleOAuthProvider } from '@react-oauth/google';
type CustomNodeData = {
blockType: string;
@@ -32,6 +34,7 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
const [activeKey, setActiveKey] = useState<string | null>(null);
const [modalValue, setModalValue] = useState<string>('');
const [errors, setErrors] = useState<{ [key: string]: string | null }>({});
const [accessToken, setAccessToken] = useState<string | null>(null);
useEffect(() => {
if (data.output_data || data.status) {
@@ -88,6 +91,15 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
));
};
const handleTokenChange = useCallback((token: string | null) => {
if (token !== data.hardcodedValues['access_token']) {
data.setHardcodedValues({
...data.hardcodedValues,
access_token: token
});
}
}, [data.hardcodedValues, data.setHardcodedValues]);
const handleInputChange = (key: string, value: any) => {
const keys = key.split('.');
const newValues = JSON.parse(JSON.stringify(data.hardcodedValues));
@@ -386,6 +398,13 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
);
}
return null;
case 'oauth2':
return (
<div key={fullKey} className="input-container">
<GoogleSignInButton onTokenChange={handleTokenChange} />
{error && <span className="error-message">{error}</span>}
</div>
);
default:
return (
<div key={fullKey} className="input-container">
@@ -425,30 +444,33 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
</div>
<div className="node-content">
<div className="input-section">
{data.inputSchema &&
Object.entries(data.inputSchema.properties).map(([key, schema]) => {
const isRequired = data.inputSchema.required?.includes(key);
return (isRequired || isAdvancedOpen) && (
<div key={key}>
<div className="handle-container">
<Handle
type="target"
position={Position.Left}
id={key}
style={{ background: '#555', borderRadius: '50%', width: '10px', height: '10px' }}
/>
<span className="handle-label">{schema.title || beautifyString(key)}</span>
<SchemaTooltip schema={schema} />
{data.inputSchema &&
Object.entries(data.inputSchema.properties).map(([key, schema]) => {
const isRequired = data.inputSchema.required?.includes(key);
return (isRequired || isAdvancedOpen) && (
<div key={key}>
<div className="handle-container">
<Handle
type="target"
position={Position.Left}
id={key}
style={{ background: '#555', borderRadius: '50%', width: '10px', height: '10px' }}
/>
<span className="handle-label">{schema.title || beautifyString(key)}</span>
<SchemaTooltip schema={schema} />
</div>
{renderInputField(key, schema, '', schema.title || beautifyString(key))}
</div>
{renderInputField(key, schema, '', schema.title || beautifyString(key))}
</div>
);
})}
);
})}
</div>
<div className="output-section">
{data.outputSchema && generateHandles(data.outputSchema, 'source')}
{data.outputSchema && generateHandles(data.outputSchema, 'source')}
</div>
</div>
<div className="google-signin-container">
<GoogleSignInButton onTokenChange={handleTokenChange} />
</div>
{isOutputOpen && (
<div className="node-output">
<p>

View File

@@ -0,0 +1,33 @@
import React, { useEffect, useCallback } from 'react';
import { Button } from './ui/button';
import { useGoogleAuth } from '@/hooks/useGoogleAuth';
interface GoogleSignInButtonProps {
onTokenChange: (token: string | null) => void;
}
const GoogleSignInButton: React.FC<GoogleSignInButtonProps> = React.memo(({ onTokenChange }) => {
const { token, error, login, logout } = useGoogleAuth('https://www.googleapis.com/auth/spreadsheets.readonly');
const handleTokenChange = useCallback((newToken: string | null) => {
onTokenChange(newToken);
}, [onTokenChange]);
useEffect(() => {
handleTokenChange(token);
}, [token, handleTokenChange]);
if (error) {
return <div>Error: {error}</div>;
}
return token ? (
<Button onClick={logout}>Sign Out of Google Sheets</Button>
) : (
<Button onClick={() => login()}>Sign in with Google Sheets</Button>
);
});
GoogleSignInButton.displayName = 'GoogleSignInButton';
export default GoogleSignInButton;

View File

@@ -146,6 +146,11 @@
padding: 5px 10px;
}
.custom-node .google-signin-container {
margin-top: 10px;
padding: 0 10px;
}
.array-item-add {
background: #5bc0de;
border: none;

View File

@@ -0,0 +1,24 @@
import { useGoogleLogin } from '@react-oauth/google';
import { useState, useCallback } from 'react';
export const useGoogleAuth = (scope: string) => {
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const login = useGoogleLogin({
onSuccess: (tokenResponse) => {
setToken(tokenResponse.access_token);
setError(null);
},
onError: (errorResponse) => {
setError(errorResponse.error_description || 'An error occurred during login');
},
scope: scope,
});
const logout = useCallback(() => {
setToken(null);
}, []);
return { token, error, login, logout };
};

View File

@@ -0,0 +1,44 @@
from autogpt_server.data.block import Block, BlockSchema, BlockOutput
from autogpt_server.data.model import SchemaField
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
class GoogleSheetsBlock(Block):
class Input(BlockSchema):
spreadsheet_id: str = SchemaField(
description="The ID of the Google Sheet to read",
placeholder="1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"
)
range: str = SchemaField(
description="The A1 notation of the range to read",
placeholder="Sheet1!A1:E10",
default= "Sheet1!A1:Z1000"
)
access_token: str = SchemaField(
description="Google OAuth2 access token",
secret=True
)
class Output(BlockSchema):
rows: list = SchemaField(description="The rows of from the specified Google Sheet range")
def __init__(self):
super().__init__(
id="google-sheets-block",
input_schema=GoogleSheetsBlock.Input,
output_schema=GoogleSheetsBlock.Output,
)
def run(self, input_data: Input) -> BlockOutput:
credentials = Credentials(input_data.access_token)
service = build("sheets", "v4", credentials=credentials)
sheet = service.spreadsheets()
result = sheet.values().get(spreadsheetId=input_data.spreadsheet_id, range=input_data.range).execute()
values = result.get("values", [])
if not values:
yield "data", {"error": "No data found."}
else:
headers = values[0]
data = [dict(zip(headers, row)) for row in values[1:]]
yield "rows", data