@fils/sanity-components 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HOSTED.md ADDED
@@ -0,0 +1,205 @@
1
+ # Sanity Dashboard Widgets
2
+
3
+ Collection of custom Sanity Studio dashboard widgets for deployment automation.
4
+
5
+ ## Components
6
+
7
+ ### DeployButton
8
+ A deployment button that triggers GitHub Actions workflows and monitors their status.
9
+
10
+ **Features:**
11
+ - Triggers GitHub Actions via repository dispatch
12
+ - Real-time workflow status monitoring
13
+ - Fast polling (3-second intervals)
14
+ - Retry logic for workflow detection
15
+ - Success/error state handling
16
+ - Links to live deployment
17
+
18
+ **Props:**
19
+ ```typescript
20
+ interface DeployButtonProps {
21
+ config: {
22
+ owner: string; // GitHub org/user
23
+ repo: string; // Repository name
24
+ token?: string; // Optional: hardcoded token (use secrets instead)
25
+ branch?: string; // Default: 'main'
26
+ eventType?: string; // Default: 'deploy-site'
27
+ };
28
+ title?: string; // Default: '🚀 Site Deployment'
29
+ deploymentUrl?: string; // Optional: URL to deployed site
30
+ environment?: string; // Default: 'production'
31
+ secretsNamespace?: string; // Default: 'deployButton'
32
+ secretKey?: string; // Default: 'github_token'
33
+ }
34
+ ```
35
+
36
+ ### GitHubTokenConfig
37
+ Configuration widget for managing GitHub tokens (dev-only).
38
+
39
+ **Props:**
40
+ ```typescript
41
+ interface TokenConfigProps {
42
+ title?: string; // Default: '🔐 GitHub Token Configuration'
43
+ description?: string; // Custom description text
44
+ secretsNamespace?: string; // Default: 'deployButton'
45
+ secretKey?: string; // Default: 'github_token'
46
+ environment?: string; // Default: 'production'
47
+ }
48
+ ```
49
+
50
+ ## Environment Utilities
51
+
52
+ **envUtils.ts** provides helpers for conditional rendering:
53
+
54
+ ```typescript
55
+ import { isDevelopment, isProduction, withDevOnly } from './envUtils';
56
+
57
+ // Check environment
58
+ if (isDevelopment()) {
59
+ // Show dev tools
60
+ }
61
+
62
+ // Wrap components
63
+ const DevOnlyWidget = withDevOnly(GitHubTokenConfig);
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ ### Basic Setup
69
+
70
+ 1. **Install dependencies:**
71
+ ```bash
72
+ npm install @sanity/dashboard @sanity/studio-secrets
73
+ ```
74
+
75
+ 2. **Add to your dashboard config:**
76
+
77
+ ```typescript
78
+ import { isDevelopment } from './envUtils';
79
+ import DeployButton from './DeployButton';
80
+ import GitHubTokenConfig from './GitHubTokenConfig';
81
+
82
+ export default {
83
+ widgets: [
84
+ // Production deploy button (always visible)
85
+ {
86
+ name: 'deploy-prod',
87
+ component: DeployButton,
88
+ options: {
89
+ config: {
90
+ owner: 'your-org',
91
+ repo: 'your-repo',
92
+ branch: 'main'
93
+ },
94
+ title: '🚀 Deploy to Production',
95
+ deploymentUrl: 'https://your-site.com',
96
+ environment: 'production'
97
+ }
98
+ },
99
+
100
+ // Token config (dev only)
101
+ ...(isDevelopment() ? [
102
+ {
103
+ name: 'github-config',
104
+ component: GitHubTokenConfig
105
+ }
106
+ ] : [])
107
+ ]
108
+ };
109
+ ```
110
+
111
+ ### GitHub Setup
112
+
113
+ 1. **Create a Personal Access Token:**
114
+ - Go to GitHub Settings → Developer settings → Personal access tokens
115
+ - Generate new token (classic)
116
+ - Select scopes: `repo` (for private repos) or `public_repo` (for public)
117
+ - Copy the token
118
+
119
+ 2. **Configure in Sanity:**
120
+ - In local dev, the GitHubTokenConfig widget will appear
121
+ - Paste your token
122
+ - Save
123
+
124
+ 3. **Create GitHub Workflow:**
125
+
126
+ ```yaml
127
+ # .github/workflows/deploy.yml
128
+ name: Deploy Site
129
+
130
+ on:
131
+ repository_dispatch:
132
+ types: [deploy-site]
133
+
134
+ jobs:
135
+ deploy:
136
+ runs-on: ubuntu-latest
137
+ steps:
138
+ - uses: actions/checkout@v3
139
+
140
+ - name: Deploy to AWS
141
+ run: |
142
+ # Your deployment script
143
+ echo "Deploying to ${{ github.event.client_payload.environment }}"
144
+ # ... your AWS deploy commands
145
+ ```
146
+
147
+ ### Conditional Dev Tools
148
+
149
+ Hide sensitive tools from clients:
150
+
151
+ ```typescript
152
+ const dashboardConfig = {
153
+ widgets: [
154
+ // Always visible
155
+ { name: 'deploy', component: DeployButton, options: {...} },
156
+
157
+ // Dev only
158
+ ...(isDevelopment() ? [
159
+ { name: 'token-config', component: GitHubTokenConfig },
160
+ { name: 'vision', component: VisionTool },
161
+ { name: 'debug-panel', component: DebugPanel }
162
+ ] : [])
163
+ ]
164
+ };
165
+ ```
166
+
167
+ ## Environment Detection
168
+
169
+ The `isDevelopment()` function checks:
170
+ 1. `window.location.hostname` for localhost/127.0.0.1/192.168.x.x
171
+ 2. `process.env.NODE_ENV === 'development'`
172
+
173
+ This means:
174
+ - ✅ Local dev: widgets visible
175
+ - ❌ Production deploy: widgets hidden
176
+ - ❌ Client access: widgets hidden
177
+
178
+ ## Security Notes
179
+
180
+ - **Never commit tokens** to your repository
181
+ - Use Sanity Studio Secrets to store tokens securely
182
+ - Tokens are stored in your Sanity project dataset
183
+ - Each user can configure their own token in local dev
184
+ - Production deployments should use GitHub Actions secrets, not personal tokens
185
+
186
+ ## Troubleshooting
187
+
188
+ **"Could not find workflow run"**
189
+ - Check that your workflow file exists
190
+ - Verify the `eventType` matches your workflow's `repository_dispatch.types`
191
+ - Ensure the token has correct permissions
192
+
193
+ **"GitHub API delays"**
194
+ - The GitHub Actions API can lag 10-30 seconds behind the UI
195
+ - This is normal and expected
196
+ - The widget polls every 3 seconds for updates
197
+
198
+ **"CORS errors"**
199
+ - Make sure you're not adding custom cache headers
200
+ - GitHub's API has strict CORS policies
201
+ - The widget handles this correctly
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Fil's Hosted Deploy Button
3
+ * Dependencies: React, @sanity/ui, @sanity/icons, @sanity/studio-secrets
4
+ * Recommended to use inside the dashboard
5
+ * Uses studio secrets to store Github token
6
+ */
7
+ interface DeployButtonConfig {
8
+ owner: string;
9
+ repo: string;
10
+ token?: string;
11
+ branch?: string;
12
+ eventType?: string;
13
+ }
14
+ interface DeployButtonProps {
15
+ config: DeployButtonConfig;
16
+ title?: string;
17
+ deploymentUrl?: string;
18
+ environment?: string;
19
+ secretsNamespace?: string;
20
+ secretKey?: string;
21
+ }
22
+ declare const DeployButton: ({ config, title, deploymentUrl, environment, secretsNamespace, secretKey }: DeployButtonProps) => import("react/jsx-runtime").JSX.Element;
23
+ export default DeployButton;
@@ -0,0 +1,246 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useRef } from 'react';
3
+ import { Button, Card, Stack, Text, Spinner, Flex } from '@sanity/ui';
4
+ import { PlayIcon, CheckmarkIcon, CloseIcon } from '@sanity/icons';
5
+ import { useSecrets, SettingsView } from '@sanity/studio-secrets';
6
+ const DeployButton = ({ config, title = "🚀 Site Deployment", deploymentUrl, environment = "production", secretsNamespace = "deployButton", secretKey = "github_token" }) => {
7
+ // ... rest of code
8
+ // ALL HOOKS MUST BE CALLED FIRST - NEVER CONDITIONALLY
9
+ const [deployState, setDeployState] = useState('idle');
10
+ const [message, setMessage] = useState('');
11
+ const [showSettings, setShowSettings] = useState(false);
12
+ const intervalRef = useRef(null);
13
+ const monitoringRef = useRef(false);
14
+ // Use Sanity secrets - ALWAYS call this hook
15
+ const { secrets } = useSecrets(secretsNamespace);
16
+ const token = secrets?.[secretKey];
17
+ // Get the effective token (secrets take priority over config)
18
+ const effectiveToken = token || config?.token;
19
+ // Only show settings if we don't have a token AND we have valid config
20
+ useEffect(() => {
21
+ if (!effectiveToken && config?.owner && config?.repo && !showSettings) {
22
+ setShowSettings(true);
23
+ }
24
+ else if (effectiveToken && showSettings) {
25
+ // Close settings if we now have a token
26
+ setShowSettings(false);
27
+ }
28
+ }, [effectiveToken, config?.owner, config?.repo, showSettings]);
29
+ // Clean up on unmount
30
+ useEffect(() => {
31
+ return () => {
32
+ if (intervalRef.current) {
33
+ clearInterval(intervalRef.current);
34
+ intervalRef.current = null;
35
+ }
36
+ };
37
+ }, []);
38
+ // Plugin config for secrets
39
+ const pluginConfigKeys = [
40
+ {
41
+ key: secretKey,
42
+ title: `GitHub Token for ${environment}`,
43
+ description: `Personal access token for ${config?.owner}/${config?.repo} deployment`,
44
+ type: 'string',
45
+ inputType: 'password'
46
+ }
47
+ ];
48
+ // Poll workflow status
49
+ const pollWorkflowStatus = async (runId) => {
50
+ try {
51
+ console.log('Polling workflow:', runId);
52
+ const cacheBuster = `_=${Date.now()}`;
53
+ const response = await fetch(`https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs/${runId}?${cacheBuster}`, {
54
+ headers: {
55
+ 'Authorization': `token ${effectiveToken}`,
56
+ 'Accept': 'application/vnd.github.v3+json'
57
+ }
58
+ });
59
+ if (!response.ok) {
60
+ throw new Error(`HTTP ${response.status}`);
61
+ }
62
+ const run = await response.json();
63
+ console.log('Run status:', run.status, 'conclusion:', run.conclusion);
64
+ if (run.status === 'completed') {
65
+ // Stop polling
66
+ if (intervalRef.current) {
67
+ clearInterval(intervalRef.current);
68
+ intervalRef.current = null;
69
+ }
70
+ monitoringRef.current = false;
71
+ if (run.conclusion === 'success') {
72
+ setDeployState('success');
73
+ setMessage('🎉 Deployment completed successfully!');
74
+ }
75
+ else {
76
+ setDeployState('error');
77
+ setMessage(`❌ Deployment failed: ${run.conclusion}`);
78
+ }
79
+ }
80
+ else if (run.status === 'in_progress' || run.status === 'queued') {
81
+ const startTime = new Date(run.run_started_at || run.created_at);
82
+ const elapsed = Math.floor((Date.now() - startTime.getTime()) / 1000);
83
+ setDeployState('deploying');
84
+ setMessage(`🔄 Deployment running... (${elapsed}s)`);
85
+ }
86
+ }
87
+ catch (error) {
88
+ console.error('Polling error:', error);
89
+ }
90
+ };
91
+ // Find the workflow run with retry logic
92
+ const findWorkflowRun = async (retryCount = 0) => {
93
+ const maxRetries = 6; // Try for 30 seconds (5s * 6)
94
+ try {
95
+ console.log(`Looking for workflow run... (attempt ${retryCount + 1}/${maxRetries})`);
96
+ const cacheBuster = `_=${Date.now()}`;
97
+ const response = await fetch(`https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs?per_page=10&${cacheBuster}`, {
98
+ headers: {
99
+ 'Authorization': `token ${effectiveToken}`,
100
+ 'Accept': 'application/vnd.github.v3+json'
101
+ }
102
+ });
103
+ if (!response.ok) {
104
+ throw new Error(`HTTP ${response.status}`);
105
+ }
106
+ const data = await response.json();
107
+ console.log('Found runs:', data.workflow_runs.length);
108
+ // Find the most recent run (within last 90 seconds)
109
+ const recentRun = data.workflow_runs.find(run => {
110
+ const ageInSeconds = (Date.now() - new Date(run.created_at).getTime()) / 1000;
111
+ console.log(`Run ${run.id}: age ${ageInSeconds}s, event: ${run.event}, status: ${run.status}`);
112
+ return ageInSeconds < 90;
113
+ });
114
+ if (recentRun) {
115
+ console.log('Found recent run:', recentRun.id, 'Event:', recentRun.event);
116
+ setDeployState('deploying');
117
+ setMessage('🚀 Deployment started...');
118
+ // Start polling every 3 seconds (faster response)
119
+ intervalRef.current = setInterval(() => {
120
+ pollWorkflowStatus(recentRun.id);
121
+ }, 3000);
122
+ // Initial poll
123
+ pollWorkflowStatus(recentRun.id);
124
+ }
125
+ else if (retryCount < maxRetries) {
126
+ // Retry after 5 seconds
127
+ console.log(`Workflow not found yet, retrying in 5s... (${retryCount + 1}/${maxRetries})`);
128
+ setMessage(`⏳ Waiting for workflow to start... (${retryCount + 1}/${maxRetries})`);
129
+ setTimeout(() => {
130
+ findWorkflowRun(retryCount + 1);
131
+ }, 5000);
132
+ }
133
+ else {
134
+ console.log('No recent workflow found after retries');
135
+ setDeployState('error');
136
+ setMessage('❌ Could not find workflow run - check if workflow exists');
137
+ monitoringRef.current = false;
138
+ }
139
+ }
140
+ catch (error) {
141
+ console.error('Error finding workflow:', error);
142
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
143
+ if (retryCount < maxRetries) {
144
+ console.log(`Error finding workflow, retrying... (${retryCount + 1}/${maxRetries})`);
145
+ setTimeout(() => {
146
+ findWorkflowRun(retryCount + 1);
147
+ }, 5000);
148
+ }
149
+ else {
150
+ setDeployState('error');
151
+ setMessage(`❌ Error finding workflow: ${errorMessage}`);
152
+ monitoringRef.current = false;
153
+ }
154
+ }
155
+ };
156
+ const triggerDeploy = async () => {
157
+ // Prevent multiple deployments
158
+ if (deployState !== 'idle' && deployState !== 'success' && deployState !== 'error')
159
+ return;
160
+ setDeployState('triggering');
161
+ setMessage('⚡ Triggering deployment...');
162
+ try {
163
+ console.log('Triggering deployment...');
164
+ const response = await fetch(`https://api.github.com/repos/${config.owner}/${config.repo}/dispatches`, {
165
+ method: 'POST',
166
+ headers: {
167
+ 'Authorization': `token ${effectiveToken}`,
168
+ 'Accept': 'application/vnd.github.v3+json',
169
+ 'Content-Type': 'application/json'
170
+ },
171
+ body: JSON.stringify({
172
+ event_type: config.eventType || 'deploy-site',
173
+ client_payload: {
174
+ environment: environment,
175
+ triggered_by: 'sanity_studio',
176
+ timestamp: new Date().toISOString()
177
+ }
178
+ })
179
+ });
180
+ if (response.ok) {
181
+ console.log('Deployment triggered successfully');
182
+ setMessage('✅ Deployment triggered! Looking for workflow...');
183
+ // Wait 5 seconds then look for the workflow
184
+ setTimeout(() => {
185
+ if (!monitoringRef.current) {
186
+ monitoringRef.current = true;
187
+ findWorkflowRun();
188
+ }
189
+ }, 5000);
190
+ }
191
+ else {
192
+ const error = await response.json();
193
+ throw new Error(error.message || `HTTP ${response.status}`);
194
+ }
195
+ }
196
+ catch (error) {
197
+ console.error('Deploy trigger failed:', error);
198
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
199
+ setDeployState('error');
200
+ setMessage(`❌ Failed to trigger: ${errorMessage}`);
201
+ }
202
+ };
203
+ const getButtonProps = () => {
204
+ switch (deployState) {
205
+ case 'triggering':
206
+ return { tone: 'primary', disabled: true, icon: Spinner, text: 'Triggering...' };
207
+ case 'deploying':
208
+ return { tone: 'primary', disabled: true, icon: Spinner, text: 'Deploying...' };
209
+ case 'success':
210
+ return { tone: 'positive', disabled: false, icon: CheckmarkIcon, text: 'Deploy Again' };
211
+ case 'error':
212
+ return { tone: 'critical', disabled: false, icon: CloseIcon, text: 'Try Again' };
213
+ default:
214
+ return { tone: 'primary', disabled: false, icon: PlayIcon, text: 'Deploy Site' };
215
+ }
216
+ };
217
+ // NOW WE CAN HANDLE CONDITIONAL RENDERING AFTER ALL HOOKS
218
+ // Check for missing config
219
+ if (!config || !config.owner || !config.repo) {
220
+ return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, tone: "critical", children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: "\u274C Deploy Button Configuration Error" }), _jsx(Text, { size: 1, children: "Missing required config: owner and repo are required" })] }) }));
221
+ }
222
+ // If showing settings, render the settings view
223
+ if (showSettings && !effectiveToken) {
224
+ return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, children: _jsxs(Stack, { space: 3, children: [_jsxs(Text, { size: 2, weight: "semibold", children: ["\uD83D\uDD10 ", title, " - Setup Required"] }), _jsx(Text, { size: 1, muted: true, children: "Please configure your GitHub token to enable deployments." }), _jsx(SettingsView, { title: `${title} Configuration`, namespace: secretsNamespace, keys: pluginConfigKeys, onClose: () => {
225
+ setShowSettings(false);
226
+ } })] }) }));
227
+ }
228
+ // Check for missing token after settings
229
+ if (!effectiveToken) {
230
+ return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, tone: "critical", children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: "\u274C Authentication Required" }), _jsx(Text, { size: 1, children: "No GitHub token configured." }), _jsx(Button, { tone: "primary", mode: "ghost", size: 1, onClick: () => setShowSettings(true), children: "Configure Token" })] }) }));
231
+ }
232
+ const buttonProps = getButtonProps();
233
+ return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: title }), _jsx(Flex, { justify: "flex-start", children: _jsx(Button, { tone: buttonProps.tone, disabled: buttonProps.disabled, onClick: triggerDeploy, size: 3, style: {
234
+ width: '100%',
235
+ maxWidth: '360px',
236
+ }, children: _jsxs(Flex, { align: "center", justify: "center", style: {
237
+ gap: '6px',
238
+ padding: '16px 0px 16px 0px'
239
+ }, children: [buttonProps.icon && (_jsx("span", { style: {
240
+ display: 'flex',
241
+ alignItems: 'center',
242
+ fontSize: '16px',
243
+ lineHeight: '1'
244
+ }, children: _jsx(buttonProps.icon, {}) })), _jsx(Text, { size: 2, weight: "medium", style: { lineHeight: '1' }, children: buttonProps.text })] }) }) }), message && (_jsx(Card, { padding: 3, radius: 1, tone: deployState === 'error' ? 'critical' : deployState === 'success' ? 'positive' : 'primary', children: _jsx(Text, { size: 1, children: message }) })), deploymentUrl && deployState === 'success' && (_jsx(Button, { as: "a", href: deploymentUrl, target: "_blank", rel: "noopener noreferrer", tone: "primary", mode: "ghost", size: 3, children: "\uD83C\uDF10 View Live Site" })), _jsxs(Text, { size: 1, muted: true, children: ["Deploys from ", config.branch || 'main', " branch to ", environment] })] }) }));
245
+ };
246
+ export default DeployButton;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Environment Detection Utilities
3
+ * Helper functions to determine if we're in development mode
4
+ */
5
+ /**
6
+ * Check if running in local development
7
+ * @returns true if in development, false otherwise
8
+ */
9
+ export declare const isDevelopment: () => boolean;
10
+ /**
11
+ * Check if running in production
12
+ * @returns true if in production, false otherwise
13
+ */
14
+ export declare const isProduction: () => boolean;
15
+ /**
16
+ * Conditionally render component only in development
17
+ * Usage: withDevOnly(MyComponent)
18
+ */
19
+ export declare const withDevOnly: <P extends object>(Component: React.ComponentType<P>) => React.FC<P>;
20
+ /**
21
+ * Conditionally render component only in production
22
+ * Usage: withProdOnly(MyComponent)
23
+ */
24
+ export declare const withProdOnly: <P extends object>(Component: React.ComponentType<P>) => React.FC<P>;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Environment Detection Utilities
4
+ * Helper functions to determine if we're in development mode
5
+ */
6
+ /**
7
+ * Check if running in local development
8
+ * @returns true if in development, false otherwise
9
+ */
10
+ export const isDevelopment = () => {
11
+ // Check for localhost
12
+ if (typeof window !== 'undefined') {
13
+ const hostname = window.location.hostname;
14
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.');
15
+ }
16
+ // Fallback to NODE_ENV if available
17
+ return process.env.NODE_ENV === 'development';
18
+ };
19
+ /**
20
+ * Check if running in production
21
+ * @returns true if in production, false otherwise
22
+ */
23
+ export const isProduction = () => {
24
+ return !isDevelopment();
25
+ };
26
+ /**
27
+ * Conditionally render component only in development
28
+ * Usage: withDevOnly(MyComponent)
29
+ */
30
+ export const withDevOnly = (Component) => {
31
+ return (props) => {
32
+ if (!isDevelopment()) {
33
+ return null;
34
+ }
35
+ return _jsx(Component, { ...props });
36
+ };
37
+ };
38
+ /**
39
+ * Conditionally render component only in production
40
+ * Usage: withProdOnly(MyComponent)
41
+ */
42
+ export const withProdOnly = (Component) => {
43
+ return (props) => {
44
+ if (!isProduction()) {
45
+ return null;
46
+ }
47
+ return _jsx(Component, { ...props });
48
+ };
49
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * GitHub Token Configuration Widget
3
+ * Only show this in local development environment
4
+ * Dependencies: React, @sanity/ui, @sanity/icons, @sanity/studio-secrets
5
+ */
6
+ interface TokenConfigProps {
7
+ title?: string;
8
+ description?: string;
9
+ secretsNamespace?: string;
10
+ secretKey?: string;
11
+ environment?: string;
12
+ }
13
+ declare const GitHubTokenConfig: ({ title, description, secretsNamespace, secretKey, environment }: TokenConfigProps) => import("react/jsx-runtime").JSX.Element;
14
+ export default GitHubTokenConfig;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Card, Stack, Text, Button } from '@sanity/ui';
4
+ import { useSecrets, SettingsView } from '@sanity/studio-secrets';
5
+ import { CogIcon } from '@sanity/icons';
6
+ const GitHubTokenConfig = ({ title = "🔐 GitHub Token Configuration", description = "Configure your GitHub personal access token for deployment automation.", secretsNamespace = "deployButton", secretKey = "github_token", environment = "production" }) => {
7
+ const [showSettings, setShowSettings] = useState(false);
8
+ // Use Sanity secrets to check if token exists
9
+ const { secrets } = useSecrets(secretsNamespace);
10
+ const token = secrets?.[secretKey];
11
+ const hasToken = Boolean(token);
12
+ // Plugin config for secrets
13
+ const pluginConfigKeys = [
14
+ {
15
+ key: secretKey,
16
+ title: `GitHub Token for ${environment}`,
17
+ description: `Personal access token for GitHub Actions deployment`,
18
+ type: 'string',
19
+ inputType: 'password'
20
+ }
21
+ ];
22
+ if (showSettings) {
23
+ return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: title }), _jsx(SettingsView, { title: title, namespace: secretsNamespace, keys: pluginConfigKeys, onClose: () => {
24
+ setShowSettings(false);
25
+ } })] }) }));
26
+ }
27
+ return (_jsx(Card, { padding: 4, radius: 2, shadow: 1, children: _jsxs(Stack, { space: 3, children: [_jsx(Text, { size: 2, weight: "semibold", children: title }), _jsx(Text, { size: 1, muted: true, children: description }), _jsxs(Stack, { space: 2, children: [_jsxs(Text, { size: 1, children: ["Status: ", hasToken ? '✅ Token configured' : '⚠️ No token configured'] }), _jsx(Button, { tone: hasToken ? 'default' : 'primary', icon: CogIcon, onClick: () => setShowSettings(true), text: hasToken ? 'Update Token' : 'Configure Token' })] })] }) }));
28
+ };
29
+ export default GitHubTokenConfig;
@@ -52,7 +52,7 @@ export declare const VideoFileNoThumb: {
52
52
  };
