@startsimpli/hooks 0.4.11 → 0.4.15
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/README.md +225 -0
- package/package.json +24 -15
- package/src/__tests__/usePresentations.test.ts +120 -0
- package/src/__tests__/useVault.test.ts +107 -0
- package/src/__tests__/useWorkflows.test.ts +162 -0
- package/src/index.ts +20 -0
- package/src/useCSV-core.ts +144 -0
- package/src/useCSV.native.ts +26 -0
- package/src/useCSV.ts +12 -136
- package/src/usePresentations.ts +120 -0
- package/src/useRecentlyViewed.native.ts +56 -0
- package/src/useReducedMotion.native.ts +34 -0
- package/src/useRefetchOnFocus.native.ts +50 -0
- package/src/useVault.ts +142 -0
- package/src/useWorkflows.ts +122 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useReducedMotion — React Native implementation.
|
|
3
|
+
*
|
|
4
|
+
* Web uses window.matchMedia('(prefers-reduced-motion: reduce)'); React Native
|
|
5
|
+
* exposes the same preference through AccessibilityInfo. Metro resolves this
|
|
6
|
+
* file over useReducedMotion.ts on native builds.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useEffect, useState } from 'react'
|
|
10
|
+
import { AccessibilityInfo } from 'react-native'
|
|
11
|
+
|
|
12
|
+
export function useReducedMotion(): boolean {
|
|
13
|
+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let active = true
|
|
17
|
+
|
|
18
|
+
AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
|
|
19
|
+
if (active) setPrefersReducedMotion(enabled)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const subscription = AccessibilityInfo.addEventListener(
|
|
23
|
+
'reduceMotionChanged',
|
|
24
|
+
(enabled) => setPrefersReducedMotion(enabled)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
active = false
|
|
29
|
+
subscription.remove()
|
|
30
|
+
}
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
return prefersReducedMotion
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { AppState } from 'react-native'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* useRefetchOnFocus — React Native implementation.
|
|
6
|
+
*
|
|
7
|
+
* Web listens for document 'visibilitychange'; React Native has no document, so
|
|
8
|
+
* this watches AppState for the app returning to the foreground ('active'). The
|
|
9
|
+
* pathname-change behaviour and 2s debounce are identical to the web version.
|
|
10
|
+
* Metro resolves this file over useRefetchOnFocus.ts on native builds.
|
|
11
|
+
*/
|
|
12
|
+
export function useRefetchOnFocus(
|
|
13
|
+
refetch: () => void,
|
|
14
|
+
pathname: string,
|
|
15
|
+
enabled = true
|
|
16
|
+
): void {
|
|
17
|
+
const mounted = useRef(false)
|
|
18
|
+
const lastRefetch = useRef(0)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!enabled) return
|
|
22
|
+
|
|
23
|
+
if (!mounted.current) {
|
|
24
|
+
mounted.current = true
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const now = Date.now()
|
|
29
|
+
if (now - lastRefetch.current < 2000) return
|
|
30
|
+
|
|
31
|
+
lastRefetch.current = now
|
|
32
|
+
refetch()
|
|
33
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
34
|
+
}, [pathname, enabled])
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!enabled) return
|
|
38
|
+
|
|
39
|
+
const subscription = AppState.addEventListener('change', (state) => {
|
|
40
|
+
if (state === 'active' && mounted.current) {
|
|
41
|
+
const now = Date.now()
|
|
42
|
+
if (now - lastRefetch.current < 2000) return
|
|
43
|
+
lastRefetch.current = now
|
|
44
|
+
refetch()
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return () => subscription.remove()
|
|
49
|
+
}, [refetch, enabled])
|
|
50
|
+
}
|
package/src/useVault.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import type {
|
|
3
|
+
VaultAccessKeyInput,
|
|
4
|
+
VaultApi,
|
|
5
|
+
VaultEnvironmentInput,
|
|
6
|
+
VaultListParams,
|
|
7
|
+
VaultSecretInput,
|
|
8
|
+
} from '@startsimpli/api'
|
|
9
|
+
|
|
10
|
+
/** TanStack Query hooks over VaultApi (startsim-d30.3.2).
|
|
11
|
+
*
|
|
12
|
+
* Each hook takes the VaultApi instance (e.g. `api.vault`), matching the
|
|
13
|
+
* useMessages convention. Secrets are revealed on demand (a mutation), never
|
|
14
|
+
* auto-fetched, so values are only pulled when the user asks.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const ENVIRONMENTS_KEY = ['vault', 'environments'] as const
|
|
18
|
+
const environmentKey = (slug: string) => ['vault', 'environment', slug]
|
|
19
|
+
const secretsKey = (slug: string) => ['vault', 'secrets', slug]
|
|
20
|
+
const accessKeysKey = (slug: string) => ['vault', 'access-keys', slug]
|
|
21
|
+
|
|
22
|
+
/** Secret mutations alter the env's `secret_count`, so refresh the list +
|
|
23
|
+
* detail too — otherwise the /environments page shows a stale count
|
|
24
|
+
* (startsim-drl). */
|
|
25
|
+
function invalidateSecretQueries(qc: ReturnType<typeof useQueryClient>, slug: string) {
|
|
26
|
+
qc.invalidateQueries({ queryKey: secretsKey(slug) })
|
|
27
|
+
qc.invalidateQueries({ queryKey: ENVIRONMENTS_KEY })
|
|
28
|
+
qc.invalidateQueries({ queryKey: environmentKey(slug) })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- Environments ---
|
|
32
|
+
export function useEnvironments(api: VaultApi, params?: VaultListParams) {
|
|
33
|
+
return useQuery({
|
|
34
|
+
queryKey: ['vault', 'environments', params],
|
|
35
|
+
queryFn: () => api.listEnvironments(params),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useEnvironment(api: VaultApi, slug: string) {
|
|
40
|
+
return useQuery({
|
|
41
|
+
queryKey: ['vault', 'environment', slug],
|
|
42
|
+
queryFn: () => api.getEnvironment(slug),
|
|
43
|
+
enabled: !!slug,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useCreateEnvironment(api: VaultApi) {
|
|
48
|
+
const qc = useQueryClient()
|
|
49
|
+
return useMutation({
|
|
50
|
+
mutationFn: (data: VaultEnvironmentInput) => api.createEnvironment(data),
|
|
51
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: ENVIRONMENTS_KEY }),
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useUpdateEnvironment(api: VaultApi) {
|
|
56
|
+
const qc = useQueryClient()
|
|
57
|
+
return useMutation({
|
|
58
|
+
mutationFn: ({ slug, data }: { slug: string; data: Partial<VaultEnvironmentInput> }) =>
|
|
59
|
+
api.updateEnvironment(slug, data),
|
|
60
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: ENVIRONMENTS_KEY }),
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function useDeleteEnvironment(api: VaultApi) {
|
|
65
|
+
const qc = useQueryClient()
|
|
66
|
+
return useMutation({
|
|
67
|
+
mutationFn: (slug: string) => api.deleteEnvironment(slug),
|
|
68
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: ENVIRONMENTS_KEY }),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- Secrets ---
|
|
73
|
+
export function useSecrets(api: VaultApi, slug: string, params?: VaultListParams) {
|
|
74
|
+
return useQuery({
|
|
75
|
+
queryKey: ['vault', 'secrets', slug, params],
|
|
76
|
+
queryFn: () => api.listSecrets(slug, params),
|
|
77
|
+
enabled: !!slug,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function useCreateSecret(api: VaultApi, slug: string) {
|
|
82
|
+
const qc = useQueryClient()
|
|
83
|
+
return useMutation({
|
|
84
|
+
mutationFn: (data: VaultSecretInput) => api.createSecret(slug, data),
|
|
85
|
+
onSuccess: () => invalidateSecretQueries(qc, slug),
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function useUpdateSecret(api: VaultApi, slug: string) {
|
|
90
|
+
const qc = useQueryClient()
|
|
91
|
+
return useMutation({
|
|
92
|
+
mutationFn: ({ id, data }: { id: string; data: Partial<VaultSecretInput> }) =>
|
|
93
|
+
api.updateSecret(slug, id, data),
|
|
94
|
+
onSuccess: () => invalidateSecretQueries(qc, slug),
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function useDeleteSecret(api: VaultApi, slug: string) {
|
|
99
|
+
const qc = useQueryClient()
|
|
100
|
+
return useMutation({
|
|
101
|
+
mutationFn: (id: string) => api.deleteSecret(slug, id),
|
|
102
|
+
onSuccess: () => invalidateSecretQueries(qc, slug),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function useRevealSecret(api: VaultApi, slug: string) {
|
|
107
|
+
return useMutation({ mutationFn: (id: string) => api.revealSecret(slug, id) })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Access keys ---
|
|
111
|
+
export function useAccessKeys(api: VaultApi, slug: string, params?: VaultListParams) {
|
|
112
|
+
return useQuery({
|
|
113
|
+
queryKey: ['vault', 'access-keys', slug, params],
|
|
114
|
+
queryFn: () => api.listAccessKeys(slug, params),
|
|
115
|
+
enabled: !!slug,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function useCreateAccessKey(api: VaultApi, slug: string) {
|
|
120
|
+
const qc = useQueryClient()
|
|
121
|
+
return useMutation({
|
|
122
|
+
mutationFn: (data: VaultAccessKeyInput) => api.createAccessKey(slug, data),
|
|
123
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: accessKeysKey(slug) }),
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function useDeleteAccessKey(api: VaultApi, slug: string) {
|
|
128
|
+
const qc = useQueryClient()
|
|
129
|
+
return useMutation({
|
|
130
|
+
mutationFn: (id: string) => api.deleteAccessKey(slug, id),
|
|
131
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: accessKeysKey(slug) }),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Audit ---
|
|
136
|
+
export function useAuditLog(api: VaultApi, slug: string, params?: VaultListParams) {
|
|
137
|
+
return useQuery({
|
|
138
|
+
queryKey: ['vault', 'audit', slug, params],
|
|
139
|
+
queryFn: () => api.listAudit(slug, params),
|
|
140
|
+
enabled: !!slug,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import type {
|
|
3
|
+
WorkflowsApi,
|
|
4
|
+
WorkflowInput,
|
|
5
|
+
WorkflowListParams,
|
|
6
|
+
ExecutionListParams,
|
|
7
|
+
RunStatus,
|
|
8
|
+
} from '@startsimpli/api'
|
|
9
|
+
|
|
10
|
+
/** TanStack Query hooks over WorkflowsApi (epic startsim-xsh.12).
|
|
11
|
+
*
|
|
12
|
+
* Each hook takes the WorkflowsApi instance (e.g. `api.workflows`), matching
|
|
13
|
+
* the usePresentations / useVault convention. Executions are async on the
|
|
14
|
+
* backend, so the execution detail query self-polls while a run is in flight
|
|
15
|
+
* and stops on a terminal status.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const WORKFLOWS_KEY = ['workflows', 'list'] as const
|
|
19
|
+
const workflowKey = (id: string) => ['workflows', 'detail', id]
|
|
20
|
+
const executionKey = (executionId: string) => ['workflows', 'execution', executionId]
|
|
21
|
+
const executionsKey = (workflowId: string) => ['workflows', 'executions', workflowId]
|
|
22
|
+
|
|
23
|
+
const POLL_MS = 1000
|
|
24
|
+
|
|
25
|
+
/** Refetch interval for the execution detail query: poll while a run is live. */
|
|
26
|
+
export function executionRefetchInterval(status: RunStatus | undefined): number | false {
|
|
27
|
+
return status === 'running' || status === 'pending' || status === 'waiting' ? POLL_MS : false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Workflows ---
|
|
31
|
+
export function useWorkflows(api: WorkflowsApi, params?: WorkflowListParams) {
|
|
32
|
+
return useQuery({
|
|
33
|
+
queryKey: ['workflows', 'list', params],
|
|
34
|
+
queryFn: () => api.listWorkflows(params),
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useWorkflow(api: WorkflowsApi, id: string) {
|
|
39
|
+
return useQuery({
|
|
40
|
+
queryKey: workflowKey(id),
|
|
41
|
+
queryFn: () => api.getWorkflow(id),
|
|
42
|
+
enabled: !!id,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useWorkflowNodeTypes(api: WorkflowsApi) {
|
|
47
|
+
return useQuery({
|
|
48
|
+
queryKey: ['workflows', 'node-types'],
|
|
49
|
+
queryFn: () => api.getNodeTypes(),
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useCreateWorkflow(api: WorkflowsApi) {
|
|
54
|
+
const qc = useQueryClient()
|
|
55
|
+
return useMutation({
|
|
56
|
+
mutationFn: (data: WorkflowInput) => api.createWorkflow(data),
|
|
57
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: WORKFLOWS_KEY }),
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useUpdateWorkflow(api: WorkflowsApi, id: string) {
|
|
62
|
+
const qc = useQueryClient()
|
|
63
|
+
return useMutation({
|
|
64
|
+
mutationFn: (data: Partial<WorkflowInput>) => api.updateWorkflow(id, data),
|
|
65
|
+
onSuccess: () => {
|
|
66
|
+
qc.invalidateQueries({ queryKey: workflowKey(id) })
|
|
67
|
+
qc.invalidateQueries({ queryKey: WORKFLOWS_KEY })
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useDeleteWorkflow(api: WorkflowsApi) {
|
|
73
|
+
const qc = useQueryClient()
|
|
74
|
+
return useMutation({
|
|
75
|
+
mutationFn: (id: string) => api.deleteWorkflow(id),
|
|
76
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: WORKFLOWS_KEY }),
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Execution ---
|
|
81
|
+
/** Kick off a run; the mutation result surfaces the executionId. */
|
|
82
|
+
export function useExecuteWorkflow(api: WorkflowsApi, id: string) {
|
|
83
|
+
const qc = useQueryClient()
|
|
84
|
+
return useMutation({
|
|
85
|
+
mutationFn: (contextData?: Record<string, unknown>) => api.executeWorkflow(id, contextData),
|
|
86
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: executionsKey(id) }),
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Self-polling execution detail; stops when the run reaches a terminal state. */
|
|
91
|
+
export function useWorkflowExecution(api: WorkflowsApi, executionId: string, enabled = true) {
|
|
92
|
+
return useQuery({
|
|
93
|
+
queryKey: executionKey(executionId),
|
|
94
|
+
queryFn: () => api.getExecution(executionId),
|
|
95
|
+
enabled: enabled && !!executionId,
|
|
96
|
+
refetchInterval: (query) => executionRefetchInterval(query.state.data?.status),
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function useWorkflowExecutions(api: WorkflowsApi, workflowId: string, params?: ExecutionListParams) {
|
|
101
|
+
return useQuery({
|
|
102
|
+
queryKey: ['workflows', 'executions', workflowId, params],
|
|
103
|
+
queryFn: () => api.listExecutions(workflowId, params),
|
|
104
|
+
enabled: !!workflowId,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function useCancelExecution(api: WorkflowsApi) {
|
|
109
|
+
const qc = useQueryClient()
|
|
110
|
+
return useMutation({
|
|
111
|
+
mutationFn: (executionId: string) => api.cancelExecution(executionId),
|
|
112
|
+
onSuccess: (_data, executionId) => qc.invalidateQueries({ queryKey: executionKey(executionId) }),
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function useRetryExecution(api: WorkflowsApi) {
|
|
117
|
+
const qc = useQueryClient()
|
|
118
|
+
return useMutation({
|
|
119
|
+
mutationFn: (executionId: string) => api.retryExecution(executionId),
|
|
120
|
+
onSuccess: (_data, executionId) => qc.invalidateQueries({ queryKey: executionKey(executionId) }),
|
|
121
|
+
})
|
|
122
|
+
}
|