53
53
  export declare const VideoURLNoThumb: {
54
54
  type: "object";
55
- name: "videoFileNoThumb";
55
+ name: "videoURLNoThumb";
56
56
  } & Omit<import("sanity").ObjectDefinition, "preview"> & {
57
57
  preview?: import("sanity").PreviewConfig<Record<string, string>, Record<never, any>> | undefined;
58
58
  };
@@ -176,7 +176,7 @@ export const VideoFileNoThumb = defineType({
176
176
  }
177
177
  });
178
178
  export const VideoURLNoThumb = defineType({
179
- name: 'videoFileNoThumb',
179
+ name: 'videoURLNoThumb',
180
180
  type: 'object',
181
181
  title: 'Video URL',
182
182
  fields: [
package/lib/main.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './components/core/SEOImage';
2
2
  export * from './components/core/SEO';
3
3
  export * from './validators/utils';
4
- export * from './components/ui/DeployButton';
4
+ export * from './components/deploy/EnvUtils';
5
+ export * from './components/deploy/DeployButton';
6
+ export * from './components/deploy/GithubTokenConfig';
5
7
  export * from './components/video/VideoSchemas';
package/lib/main.js CHANGED
@@ -2,5 +2,7 @@ export * from './components/core/SEOImage';
2
2
  export * from './components/core/SEO';
3
3
  // export * from './config/utils';
4
4
  export * from './validators/utils';
5
- export * from './components/ui/DeployButton';
5
+ export * from './components/deploy/EnvUtils';
6
+ export * from './components/deploy/DeployButton';
7
+ export * from './components/deploy/GithubTokenConfig';
6
8
  export * from './components/video/VideoSchemas';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fils/sanity-components",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Fil's Components for Sanity Back-Ends",
5
5
  "repository": "git@github.com:fil-studio/fils.git",
6
6
  "author": "Fil Studio <hello@fil.studio>",
@@ -102,8 +102,9 @@ const DeployButton = ({
102
102
  try {
103
103
  console.log('Polling workflow:', runId);
104
104
 
105
+ const cacheBuster = `_=${Date.now()}`;
105
106
  const response = await fetch(
106
- `https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs/${runId}`,
107
+ `https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs/${runId}?${cacheBuster}`,
107
108
  {
108
109
  headers: {
109
110
  'Authorization': `token ${effectiveToken}`,
@@ -145,13 +146,16 @@ const DeployButton = ({
145
146
  }
146
147
  };
147
148
 
148
- // Find the workflow run
149
- const findWorkflowRun = async (): Promise<void> => {
149
+ // Find the workflow run with retry logic
150
+ const findWorkflowRun = async (retryCount = 0): Promise<void> => {
151
+ const maxRetries = 6; // Try for 30 seconds (5s * 6)
152
+
150
153
  try {
151
- console.log('Looking for workflow run...');
154
+ console.log(`Looking for workflow run... (attempt ${retryCount + 1}/${maxRetries})`);
152
155
 
156
+ const cacheBuster = `_=${Date.now()}`;
153
157
  const response = await fetch(
154
- `https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs?per_page=10`,
158
+ `https://api.github.com/repos/${config.owner}/${config.repo}/actions/runs?per_page=10&${cacheBuster}`,
155
159
  {
156
160
  headers: {
157
161
  'Authorization': `token ${effectiveToken}`,
@@ -179,23 +183,42 @@ const DeployButton = ({
179
183
  setDeployState('deploying');
180
184
  setMessage('🚀 Deployment started...');
181
185
 
182
- // Start polling every 5 seconds
186
+ // Start polling every 3 seconds (faster response)
183
187
  intervalRef.current = setInterval(() => {
184
188
  pollWorkflowStatus(recentRun.id);
185
- }, 5000);
189
+ }, 3000);
186
190
 
187
191
  // Initial poll
188
192
  pollWorkflowStatus(recentRun.id);
193
+
194
+ } else if (retryCount < maxRetries) {
195
+ // Retry after 5 seconds
196
+ console.log(`Workflow not found yet, retrying in 5s... (${retryCount + 1}/${maxRetries})`);
197
+ setMessage(`⏳ Waiting for workflow to start... (${retryCount + 1}/${maxRetries})`);
198
+ setTimeout(() => {
199
+ findWorkflowRun(retryCount + 1);
200
+ }, 5000);
201
+
189
202
  } else {
190
- console.log('No recent workflow found');
203
+ console.log('No recent workflow found after retries');
191
204
  setDeployState('error');
192
205
  setMessage('❌ Could not find workflow run - check if workflow exists');
206
+ monitoringRef.current = false;
193
207
  }
194
208
  } catch (error) {
195
209
  console.error('Error finding workflow:', error);
196
210
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
197
- setDeployState('error');
198
- setMessage(`❌ Error finding workflow: ${errorMessage}`);
211
+
212
+ if (retryCount < maxRetries) {
213
+ console.log(`Error finding workflow, retrying... (${retryCount + 1}/${maxRetries})`);
214
+ setTimeout(() => {
215
+ findWorkflowRun(retryCount + 1);
216
+ }, 5000);
217
+ } else {
218
+ setDeployState('error');
219
+ setMessage(`❌ Error finding workflow: ${errorMessage}`);
220
+ monitoringRef.current = false;
221
+ }
199
222
  }
200
223
  };
201
224
 
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Environment Detection Utilities
3
+ * Helper functions to determine if we're in development mode
4
+ */
5
+
6
+ /**
7
+ * Check if running in local development
8
+ * @returns true if in development, false otherwise
9
+ */
10
+ export const isDevelopment = (): boolean => {
11
+ // Check for localhost
12
+ if (typeof window !== 'undefined') {
13
+ const hostname = window.location.hostname;
14
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.');
15
+ }
16
+
17
+ // Fallback to NODE_ENV if available
18
+ return process.env.NODE_ENV === 'development';
19
+ };
20
+
21
+ /**
22
+ * Check if running in production
23
+ * @returns true if in production, false otherwise
24
+ */
25
+ export const isProduction = (): boolean => {
26
+ return !isDevelopment();
27
+ };
28
+
29
+ /**
30
+ * Conditionally render component only in development
31
+ * Usage: withDevOnly(MyComponent)
32
+ */
33
+ export const withDevOnly = <P extends object>(
34
+ Component: React.ComponentType<P>
35
+ ): React.FC<P> => {
36
+ return (props: P) => {
37
+ if (!isDevelopment()) {
38
+ return null;
39
+ }
40
+ return <Component {...props} />;
41
+ };
42
+ };
43
+
44
+ /**
45
+ * Conditionally render component only in production
46
+ * Usage: withProdOnly(MyComponent)
47
+ */
48
+ export const withProdOnly = <P extends object>(
49
+ Component: React.ComponentType<P>
50
+ ): React.FC<P> => {
51
+ return (props: P) => {
52
+ if (!isProduction()) {
53
+ return null;
54
+ }
55
+ return <Component {...props} />;
56
+ };
57
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * GitHub Token Configuration Widget
3
+ * Only show this in local development environment
4
+ * Dependencies: React, @sanity/ui, @sanity/icons, @sanity/studio-secrets
5
+ */
6
+
7
+ import * as React from 'react';
8
+ import { useState } from 'react';
9
+ import { Card, Stack, Text, Button } from '@sanity/ui';
10
+ import { useSecrets, SettingsView } from '@sanity/studio-secrets';
11
+ import { CogIcon } from '@sanity/icons';
12
+
13
+ interface TokenConfigProps {
14
+ title?: string;
15
+ description?: string;
16
+ secretsNamespace?: string;
17
+ secretKey?: string;
18
+ environment?: string;
19
+ }
20
+
21
+ const GitHubTokenConfig = ({
22
+ title = "🔐 GitHub Token Configuration",
23
+ description = "Configure your GitHub personal access token for deployment automation.",
24
+ secretsNamespace = "deployButton",
25
+ secretKey = "github_token",
26
+ environment = "production"
27
+ }: TokenConfigProps) => {
28
+ const [showSettings, setShowSettings] = useState(false);
29
+
30
+ // Use Sanity secrets to check if token exists
31
+ const { secrets } = useSecrets(secretsNamespace);
32
+ const token = (secrets as Record<string, string | undefined>)?.[secretKey];
33
+ const hasToken = Boolean(token);
34
+
35
+ // Plugin config for secrets
36
+ const pluginConfigKeys = [
37
+ {
38
+ key: secretKey,
39
+ title: `GitHub Token for ${environment}`,
40
+ description: `Personal access token for GitHub Actions deployment`,
41
+ type: 'string' as const,
42
+ inputType: 'password' as const
43
+ }
44
+ ];
45
+
46
+ if (showSettings) {
47
+ return (
48
+ <Card padding={4} radius={2} shadow={1}>
49
+ <Stack space={3}>
50
+ <Text size={2} weight="semibold">
51
+ {title}
52
+ </Text>
53
+ <SettingsView
54
+ title={title}
55
+ namespace={secretsNamespace}
56
+ keys={pluginConfigKeys}
57
+ onClose={() => {
58
+ setShowSettings(false);
59
+ }}
60
+ />
61
+ </Stack>
62
+ </Card>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <Card padding={4} radius={2} shadow={1}>
68
+ <Stack space={3}>
69
+ <Text size={2} weight="semibold">
70
+ {title}
71
+ </Text>
72
+ <Text size={1} muted>
73
+ {description}
74
+ </Text>
75
+
76
+ <Stack space={2}>
77
+ <Text size={1}>
78
+ Status: {hasToken ? '✅ Token configured' : '⚠️ No token configured'}
79
+ </Text>
80
+
81
+ <Button
82
+ tone={hasToken ? 'default' : 'primary'}
83
+ icon={CogIcon}
84
+ onClick={() => setShowSettings(true)}
85
+ text={hasToken ? 'Update Token' : 'Configure Token'}
86
+ />
87
+ </Stack>
88
+ </Stack>
89
+ </Card>
90
+ );
91
+ };
92
+
93
+ export default GitHubTokenConfig;
@@ -184,7 +184,7 @@ export const VideoFileNoThumb = defineType({
184
184
  });
185
185
 
186
186
  export const VideoURLNoThumb = defineType({
187
- name: 'videoFileNoThumb',
187
+ name: 'videoURLNoThumb',
188
188
  type: 'object',
189
189
  title: 'Video URL',
190
190
  fields: [
package/src/main.ts CHANGED
@@ -2,5 +2,7 @@ export * from './components/core/SEOImage';
2
2
  export * from './components/core/SEO';
3
3
  // export * from './config/utils';
4
4
  export * from './validators/utils';
5
- export * from './components/ui/DeployButton';
5
+ export * from './components/deploy/EnvUtils';
6
+ export * from './components/deploy/DeployButton';
7
+ export * from './components/deploy/GithubTokenConfig';
6
8
  export * from './components/video/VideoSchemas';