27 | {data.resource.expression ? (
28 | <>
29 |
30 | Cron:
31 | {data.resource.expression}
32 |
33 |
34 | Description:
35 |
36 | {cronstrue.toString(data.resource.expression, {
37 | verbose: true,
38 | })}
39 |
40 |
41 | >
42 | ) : (
43 |
44 | Rate:
45 | Every {data.resource.rate}
46 |
47 | )}
48 | >
49 | ),
50 | }}
51 | />
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/architecture/nodes/SecretNode.tsx:
--------------------------------------------------------------------------------
1 | import { type ComponentType } from 'react'
2 |
3 | import type { Secret } from '@/types'
4 | import type { NodeProps } from 'reactflow'
5 | import NodeBase, { type NodeBaseData } from './NodeBase'
6 |
7 | export type SecretNodeData = NodeBaseData
8 |
9 | export const SecretNode: ComponentType> = (props) => {
10 | const { data } = props
11 |
12 | return (
13 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/architecture/nodes/ServiceNode.tsx:
--------------------------------------------------------------------------------
1 | import { type ComponentType } from 'react'
2 |
3 | import type { Edge, NodeProps } from 'reactflow'
4 | import NodeBase, { type NodeBaseData } from './NodeBase'
5 | import { Button } from '@/components/ui/button'
6 |
7 | type ServiceData = {
8 | filePath: string
9 | }
10 |
11 | export interface ServiceNodeData extends NodeBaseData {
12 | connectedEdges: Edge[]
13 | }
14 |
15 | export const ServiceNode: ComponentType> = (
16 | props,
17 | ) => {
18 | const { data } = props
19 |
20 | const Icon = data.icon
21 |
22 | return (
23 |
32 |
33 |
34 | Open in VSCode
35 |
36 |
37 | ),
38 | }}
39 | />
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/architecture/nodes/TopicNode.tsx:
--------------------------------------------------------------------------------
1 | import { type ComponentType } from 'react'
2 |
3 | import type { Topic } from '@/types'
4 | import type { NodeProps } from 'reactflow'
5 | import NodeBase, { type NodeBaseData } from './NodeBase'
6 |
7 | export type TopicNodeData = NodeBaseData
8 |
9 | export const TopicNode: ComponentType> = (props) => {
10 | const { data } = props
11 | //http://localhost:4001/topics/updates
12 | return (
13 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/architecture/nodes/WebsitesNode.tsx:
--------------------------------------------------------------------------------
1 | import { type ComponentType } from 'react'
2 |
3 | import type { Website } from '@/types'
4 | import type { NodeProps } from 'reactflow'
5 | import NodeBase, { type NodeBaseData } from './NodeBase'
6 | import SitesList from '@/components/websites/SitesList'
7 |
8 | export type WebsitesNodeData = NodeBaseData
9 |
10 | export const WebsitesNode: ComponentType> = (
11 | props,
12 | ) => {
13 | const { data } = props
14 |
15 | const websites = data.resource
16 |
17 | const rootWebsite = websites.find((website) =>
18 | /localhost:\d+$/.test(website.url.replace(/\/$/, '')),
19 | )
20 |
21 | const description = `${websites.length === 1 ? 'website' : 'websites'} stored in a bucket and served via CDN.`
22 |
23 | return (
24 | site !== rootWebsite)}
37 | />
38 | ) : null,
39 | }}
40 | />
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/architecture/nodes/WebsocketNode.tsx:
--------------------------------------------------------------------------------
1 | import { type ComponentType } from 'react'
2 |
3 | import type { WebSocket } from '@/types'
4 | import type { NodeProps } from 'reactflow'
5 | import NodeBase, { type NodeBaseData } from './NodeBase'
6 |
7 | export type WebsocketNodeData = NodeBaseData
8 |
9 | export const WebsocketNode: ComponentType> = (
10 | props,
11 | ) => {
12 | const { data } = props
13 |
14 | return (
15 |
27 | {data.resource.targets ? (
28 | <>
29 |
30 | Events:
31 | {Object.keys(data.resource.targets).join(', ')}
32 |
33 | >
34 | ) : null}
35 | >
36 | ),
37 | }}
38 | />
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/databases/DatabasesMenu.tsx:
--------------------------------------------------------------------------------
1 | import { TrashIcon } from '@heroicons/react/20/solid'
2 |
3 | import { useHistory } from '../../lib/hooks/use-history'
4 | import ResourceDropdownMenu from '../shared/ResourceDropdownMenu'
5 | import {
6 | DropdownMenuGroup,
7 | DropdownMenuItem,
8 | DropdownMenuLabel,
9 | DropdownMenuSeparator,
10 | } from '../ui/dropdown-menu'
11 |
12 | interface Props {
13 | storageKey: string
14 | selected: string
15 | onAfterClear: () => void
16 | }
17 |
18 | const DatabasesMenu: React.FC = ({
19 | storageKey,
20 | selected,
21 | onAfterClear,
22 | }) => {
23 | const clearHistory = async () => {
24 | localStorage.removeItem(storageKey)
25 |
26 | onAfterClear()
27 | }
28 |
29 | return (
30 |
31 |
32 | Database Menu
33 |
34 |
35 |
36 |
37 |
38 | Clear Saved Query
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | export default DatabasesMenu
46 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/events/EventsHistory.tsx:
--------------------------------------------------------------------------------
1 | import type {
2 | EventHistoryItem,
3 | EventResource,
4 | TopicHistoryItem,
5 | } from '../../types'
6 | import { formatJSON } from '@/lib/utils'
7 | import CodeEditor from '../apis/CodeEditor'
8 | import HistoryAccordion from '../shared/HistoryAccordion'
9 |
10 | interface Props {
11 | history: EventHistoryItem[]
12 | selectedWorker: EventResource
13 | workerType: 'schedules' | 'topics' | 'jobs'
14 | }
15 |
16 | const EventsHistory: React.FC = ({
17 | selectedWorker,
18 | workerType,
19 | history,
20 | }) => {
21 | const requestHistory = history
22 | .sort((a, b) => b.time - a.time)
23 | .filter((h) => h.event)
24 | .filter((h) => h.event.name === selectedWorker.name)
25 |
26 | if (!requestHistory.length) {
27 | return There is no history.
28 | }
29 |
30 | return (
31 |
32 |
{
34 | let payload = ''
35 |
36 | if (workerType === 'topics' || workerType === 'jobs') {
37 | payload = (h.event as TopicHistoryItem['event']).payload
38 | }
39 |
40 | const formattedPayload = payload ? formatJSON(payload) : ''
41 |
42 | return {
43 | label: h.event.name,
44 | time: h.time,
45 | success: Boolean(h.event.success),
46 | content: formattedPayload ? (
47 |
58 | ) : undefined,
59 | }
60 | })}
61 | />
62 |
63 | )
64 | }
65 |
66 | export default EventsHistory
67 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/events/EventsMenu.tsx:
--------------------------------------------------------------------------------
1 | import { TrashIcon } from '@heroicons/react/20/solid'
2 |
3 | import { useHistory } from '../../lib/hooks/use-history'
4 | import ResourceDropdownMenu from '../shared/ResourceDropdownMenu'
5 | import {
6 | DropdownMenuGroup,
7 | DropdownMenuItem,
8 | DropdownMenuLabel,
9 | DropdownMenuSeparator,
10 | } from '../ui/dropdown-menu'
11 |
12 | interface Props {
13 | storageKey: string
14 | workerType: string
15 | selected: string
16 | onAfterClear: () => void
17 | }
18 |
19 | const EventsMenu: React.FC = ({
20 | workerType,
21 | storageKey,
22 | selected,
23 | onAfterClear,
24 | }) => {
25 | const { deleteHistory } = useHistory(workerType)
26 |
27 | const clearHistory = async () => {
28 | const prefix = `${storageKey}-${selected}-`
29 |
30 | for (let i = 0; i < localStorage.length; i++) {
31 | const key = localStorage.key(i)
32 | if (key?.startsWith(prefix)) {
33 | localStorage.removeItem(key)
34 | }
35 | }
36 |
37 | localStorage.removeItem(`${storageKey}-requests`)
38 |
39 | await deleteHistory()
40 |
41 | onAfterClear()
42 | }
43 |
44 | return (
45 |
46 |
47 | {workerType} Menu
48 |
49 |
50 |
51 |
52 |
53 | Clear History
54 |
55 |
56 |
57 | )
58 | }
59 |
60 | export default EventsMenu
61 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/layout/AppLayout/NavigationItem.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import React from 'react'
3 |
4 | export interface NavigationItemProps {
5 | name: string
6 | href: string
7 | icon: React.ForwardRefExoticComponent<
8 | Omit, 'ref'> & {
9 | title?: string
10 | titleId?: string
11 | } & React.RefAttributes
12 | >
13 | onClick?: () => void
14 | routePath: string
15 | }
16 |
17 | const NavigationItem: React.FC = ({
18 | name,
19 | href,
20 | icon: Icon,
21 | onClick,
22 | routePath,
23 | }) => {
24 | const isActive = href === routePath
25 |
26 | return (
27 |
28 |
41 |
42 |
50 | {name}
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | export default NavigationItem
58 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/layout/AppLayout/index.ts:
--------------------------------------------------------------------------------
1 | import AppLayout from './AppLayout'
2 |
3 | export default AppLayout
4 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/layout/BreadCrumbs.tsx:
--------------------------------------------------------------------------------
1 | import { type PropsWithChildren, Children } from 'react'
2 | import { Separator } from '../ui/separator'
3 | import { cn } from '@/lib/utils'
4 |
5 | interface Props extends PropsWithChildren {
6 | className?: string
7 | }
8 |
9 | const BreadCrumbs = ({ children, className }: Props) => {
10 | const childArray = Children.toArray(children)
11 |
12 | return (
13 |
32 | )
33 | }
34 |
35 | export default BreadCrumbs
36 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/logs/FilterTrigger.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSidebar } from '../ui/sidebar'
3 | import { Button } from '../ui/button'
4 | import { FunnelIcon } from '@heroicons/react/24/outline'
5 |
6 | const FilterTrigger: React.FC = () => {
7 | const { toggleSidebar } = useSidebar()
8 |
9 | return (
10 |
19 | )
20 | }
21 |
22 | export default FilterTrigger
23 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/secrets/SecretVersionsTable/index.ts:
--------------------------------------------------------------------------------
1 | import { SecretVersionsTable } from './SecretVersionsTable'
2 |
3 | export default SecretVersionsTable
4 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/secrets/SecretsContext.tsx:
--------------------------------------------------------------------------------
1 | import { useSecret } from '@/lib/hooks/use-secret'
2 | import type { Secret, SecretVersion } from '@/types'
3 | import React, { createContext, useState, type PropsWithChildren } from 'react'
4 | import { VersionActionDialog } from './VersionActionDialog'
5 |
6 | interface SecretsContextProps {
7 | selectedVersions: SecretVersion[]
8 | setSelectedVersions: React.Dispatch>
9 | selectedSecret?: Secret
10 | setSelectedSecret: (secret: Secret | undefined) => void
11 | setDialogAction: React.Dispatch>
12 | setDialogOpen: React.Dispatch>
13 | }
14 |
15 | export const SecretsContext = createContext({
16 | selectedVersions: [],
17 | setSelectedVersions: () => {},
18 | selectedSecret: undefined,
19 | setSelectedSecret: () => {},
20 | setDialogAction: () => {},
21 | setDialogOpen: () => {},
22 | })
23 |
24 | export const SecretsProvider: React.FC = ({ children }) => {
25 | const [selectedSecret, setSelectedSecret] = useState()
26 |
27 | const [selectedVersions, setSelectedVersions] = useState([])
28 | const [dialogOpen, setDialogOpen] = useState(false)
29 | const [dialogAction, setDialogAction] = useState<'add' | 'delete'>('add')
30 |
31 | return (
32 |
42 | {selectedSecret && (
43 |
48 | )}
49 | {children}
50 |
51 | )
52 | }
53 |
54 | export const useSecretsContext = () => {
55 | return React.useContext(SecretsContext)
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/secrets/SecretsTreeView.tsx:
--------------------------------------------------------------------------------
1 | import { type FC, useMemo } from 'react'
2 | import type { Secret } from '@/types'
3 | import TreeView, { type TreeItemType } from '../shared/TreeView'
4 | import type { TreeItem, TreeItemIndex } from 'react-complex-tree'
5 |
6 | export type SecretsTreeItemType = TreeItemType
7 |
8 | interface Props {
9 | resources: Secret[]
10 | onSelect: (resource: Secret) => void
11 | initialItem: Secret
12 | }
13 |
14 | const SecretsTreeView: FC = ({ resources, onSelect, initialItem }) => {
15 | const treeItems: Record<
16 | TreeItemIndex,
17 | TreeItem
18 | > = useMemo(() => {
19 | const rootItem: TreeItem = {
20 | index: 'root',
21 | isFolder: true,
22 | children: [],
23 | data: null,
24 | }
25 |
26 | const rootItems: Record> = {
27 | root: rootItem,
28 | }
29 |
30 | for (const resource of resources) {
31 | // add api if not added already
32 | if (!rootItems[resource.name]) {
33 | rootItems[resource.name] = {
34 | index: resource.name,
35 | data: {
36 | label: resource.name,
37 | data: resource,
38 | },
39 | }
40 |
41 | rootItem.children!.push(resource.name)
42 | }
43 | }
44 |
45 | return rootItems
46 | }, [resources])
47 |
48 | return (
49 |
50 | label={'Secrets'}
51 | items={treeItems}
52 | initialItem={initialItem.name}
53 | getItemTitle={(item) => item.data.label}
54 | onPrimaryAction={(items) => {
55 | if (items.data.data) {
56 | onSelect(items.data.data)
57 | }
58 | }}
59 | renderItemTitle={({ item }) => {
60 | return {item.data.label}
61 | }}
62 | />
63 | )
64 | }
65 |
66 | export default SecretsTreeView
67 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/shared/Badge.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import type { PropsWithChildren } from 'react'
3 |
4 | interface Props extends PropsWithChildren {
5 | status: 'red' | 'green' | 'yellow' | 'orange' | 'blue' | 'default'
6 | className?: string
7 | }
8 |
9 | const Badge: React.FC = ({
10 | status = 'default',
11 | className,
12 | children,
13 | ...rest
14 | }) => {
15 | return (
16 |
29 | {children}
30 |
31 | )
32 | }
33 |
34 | export default Badge
35 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/shared/Loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | import Lottie from 'lottie-react'
2 | import loadingAnim from './loading.animation.json'
3 | import { PropsWithChildren, Suspense, useEffect, useRef, useState } from 'react'
4 | import { cn } from '@/lib/utils'
5 |
6 | interface Props extends PropsWithChildren {
7 | delay: number
8 | conditionToShow: boolean
9 | className?: string
10 | }
11 |
12 | const Loading: React.FC = ({
13 | delay,
14 | conditionToShow,
15 | className,
16 | children,
17 | }) => {
18 | const timeoutRef = useRef()
19 | const [showLoader, setShowLoader] = useState(true)
20 |
21 | useEffect(() => {
22 | if (delay) {
23 | if (timeoutRef.current) {
24 | clearTimeout(timeoutRef.current)
25 | }
26 |
27 | timeoutRef.current = setTimeout(() => {
28 | setShowLoader(false)
29 | }, delay)
30 | }
31 |
32 | return () => clearTimeout(timeoutRef.current)
33 | }, [delay])
34 |
35 | const Loader = (
36 |
40 |
41 |
47 |
48 |
Loading...
49 |
50 | )
51 |
52 | return showLoader || !conditionToShow ? (
53 | Loader
54 | ) : (
55 | {children}
56 | )
57 | }
58 |
59 | export default Loading
60 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/shared/Loading/index.tsx:
--------------------------------------------------------------------------------
1 | import Loading from './Loading'
2 |
3 | export default Loading
4 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/shared/NotFoundAlert.tsx:
--------------------------------------------------------------------------------
1 | import React, { type PropsWithChildren } from 'react'
2 | import { Alert } from '../ui/alert'
3 | import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
4 |
5 | interface Props extends PropsWithChildren {
6 | className?: string
7 | }
8 |
9 | const NotFoundAlert: React.FC = ({ children, className }) => {
10 | return (
11 |
12 |
20 |
21 | )
22 | }
23 |
24 | export default NotFoundAlert
25 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/shared/ResourceDropdownMenu.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DropdownMenu,
3 | DropdownMenuContent,
4 | DropdownMenuTrigger,
5 | } from '../ui/dropdown-menu'
6 | import { EllipsisHorizontalIcon } from '@heroicons/react/20/solid'
7 |
8 | import { Button } from '../ui/button'
9 | import type { PropsWithChildren } from 'react'
10 |
11 | const ResourceDropdownMenu = ({ children }: PropsWithChildren) => {
12 | return (
13 |
14 |
15 |
19 |
20 | {children}
21 |
22 | )
23 | }
24 |
25 | export default ResourceDropdownMenu
26 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/shared/SectionCard.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardFooter,
7 | CardHeader,
8 | CardTitle,
9 | } from '../ui/card'
10 |
11 | interface SectionCardProps {
12 | title?: string
13 | description?: string
14 | children: React.ReactNode
15 | className?: string
16 | innerClassName?: string
17 | headerClassName?: string
18 | headerSiblings?: React.ReactNode
19 | footer?: React.ReactNode
20 | }
21 |
22 | const SectionCard = ({
23 | title,
24 | description,
25 | children,
26 | className,
27 | innerClassName,
28 | headerClassName,
29 | headerSiblings,
30 | footer,
31 | }: SectionCardProps) => {
32 | return (
33 |
34 | {title && (
35 |
36 |
37 |
38 | {title}
39 |
40 | {headerSiblings}
41 |
42 | {description && {description}}
43 |
44 | )}
45 |
46 | {children}
47 |
48 | {footer && {footer}}
49 |
50 | )
51 | }
52 |
53 | export default SectionCard
54 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/shared/TextField.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import type {
3 | ForwardRefExoticComponent,
4 | InputHTMLAttributes,
5 | SVGProps,
6 | } from 'react'
7 | import { Input } from '../ui/input'
8 | import { Label } from '../ui/label'
9 |
10 | interface Props extends InputHTMLAttributes {
11 | label: string
12 | hideLabel?: boolean
13 | id: string
14 | icon?: ForwardRefExoticComponent>
15 | }
16 |
17 | const TextField = ({
18 | label,
19 | hideLabel,
20 | id,
21 | icon: Icon,
22 | ...inputProps
23 | }: Props) => {
24 | return (
25 |
26 |
29 |
32 | {Icon && (
33 |
34 |
35 |
36 | )}
37 |
42 |
43 |
44 | )
45 | }
46 |
47 | export default TextField
48 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/shared/index.ts:
--------------------------------------------------------------------------------
1 | import Loading from './Loading'
2 | import Badge from './Badge'
3 | import FieldRows, { type FieldRow } from './FieldRows'
4 | import Spinner from './Spinner'
5 | import Tabs from './Tabs'
6 |
7 | export { Loading, Badge, FieldRows, Spinner, Tabs }
8 | export type { FieldRow }
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/storage/FileUpload.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import { ArrowUpOnSquareIcon } from '@heroicons/react/24/outline'
3 | import { type DropzoneOptions, useDropzone } from 'react-dropzone'
4 | import type { FC } from 'react'
5 |
6 | type Props = DropzoneOptions
7 |
8 | const FileUpload: FC = ({ multiple, ...rest }) => {
9 | const { getRootProps, getInputProps, isDragActive } = useDropzone({
10 | multiple,
11 | ...rest,
12 | })
13 |
14 | return (
15 |
22 |
23 |
24 |
25 | {isDragActive
26 | ? `Drop the ${multiple ? 'files' : 'file'} here ...`
27 | : `Drag or click to add ${multiple ? 'files' : 'file'}.`}
28 |
29 |
30 | )
31 | }
32 |
33 | export default FileUpload
34 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/storage/download-files.ts:
--------------------------------------------------------------------------------
1 | export const downloadFiles = async (
2 | files: Array<{ url: string; name: string }>,
3 | ): Promise => {
4 | const promises = files.map(async (file) => {
5 | const response = await fetch(file.url)
6 | const blob = await response.blob()
7 | const link = document.createElement('a')
8 | link.href = window.URL.createObjectURL(blob)
9 | link.setAttribute('download', file.name)
10 | document.body.appendChild(link)
11 | link.click()
12 | document.body.removeChild(link)
13 | })
14 |
15 | await Promise.all(promises)
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/storage/file-browser-styles.css:
--------------------------------------------------------------------------------
1 | .file-explorer .chonky-activeButton,
2 | .MuiPopover-root .chonky-activeButton,
3 | .chonky-selectionSizeText {
4 | @apply !text-primary;
5 | }
6 |
7 | [data-test-id='file-entry'] > *:first-child {
8 | @apply !bg-blue-100;
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/storage/index.ts:
--------------------------------------------------------------------------------
1 | import StorageExplorer from './StorageExplorer'
2 |
3 | export default StorageExplorer
4 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils/cn'
5 |
6 | const alertVariants = cva(
7 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
8 | {
9 | variants: {
10 | variant: {
11 | default: 'bg-background text-foreground',
12 | destructive:
13 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
14 | warning:
15 | 'border-yellow-50 bg-yellow-50 text-yellow-700 [&>svg]:text-yellow-400',
16 | info: 'border-blue-50 bg-blue-50 text-blue-700 [&>svg]:text-blue-400',
17 | },
18 | },
19 | defaultVariants: {
20 | variant: 'default',
21 | },
22 | },
23 | )
24 |
25 | const Alert = React.forwardRef<
26 | HTMLDivElement,
27 | React.HTMLAttributes & VariantProps
28 | >(({ className, variant, ...props }, ref) => (
29 |
35 | ))
36 | Alert.displayName = 'Alert'
37 |
38 | const AlertTitle = React.forwardRef<
39 | HTMLParagraphElement,
40 | React.HTMLAttributes
41 | >(({ className, ...props }, ref) => (
42 |
47 | ))
48 | AlertTitle.displayName = 'AlertTitle'
49 |
50 | const AlertDescription = React.forwardRef<
51 | HTMLParagraphElement,
52 | React.HTMLAttributes
53 | >(({ className, ...props }, ref) => (
54 |
59 | ))
60 | AlertDescription.displayName = 'AlertDescription'
61 |
62 | export { Alert, AlertTitle, AlertDescription }
63 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils/cn'
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | success:
16 | 'border-transparent bg-green-500 text-white hover:bg-green-500/80',
17 | destructive:
18 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
19 | outline: 'text-foreground',
20 | },
21 | },
22 | defaultVariants: {
23 | variant: 'default',
24 | },
25 | },
26 | )
27 |
28 | export interface BadgeProps
29 | extends React.HTMLAttributes,
30 | VariantProps {}
31 |
32 | function Badge({ className, variant, ...props }: BadgeProps) {
33 | return (
34 |
35 | )
36 | }
37 |
38 | export { Badge, badgeVariants }
39 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils/cn'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-9 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8 text-lg',
26 | icon: 'h-10 w-10',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | },
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button'
45 | return (
46 |
51 | )
52 | },
53 | )
54 | Button.displayName = 'Button'
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
3 | import { Check } from 'lucide-react'
4 |
5 | import { cn } from '@/lib/utils/cn'
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
22 |
23 |
24 |
25 | ))
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
27 |
28 | export { Checkbox }
29 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
2 |
3 | const Collapsible = CollapsiblePrimitive.Root
4 |
5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6 |
7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8 |
9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
10 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils/cn'
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | },
19 | )
20 | Input.displayName = 'Input'
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as LabelPrimitive from '@radix-ui/react-label'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils/cn'
6 |
7 | const labelVariants = cva(
8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as PopoverPrimitive from '@radix-ui/react-popover'
3 |
4 | import { cn } from '@/lib/utils/cn'
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
14 |
15 |
25 |
26 | ))
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
28 |
29 | export { Popover, PopoverTrigger, PopoverContent }
30 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
3 |
4 | import { cn } from '@/lib/utils/cn'
5 |
6 | const ScrollArea = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, children, ...props }, ref) => (
10 |
14 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = 'vertical', ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
3 |
4 | import { cn } from '@/lib/utils/cn'
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = 'horizontal', decorative = true, ...props },
12 | ref,
13 | ) => (
14 |
25 | ),
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils/cn'
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as SwitchPrimitives from '@radix-ui/react-switch'
3 |
4 | import { cn } from '@/lib/utils/cn'
5 |
6 | const Switch = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
23 |
24 | ))
25 | Switch.displayName = SwitchPrimitives.Root.displayName
26 |
27 | export { Switch }
28 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as TabsPrimitive from '@radix-ui/react-tabs'
3 |
4 | import { cn } from '@/lib/utils/cn'
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | TabsList.displayName = TabsPrimitive.List.displayName
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | TabsContent.displayName = TabsPrimitive.Content.displayName
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent }
54 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils/cn'
4 |
5 | export type TextareaProps = React.TextareaHTMLAttributes
6 |
7 | const Textarea = React.forwardRef(
8 | ({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | },
20 | )
21 | Textarea.displayName = 'Textarea'
22 |
23 | export { Textarea }
24 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'
3 |
4 | import { cn } from '@/lib/utils/cn'
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ))
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/websites/SiteTreeView.tsx:
--------------------------------------------------------------------------------
1 | import { type FC, useMemo } from 'react'
2 | import TreeView, { type TreeItemType } from '../shared/TreeView'
3 | import type { TreeItem, TreeItemIndex } from 'react-complex-tree'
4 | import type { Website, Notification } from '@/types'
5 | import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
6 | import { Badge } from '../ui/badge'
7 | import { cn } from '@/lib/utils/cn'
8 |
9 | export type SiteTreeItemType = TreeItemType
10 |
11 | interface Props {
12 | websites: Website[]
13 | onSelect: (website: Website) => void
14 | initialItem: Website
15 | }
16 |
17 | const SiteTreeView: FC = ({ websites, onSelect, initialItem }) => {
18 | const treeItems: Record<
19 | TreeItemIndex,
20 | TreeItem
21 | > = useMemo(() => {
22 | const rootItem: TreeItem = {
23 | index: 'root',
24 | isFolder: true,
25 | children: [],
26 | data: null,
27 | }
28 |
29 | const rootItems: Record> = {
30 | root: rootItem,
31 | }
32 |
33 | for (const website of websites) {
34 | // add api if not added already
35 | rootItems[website.name] = {
36 | index: website.name,
37 | data: {
38 | label: website.name,
39 | data: website,
40 | },
41 | }
42 |
43 | rootItem.children!.push(website.name)
44 | }
45 |
46 | return rootItems
47 | }, [websites])
48 |
49 | return (
50 |
51 | label="Websites"
52 | initialItem={initialItem.name}
53 | items={treeItems}
54 | getItemTitle={(item) => item.data.label}
55 | onPrimaryAction={(items) => {
56 | if (items.data.data) {
57 | onSelect(items.data.data)
58 | }
59 | }}
60 | />
61 | )
62 | }
63 |
64 | export default SiteTreeView
65 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/components/websites/SitesList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type { Website } from '@/types'
3 |
4 | interface SitesListProps {
5 | subsites: Website[]
6 | rootSite: Website
7 | }
8 |
9 | const SitesList: React.FC = ({ rootSite, subsites }) => {
10 | return (
11 |
12 |
Root Site:
13 |
25 | {subsites.length > 0 ? (
26 | <>
27 |
Subsites:
28 |
29 | {subsites.map((website) => (
30 |
45 | ))}
46 |
47 | >
48 | ) : null}
49 |
50 | )
51 | }
52 |
53 | export default SitesList
54 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener('change', onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener('change', onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/hooks/use-params.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | useContext,
4 | useMemo,
5 | useCallback,
6 | useState,
7 | useEffect,
8 | type ReactNode,
9 | } from 'react'
10 |
11 | type ParamsContextValue = {
12 | setParams: (name: string, value: string | null) => void
13 | searchParams: URLSearchParams
14 | }
15 |
16 | const ParamsContext = createContext(null)
17 |
18 | type ParamsProviderProps = {
19 | children: ReactNode
20 | }
21 |
22 | export const ParamsProvider = ({ children }: ParamsProviderProps) => {
23 | const [search, setSearch] = useState(window.location.search)
24 | const pathname = window.location.pathname
25 |
26 | useEffect(() => {
27 | const handlePopState = () => {
28 | setSearch(window.location.search)
29 | }
30 |
31 | window.addEventListener('popstate', handlePopState)
32 | return () => {
33 | window.removeEventListener('popstate', handlePopState)
34 | }
35 | }, [])
36 |
37 | const setParams = useCallback(
38 | (name: string, value: string | null) => {
39 | const latestSearchParams = new URLSearchParams(window.location.search)
40 |
41 | if (!value) {
42 | latestSearchParams.delete(name)
43 | } else {
44 | latestSearchParams.set(name, value)
45 | }
46 |
47 | const updatedSearch = latestSearchParams.toString()
48 | const url = updatedSearch ? `${pathname}?${updatedSearch}` : pathname
49 |
50 | window.history.pushState(null, '', url)
51 | setSearch(updatedSearch ? `?${updatedSearch}` : '')
52 | },
53 | [search, pathname],
54 | )
55 |
56 | const value = useMemo(
57 | () => ({
58 | setParams,
59 | searchParams: new URLSearchParams(search),
60 | }),
61 | [setParams, search],
62 | )
63 |
64 | return (
65 | {children}
66 | )
67 | }
68 |
69 | export const useParams = () => {
70 | const context = useContext(ParamsContext)
71 | if (!context) {
72 | throw new Error('useParams must be used within a ParamsProvider')
73 | }
74 | return context
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/layouts/Layout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Fathom } from "astro-fathom";
3 | import "@fontsource/sora";
4 | import "@fontsource/sora/500.css";
5 | import "@fontsource/sora/600.css";
6 | import "@fontsource/sora/700.css";
7 |
8 | import "@fontsource/jetbrains-mono/500.css";
9 | import "@fontsource/jetbrains-mono/600.css";
10 | import "@fontsource/jetbrains-mono/700.css";
11 |
12 | import 'react-data-grid/lib/styles.css'
13 | import "@/styles/globals.css";
14 | import "@/styles/grid.css";
15 |
16 | export interface Props {
17 | title: string;
18 | }
19 |
20 | const { title } = Astro.props;
21 |
22 | const fathomID = import.meta.env.FATHOM_SITE;
23 | ---
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {title}
33 | {fathomID && }
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/hooks/fetcher.ts:
--------------------------------------------------------------------------------
1 | export const fetcher = (options?: RequestInit) => (url: string) =>
2 | fetch(url, options).then((r) => r.json())
3 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/hooks/use-bucket.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import useSWR from 'swr'
3 | import { fetcher } from './fetcher'
4 | import type { BucketFile } from '../../types'
5 | import { STORAGE_API } from '../constants'
6 |
7 | export const useBucket = (bucket?: string, prefix?: string) => {
8 | const { data, mutate } = useSWR(
9 | bucket && prefix
10 | ? `${STORAGE_API}?action=list-files&bucket=${bucket}`
11 | : null,
12 | fetcher(),
13 | )
14 |
15 | const writeFile = useCallback(
16 | async (file: File) => {
17 | return fetch(
18 | `${STORAGE_API}?action=write-file&bucket=${bucket}&fileKey=${encodeURI(
19 | prefix === '/' ? file.name : prefix + file.name,
20 | )}`,
21 | {
22 | method: 'PUT',
23 | body: file,
24 | },
25 | )
26 | },
27 | [bucket, prefix],
28 | )
29 |
30 | const deleteFile = useCallback(
31 | (fileKey: string) => {
32 | return fetch(
33 | `${STORAGE_API}?action=delete-file&bucket=${bucket}&fileKey=${encodeURI(
34 | fileKey,
35 | )}`,
36 | {
37 | method: 'DELETE',
38 | },
39 | )
40 | },
41 | [bucket, prefix],
42 | )
43 |
44 | return {
45 | data,
46 | mutate,
47 | deleteFile,
48 | writeFile,
49 | loading: !data,
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/hooks/use-history.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { History } from '../../types'
3 | import { getHost } from '../utils'
4 | import useSWRSubscription from 'swr/subscription'
5 | import toast from 'react-hot-toast'
6 |
7 | export function useHistory(recordType: string) {
8 | const host = getHost()
9 |
10 | const { data, error } = useSWRSubscription(
11 | host ? `ws://${host}/history` : null,
12 | (key, { next }) => {
13 | const socket = new WebSocket(key)
14 |
15 | socket.addEventListener('message', (event) => {
16 | const message = JSON.parse(event.data) as History
17 |
18 | next(null, message)
19 | })
20 |
21 | socket.addEventListener('error', (event: any) => next(event.error))
22 | return () => socket.close()
23 | },
24 | )
25 |
26 | const deleteHistory = useCallback(async () => {
27 | const resp = await fetch(
28 | `http://${host}/api/history?type=${recordType.toLowerCase()}`,
29 | {
30 | method: 'DELETE',
31 | },
32 | )
33 |
34 | if (resp.ok) {
35 | toast.success('Cleared History')
36 | }
37 | }, [recordType])
38 |
39 | if (error) {
40 | console.error(error)
41 | }
42 |
43 | return {
44 | data: data as History | null,
45 | deleteHistory,
46 | loading: !data,
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/hooks/use-logs.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import useSWR from 'swr'
3 | import { fetcher } from './fetcher'
4 | import type { LogEntry } from '@/types'
5 | import { LOGS_API } from '../constants'
6 |
7 | interface LogQueryParams {
8 | origin?: string
9 | timeline?: string
10 | level?: LogEntry['level']
11 | search?: string
12 | }
13 |
14 | const buildQueryString = (params: LogQueryParams) => {
15 | const searchParams = new URLSearchParams()
16 | Object.entries(params).forEach(([key, value]) => {
17 | if (value !== undefined && value !== null) {
18 | searchParams.append(key, String(value))
19 | }
20 | })
21 | return searchParams.toString()
22 | }
23 |
24 | export const useLogs = (query: LogQueryParams) => {
25 | // Build query string dynamically
26 | const queryString = buildQueryString(query)
27 |
28 | // build query string
29 | const { data, mutate } = useSWR(
30 | `${LOGS_API}?${queryString}`,
31 | fetcher(),
32 | {
33 | refreshInterval: 250,
34 | },
35 | )
36 |
37 | const purgeLogs = useCallback(async () => {
38 | await fetch(LOGS_API, {
39 | method: 'DELETE',
40 | })
41 |
42 | return mutate()
43 | }, [])
44 |
45 | return {
46 | data: data || [],
47 | mutate,
48 | purgeLogs,
49 | loading: !data,
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/hooks/use-secret.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import useSWR from 'swr'
3 | import { fetcher } from './fetcher'
4 | import type { SecretVersion } from '@/types'
5 | import { SECRETS_API } from '../constants'
6 |
7 | export const useSecret = (secretName?: string) => {
8 | const { data, mutate } = useSWR(
9 | secretName
10 | ? `${SECRETS_API}?action=list-versions&secret=${secretName}`
11 | : null,
12 | fetcher(),
13 | )
14 |
15 | const addSecretVersion = useCallback(
16 | async (value: string) => {
17 | return fetch(
18 | `${SECRETS_API}?action=add-secret-version&secret=${secretName}`,
19 | {
20 | method: 'POST',
21 | body: JSON.stringify({ value }),
22 | },
23 | )
24 | },
25 | [secretName],
26 | )
27 |
28 | const deleteSecretVersion = useCallback(
29 | async (sv: SecretVersion) => {
30 | return fetch(
31 | `${SECRETS_API}?action=delete-secret&secret=${secretName}&version=${sv.version}&latest=${sv.latest}`,
32 | {
33 | method: 'DELETE',
34 | },
35 | )
36 | },
37 | [secretName],
38 | )
39 |
40 | return {
41 | data,
42 | mutate,
43 | addSecretVersion,
44 | deleteSecretVersion,
45 | loading: !data,
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/hooks/use-sql-meta.ts:
--------------------------------------------------------------------------------
1 | import useSWR from 'swr'
2 | import { fetcher } from './fetcher'
3 | import { SQL_API, TABLE_QUERY } from '../constants'
4 |
5 | export interface SqlMetaResult {
6 | columns: {
7 | column_name: string
8 | data_type: string
9 | column_order: number
10 | }[]
11 | is_table: boolean
12 | qualified_name: string
13 | schema_name: string
14 | table_name: string
15 | }
16 |
17 | export const useSqlMeta = (connectionString?: string) => {
18 | const { data, mutate } = useSWR(
19 | connectionString ? SQL_API : null,
20 | fetcher({
21 | method: 'POST',
22 | body: JSON.stringify({ query: TABLE_QUERY, connectionString }),
23 | }),
24 | )
25 |
26 | return {
27 | data,
28 | mutate,
29 | loading: !data,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/copy-to-clipboard.ts:
--------------------------------------------------------------------------------
1 | export const copyToClipboard = (str: string) => {
2 | const focused = window.document.hasFocus()
3 | if (focused) {
4 | window.navigator?.clipboard?.writeText(str)
5 | } else {
6 | console.warn('Unable to copy to clipboard')
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/flatten-paths.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIV3 } from 'openapi-types'
2 | import type { Endpoint, Method, Param } from '../../types'
3 | import { uniqBy } from '../utils'
4 |
5 | export function flattenPaths(doc: OpenAPIV3.Document): Endpoint[] {
6 | const uniquePaths: Record = {}
7 | const params: Param[] = []
8 |
9 | Object.entries(doc.paths).forEach(([path, pathItem]) => {
10 | Object.entries(pathItem as any).forEach(([method, value]) => {
11 | if (method === 'parameters') {
12 | params.push({
13 | path,
14 | value: value as OpenAPIV3.ParameterObject[],
15 | })
16 | return
17 | }
18 |
19 | // Get the service that is requesting this endpoint
20 | const requestingService = (doc.paths[path] as any)?.[method]?.[
21 | 'x-nitric-target'
22 | ]?.['name']
23 |
24 | method = method.toUpperCase()
25 | const key = `${doc.info.title}-${path}-${method}`
26 | const endpoint: Endpoint = {
27 | id: key,
28 | api: doc.info.title,
29 | path,
30 | method: method as Method,
31 | doc,
32 | requestingService,
33 | }
34 |
35 | uniquePaths[key] = endpoint
36 | })
37 | })
38 |
39 | return uniqBy(
40 | Object.entries(uniquePaths).map(([_, value]) => {
41 | const param = params.find((param) => param.path == value.path)
42 |
43 | if (param) {
44 | value.params = value.params ? [...value.params, param] : [param]
45 | }
46 |
47 | return value
48 | }),
49 | 'id',
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/format-file-size.ts:
--------------------------------------------------------------------------------
1 | export function formatFileSize(bytes: number): string {
2 | const units = ['bytes', 'KB', 'MB', 'GB', 'TB']
3 | let size = bytes
4 | let unitIndex = 0
5 |
6 | while (size >= 1024 && unitIndex < units.length - 1) {
7 | size /= 1024
8 | unitIndex++
9 | }
10 |
11 | const unit = units[unitIndex]
12 |
13 | return `${bytes > 1024 ? size.toFixed(2) : size} ${unit}`
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/format-response-time.ts:
--------------------------------------------------------------------------------
1 | export function formatResponseTime(milliseconds: number): string {
2 | if (milliseconds < 1000) {
3 | return milliseconds.toFixed(2) + ' ms'
4 | } else if (milliseconds < 60 * 1000) {
5 | return (milliseconds / 1000).toFixed(2) + ' s'
6 | } else if (milliseconds < 60 * 60 * 1000) {
7 | return (milliseconds / (60 * 1000)).toFixed(2) + ' m'
8 | } else if (milliseconds < 24 * 60 * 60 * 1000) {
9 | return (milliseconds / (60 * 60 * 1000)).toFixed(2) + ' h'
10 | } else {
11 | return (milliseconds / (24 * 60 * 60 * 1000)).toFixed(2) + ' d'
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/generate-path-params.ts:
--------------------------------------------------------------------------------
1 | import type { FieldRow } from '../../components/shared'
2 | import type { APIRequest, Endpoint } from '../../types'
3 |
4 | export const generatePathParams = (endpoint: Endpoint, request: APIRequest) => {
5 | const pathParams: FieldRow[] = []
6 |
7 | if (endpoint.params?.length) {
8 | endpoint.params.forEach((p) => {
9 | p.value.forEach((v) => {
10 | if (v.in === 'path') {
11 | const existing = request.pathParams.find((pp) => pp.key === v.name)
12 |
13 | pathParams.push({
14 | key: v.name,
15 | value: existing?.value || '',
16 | })
17 | }
18 | })
19 | })
20 | }
21 |
22 | return pathParams
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/generate-path.ts:
--------------------------------------------------------------------------------
1 | import type { FieldRow } from '../../components/shared'
2 |
3 | export function generatePath(
4 | path: string,
5 | pathParams: FieldRow[],
6 | queryParams: FieldRow[],
7 | ) {
8 | pathParams.forEach((p) => {
9 | path = path.replace(`{${p.key}}`, p.value)
10 | })
11 |
12 | if (queryParams.length) {
13 | const searchParams = new URLSearchParams()
14 |
15 | queryParams.forEach((param) => {
16 | if (param.key) {
17 | searchParams.append(param.key, param.value)
18 | }
19 | })
20 |
21 | const queryPath = searchParams.toString()
22 |
23 | if (queryPath) {
24 | path = `${path}?${queryPath.replace(/^(\?)/, '')}`
25 | }
26 | }
27 |
28 | return path
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/generate-response.ts:
--------------------------------------------------------------------------------
1 | import { formatJSON, headersToObject } from '../utils'
2 |
3 | export const generateResponse = async (res: Response, startTime: number) => {
4 | const contentType = res.headers.get('Content-Type')
5 |
6 | let data
7 |
8 | if (contentType === 'application/json') {
9 | data = formatJSON(await res.text())
10 | } else if (
11 | contentType?.startsWith('image/') ||
12 | contentType?.startsWith('video/') ||
13 | contentType?.startsWith('audio/') ||
14 | contentType?.startsWith('application')
15 | ) {
16 | const blob = await res.blob()
17 | const url = URL.createObjectURL(blob)
18 |
19 | data = url
20 | } else {
21 | data = await res.text()
22 | }
23 |
24 | const endTime = window.performance.now()
25 | const responseSize = res.headers.get('Content-Length')
26 |
27 | return {
28 | data,
29 | time: endTime - startTime,
30 | status: res.status,
31 | size: responseSize ? parseInt(responseSize) : 0,
32 | headers: headersToObject(res.headers),
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/get-bucket-notifications.ts:
--------------------------------------------------------------------------------
1 | import type { Bucket, Notification } from '@/types'
2 |
3 | export const getBucketNotifications = (
4 | bucket: Bucket,
5 | notifications: Notification[],
6 | ) => notifications.filter((n) => n.bucket === bucket.name)
7 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/get-date-string.ts:
--------------------------------------------------------------------------------
1 | // Returns a user friendly time representation
2 | export const getDateString = (requestTime: number) => {
3 | const currentDate = new Date()
4 | const requestDate = new Date(requestTime)
5 |
6 | const outputTimeString = (time: number, word: string) =>
7 | `${time} ${time > 1 ? word + 's' : word} ago`
8 |
9 | // Time diff is initially the difference in milliseconds, so convert to seconds
10 | const secondsDifference =
11 | (currentDate.getTime() - requestDate.getTime()) / 1000
12 | // Time is less than a minute
13 | if (secondsDifference < 60) {
14 | return 'just now'
15 | }
16 | // Time is less than an hour
17 | if (secondsDifference < 3600) {
18 | return outputTimeString(Math.floor(secondsDifference / 60), 'min')
19 | }
20 | // Time is less than a day
21 | if (secondsDifference < 86400) {
22 | return outputTimeString(Math.floor(secondsDifference / 3600), 'hour')
23 | }
24 | // Time is less than a week
25 | if (secondsDifference < 604800) {
26 | return outputTimeString(Math.floor(secondsDifference / 86400), 'day')
27 | }
28 | // Time is less than a month
29 | if (secondsDifference < 2630000) {
30 | return outputTimeString(Math.floor(secondsDifference / 604800), 'week')
31 | }
32 | // Time is less than a year
33 | if (secondsDifference < 31536000) {
34 | return outputTimeString(Math.floor(secondsDifference / 2630000), 'month')
35 | }
36 | // Time is greater than a year
37 | return outputTimeString(Math.floor(secondsDifference / 31536000), 'year')
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/get-file-extension.ts:
--------------------------------------------------------------------------------
1 | export function getFileExtension(contentType: string): string {
2 | switch (contentType) {
3 | case 'application/xml':
4 | case 'text/xml':
5 | return '.xml'
6 | case 'application/json':
7 | return '.json'
8 | case 'application/pdf':
9 | return '.pdf'
10 | case 'application/zip':
11 | return '.zip'
12 | case 'application/octet-stream':
13 | return '.bin'
14 | default:
15 | return ''
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/get-host.ts:
--------------------------------------------------------------------------------
1 | const isDev = import.meta.env.DEV
2 |
3 | export const getHost = () => {
4 | if (typeof window === 'undefined') {
5 | return null
6 | }
7 |
8 | return isDev ? 'localhost:49152' : window.location.host
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/get-topic-subscriptions.ts:
--------------------------------------------------------------------------------
1 | import type { Topic, Subscriber } from '@/types'
2 |
3 | export const getTopicSubscriptions = (
4 | topic: Topic,
5 | subscriptions: Subscriber[],
6 | ) => subscriptions.filter((n) => n.topic === topic.name)
7 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/headers.ts:
--------------------------------------------------------------------------------
1 | import type { FieldRow } from '../../components/shared/FieldRows'
2 |
3 | export const headersToObject = (headers: Headers): Record => {
4 | return Array.from(headers.entries()).reduce(
5 | (acc, [key, value]) => {
6 | acc[key] = value
7 | return acc
8 | },
9 | {} as Record,
10 | )
11 | }
12 |
13 | export const fieldRowArrToHeaders = (arr: FieldRow[]) => {
14 | const headers = new Headers()
15 | arr.forEach((obj) => {
16 | if (obj.key) {
17 | headers.append(obj.key, obj.value)
18 | }
19 | })
20 | return headers
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './flatten-paths'
2 | export * from './format-file-size'
3 | export * from './format-response-time'
4 | export * from './generate-path-params'
5 | export * from './generate-path'
6 | export * from './get-file-extension'
7 | export * from './get-date-string'
8 | export * from './headers'
9 | export * from './uniq'
10 | export * from './get-host'
11 | export * from './generate-response'
12 | export * from './strings'
13 | export * from './cn'
14 | export * from './is-valid-url'
15 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/is-valid-url.ts:
--------------------------------------------------------------------------------
1 | export const isValidUrl = (value: string) => {
2 | try {
3 | return !decodeURI(value).includes('%')
4 | } catch (e) {
5 | return false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/strings.ts:
--------------------------------------------------------------------------------
1 | export const formatJSON = (
2 | value: any,
3 | space: string | number | undefined = 2,
4 | ) => {
5 | try {
6 | if (typeof value === 'string') {
7 | value = JSON.parse(value)
8 | }
9 |
10 | return JSON.stringify(value, null, space)
11 | } catch (e) {
12 | return value
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/lib/utils/uniq.ts:
--------------------------------------------------------------------------------
1 | export const sortedUniq = (arr: any[]) => [...new Set(arr)].sort()
2 |
3 | export const uniqBy = (arr: any[], iteratee: any) => {
4 | if (typeof iteratee === 'string') {
5 | const prop = iteratee
6 | iteratee = (item: any) => item[prop]
7 | }
8 |
9 | return arr.filter(
10 | (x, i, self) => i === self.findIndex((y) => iteratee(x) === iteratee(y)),
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/architecture.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import ArchitectureExplorer from "../components/architecture";
3 | import Layout from "../layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/databases.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import DatabasesExplorer from "@/components/databases/DatabasesExplorer";
3 | import Layout from "@/layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import APIExplorer from "../components/apis/APIExplorer";
3 | import Layout from "../layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/jobs.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import EventExplorer from "../components/events/EventsExplorer";
3 | import Layout from "../layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/logs.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Layout from "../layouts/Layout.astro";
3 | import LogsExplorer from "@/components/logs/LogsExplorer";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/not-found.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { buttonVariants } from "@/components/ui/button";
3 | import Layout from "../layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
10 |
11 |
404
12 |
15 | Route not found
16 |
17 |
18 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/schedules.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import EventExplorer from "../components/events/EventsExplorer";
3 | import Layout from "../layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/secrets.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import SecretsExplorer from "@/components/secrets/SecretsExplorer";
3 | import Layout from "@/layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/storage.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import StorageExplorer from "../components/storage/StorageExplorer";
3 | import Layout from "../layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/topics.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import EventExplorer from "../components/events/EventsExplorer";
3 | import Layout from "../layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/websites.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import SiteExplorer from "@/components/websites/SiteExplorer";
3 | import Layout from "../layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/pages/websockets.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import WSExplorer from "../components/websockets/WSExplorer";
3 | import Layout from "../layouts/Layout.astro";
4 | ---
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/src/styles/grid.css:
--------------------------------------------------------------------------------
1 | .rdg {
2 | @apply box-border h-full overflow-x-auto overflow-y-scroll rounded-lg bg-code;
3 | }
4 |
5 | .rdg *,
6 | .rdg ::after,
7 | .rdg ::before {
8 | box-sizing: inherit;
9 | }
10 |
11 | .rdg-cell {
12 | @apply flex flex border-b border-r border-gray-600 bg-code text-sm text-gray-200;
13 | white-space: nowrap;
14 | overflow: hidden;
15 | text-overflow: ellipsis;
16 | line-height: inherit;
17 | }
18 |
19 | .rdg-header-row .rdg-cell {
20 | @apply bg-gray-700 text-gray-100;
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .nitric/
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ## About Nitric
4 |
5 | This is a [Nitric](https://nitric.io) TypeScript project, but Nitric is a framework for rapid development of cloud-native and serverless applications in many languages.
6 |
7 | Using Nitric you define your apps in terms of the resources they need, then write the code for serverless function based APIs, event subscribers and scheduled jobs.
8 |
9 | Apps built with Nitric can be deployed to AWS, Azure or Google Cloud all from the same code base so you can focus on your products, not your cloud provider.
10 |
11 | Nitric makes it easy to:
12 |
13 | - Create smart [serverless functions and APIs](https://nitric.io/docs/apis)
14 | - Build reliable distributed apps that use [events](https://nitric.io/docs/messaging/topics) and/or [queues](https://nitric.io/docs/messaging/queues)
15 | - Securely store, retrieve and rotate [secrets](https://nitric.io/docs/secrets)
16 | - Read and write files from [buckets](https://nitric.io/docs/storage)
17 |
18 | ## Learning Nitric
19 |
20 | Nitric provides detailed and intuitive [documentation](https://nitric.io/docs) and [guides](https://nitric.io/docs/getting-started) to help you get started quickly.
21 |
22 | If you'd rather chat with the maintainers or community, come and join our [Discord](https://nitric.io/chat) server, [GitHub Discussions](https://github.com/nitrictech/nitric/discussions) or find us on [Twitter](https://twitter.com/nitric_io).
23 |
24 | ## Running this project
25 |
26 | To run this project you'll need the [Nitric CLI](https://nitric.io/docs/installation) installed, then you can use the CLI commands to run, build or deploy the project.
27 |
28 | You'll also want to make sure the project's required dependencies have been installed.
29 |
30 | ```bash
31 | # install dependencies
32 | npm install
33 |
34 | # run locally
35 | npm run dev
36 | ```
37 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/docs-website/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/docs-website/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/docs-website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs-website",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "devDependencies": {
12 | "vite": "^6.1.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/docs-website/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/docs-website/src/counter.js:
--------------------------------------------------------------------------------
1 | export function setupCounter(element) {
2 | let counter = 0
3 | const setCounter = (count) => {
4 | counter = count
5 | element.innerHTML = `count is ${counter}`
6 | }
7 | element.addEventListener('click', () => setCounter(counter + 1))
8 | setCounter(0)
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/docs-website/src/javascript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/docs-website/src/main.js:
--------------------------------------------------------------------------------
1 | import './style.css'
2 | import javascriptLogo from './javascript.svg'
3 | import viteLogo from '/vite.svg'
4 | import { setupCounter } from './counter.js'
5 |
6 | document.querySelector('#app').innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Hello Nitric Docs Test!
15 |
16 |
17 |
18 |
19 | Click on the Vite logo to learn more
20 |
21 |
22 | `
23 |
24 | setupCounter(document.querySelector('#counter'))
25 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/docs-website/src/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | a {
17 | font-weight: 500;
18 | color: #646cff;
19 | text-decoration: inherit;
20 | }
21 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | #app {
39 | max-width: 1280px;
40 | margin: 0 auto;
41 | padding: 2rem;
42 | text-align: center;
43 | }
44 |
45 | .logo {
46 | height: 6em;
47 | padding: 1.5em;
48 | will-change: filter;
49 | transition: filter 300ms;
50 | }
51 | .logo:hover {
52 | filter: drop-shadow(0 0 2em #646cffaa);
53 | }
54 | .logo.vanilla:hover {
55 | filter: drop-shadow(0 0 2em #f7df1eaa);
56 | }
57 |
58 | .card {
59 | padding: 2em;
60 | }
61 |
62 | .read-the-docs {
63 | color: #888;
64 | }
65 |
66 | button {
67 | border-radius: 8px;
68 | border: 1px solid transparent;
69 | padding: 0.6em 1.2em;
70 | font-size: 1em;
71 | font-weight: 500;
72 | font-family: inherit;
73 | background-color: #1a1a1a;
74 | cursor: pointer;
75 | transition: border-color 0.25s;
76 | }
77 | button:hover {
78 | border-color: #646cff;
79 | }
80 | button:focus,
81 | button:focus-visible {
82 | outline: 4px auto -webkit-focus-ring-color;
83 | }
84 |
85 | @media (prefers-color-scheme: light) {
86 | :root {
87 | color: #213547;
88 | background-color: #ffffff;
89 | }
90 | a:hover {
91 | color: #747bff;
92 | }
93 | button {
94 | background-color: #f9f9f9;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/local.nitric.yaml:
--------------------------------------------------------------------------------
1 | # local.nitric.yaml
2 | # Local development configuration for Nitric services.
3 |
4 | apis:
5 | # API Resources
6 | first-api:
7 | port: 6001
8 |
9 | second-api:
10 | port: 6002
11 |
12 | my-db-api:
13 | port: 6003
14 |
15 | my-secret-api:
16 | port: 6004
17 |
18 | websockets:
19 | # WebSocket Resources
20 | socket:
21 | port: 7001
22 |
23 | socket-2:
24 | port: 7002
25 |
26 | socket-3:
27 | port: 7003
28 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/migrations/my-db/1_create_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE my_migration_table (
2 | id SERIAL PRIMARY KEY,
3 | name TEXT NOT NULL DEFAULT ''
4 | );
5 |
6 | -- seed some data
7 | INSERT INTO my_migration_table (name) VALUES ('my-db-foo');
8 | INSERT INTO my_migration_table (name) VALUES ('my-db-bar');
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/migrations/my-second-db/1_create_table.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE my_migration_table (
2 | id SERIAL PRIMARY KEY,
3 | name TEXT NOT NULL DEFAULT ''
4 | );
5 |
6 | -- seed some data
7 | INSERT INTO my_migration_table (name) VALUES ('my-second-db-foo');
8 | INSERT INTO my_migration_table (name) VALUES ('my-second-db-bar');
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/nitric.yaml:
--------------------------------------------------------------------------------
1 | name: test-app
2 | services:
3 | - match: ./services/*.ts
4 | start: yarn dev:functions $SERVICE_PATH
5 | websites:
6 | - basedir: ./vite-website
7 | build:
8 | command: yarn build
9 | output: ./dist
10 | dev:
11 | command: yarn dev --port 7850
12 | url: http://localhost:7850
13 | - basedir: ./docs-website
14 | path: /docs
15 | build:
16 | command: yarn build --base=/docs
17 | output: ./dist
18 | dev:
19 | command: yarn dev --port 7851 --base=/docs
20 | url: http://localhost:7851/docs
21 | preview:
22 | - sql-databases
23 | - websites
24 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typescript-starter",
3 | "version": "1.0.0",
4 | "description": "nitric typescript starter template",
5 | "private": true,
6 | "dependencies": {
7 | "@nitric/sdk": "^1.3.0",
8 | "express": "^4.18.2",
9 | "pg": "^8.12.0"
10 | },
11 | "engines": {
12 | "node": ">=20.0.0"
13 | },
14 | "devDependencies": {
15 | "@types/express": "^4.17.21",
16 | "@types/pg": "^8.11.6",
17 | "dotenv": "^16.0.2",
18 | "nodemon": "^3.1.4",
19 | "ts-node": "^10.9.1",
20 | "typescript": "^4.8.3"
21 | },
22 | "scripts": {
23 | "dev:functions": "nodemon -r dotenv/config",
24 | "install:websites": "yarn --cwd ./vite-website install && yarn --cwd ./docs-website install"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/services/my-test-db.ts:
--------------------------------------------------------------------------------
1 | import { api, sql } from '@nitric/sdk'
2 | import pg from 'pg'
3 | const { Client } = pg
4 |
5 | const myDb = sql('my-db', {
6 | migrations: 'file://migrations/my-db',
7 | })
8 | const mySecondDb = sql('my-second-db', {
9 | migrations: 'file://migrations/my-second-db',
10 | })
11 |
12 | const dbApi = api('my-db-api')
13 |
14 | const getClient = async () => {
15 | const connStr = await myDb.connectionString()
16 | const client = new Client(connStr)
17 |
18 | return client
19 | }
20 |
21 | dbApi.get('/get', async (ctx) => {
22 | const client = await getClient()
23 |
24 | const res = await client.query('SELECT $1::text as message', ['Hello world!'])
25 | await client.end()
26 |
27 | return ctx.res.json(res.rows[0].message)
28 | })
29 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/services/my-test-secret.ts:
--------------------------------------------------------------------------------
1 | import { api, secret } from '@nitric/sdk'
2 |
3 | const mySecret = secret('my-first-secret').allow('access', 'put')
4 |
5 | const mySecondSecret = secret('my-second-secret').allow('access', 'put')
6 |
7 | const shhApi = api('my-secret-api')
8 |
9 | shhApi.get('/get', async (ctx) => {
10 | const latestValue = await mySecret.latest().access()
11 |
12 | ctx.res.body = latestValue.asString()
13 |
14 | return ctx
15 | })
16 |
17 | shhApi.post('/set', async (ctx) => {
18 | const data = ctx.req.json()
19 |
20 | await mySecret.put(JSON.stringify(data))
21 |
22 | return ctx
23 | })
24 |
25 | shhApi.post('/set-binary', async (ctx) => {
26 | const data = new Uint8Array(1024)
27 | for (let i = 0; i < data.length; i++) {
28 | data[i] = i % 256
29 | }
30 |
31 | await mySecret.put(data)
32 |
33 | return ctx
34 | })
35 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "target": "ESNext",
5 | "moduleResolution": "node",
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/vite-website/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/vite-website/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/vite-website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-website",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "devDependencies": {
12 | "vite": "^6.1.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/vite-website/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/vite-website/src/counter.js:
--------------------------------------------------------------------------------
1 | export function setupCounter(element) {
2 | let counter = 0
3 | const setCounter = (count) => {
4 | counter = count
5 | element.innerHTML = `count is ${counter}`
6 | }
7 | element.addEventListener('click', () => setCounter(counter + 1))
8 | setCounter(0)
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/vite-website/src/javascript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/vite-website/src/main.js:
--------------------------------------------------------------------------------
1 | import './style.css'
2 | import javascriptLogo from './javascript.svg'
3 | import viteLogo from '/vite.svg'
4 | import { setupCounter } from './counter.js'
5 |
6 | document.querySelector('#app').innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Hello Nitric!
15 |
16 |
17 |
18 |
19 | Click on the Vite logo to learn more
20 |
21 |
22 | `
23 |
24 | setupCounter(document.querySelector('#counter'))
25 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/test-app/vite-website/src/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | a {
17 | font-weight: 500;
18 | color: #646cff;
19 | text-decoration: inherit;
20 | }
21 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | #app {
39 | max-width: 1280px;
40 | margin: 0 auto;
41 | padding: 2rem;
42 | text-align: center;
43 | }
44 |
45 | .logo {
46 | height: 6em;
47 | padding: 1.5em;
48 | will-change: filter;
49 | transition: filter 300ms;
50 | }
51 | .logo:hover {
52 | filter: drop-shadow(0 0 2em #646cffaa);
53 | }
54 | .logo.vanilla:hover {
55 | filter: drop-shadow(0 0 2em #f7df1eaa);
56 | }
57 |
58 | .card {
59 | padding: 2em;
60 | }
61 |
62 | .read-the-docs {
63 | color: #888;
64 | }
65 |
66 | button {
67 | border-radius: 8px;
68 | border: 1px solid transparent;
69 | padding: 0.6em 1.2em;
70 | font-size: 1em;
71 | font-weight: 500;
72 | font-family: inherit;
73 | background-color: #1a1a1a;
74 | cursor: pointer;
75 | transition: border-color 0.25s;
76 | }
77 | button:hover {
78 | border-color: #646cff;
79 | }
80 | button:focus,
81 | button:focus-visible {
82 | outline: 4px auto -webkit-focus-ring-color;
83 | }
84 |
85 | @media (prefers-color-scheme: light) {
86 | :root {
87 | color: #213547;
88 | background-color: #ffffff;
89 | }
90 | a:hover {
91 | color: #747bff;
92 | }
93 | button {
94 | background-color: #f9f9f9;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/dashboard/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "jsxImportSource": "react",
6 | "baseUrl": ".",
7 | "paths": {
8 | "@/*": ["./src/*"],
9 | },
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/env/env.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package env
18 |
19 | import (
20 | "os"
21 |
22 | "github.com/joho/godotenv"
23 | )
24 |
25 | var defaultEnv = ".env"
26 |
27 | func ReadEnv(filePath string) (map[string]string, error) {
28 | file, err := os.OpenFile(filePath, os.O_RDONLY, 0o666)
29 | if err != nil {
30 | return nil, err
31 | }
32 |
33 | return godotenv.Parse(file)
34 | }
35 |
36 | func ReadLocalEnv(additionalFilePaths ...string) (map[string]string, error) {
37 | envVariables, err := ReadEnv(defaultEnv)
38 | if err != nil && !os.IsNotExist(err) {
39 | return nil, err
40 | }
41 |
42 | if envVariables == nil {
43 | envVariables = map[string]string{}
44 | }
45 |
46 | for _, filePath := range additionalFilePaths {
47 | additionalEnvVariables, err := ReadEnv(filePath)
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | for key, value := range additionalEnvVariables {
53 | envVariables[key] = value
54 | }
55 | }
56 |
57 | return envVariables, nil
58 | }
59 |
60 | func LoadLocalEnv(additionalFilePaths ...string) error {
61 | paths := append(additionalFilePaths, defaultEnv)
62 | return godotenv.Load(paths...)
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/eventbus/eventbus.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package eventbus
18 |
19 | import "github.com/asaskevich/EventBus"
20 |
21 | var bus EventBus.Bus
22 |
23 | func Bus() EventBus.Bus {
24 | if bus == nil {
25 | bus = EventBus.New()
26 | }
27 |
28 | return bus
29 | }
30 |
31 | var topicBus EventBus.Bus
32 |
33 | func TopicBus() EventBus.Bus {
34 | if topicBus == nil {
35 | topicBus = EventBus.New()
36 | }
37 |
38 | return topicBus
39 | }
40 |
41 | var storageBus EventBus.Bus
42 |
43 | func StorageBus() EventBus.Bus {
44 | if storageBus == nil {
45 | storageBus = EventBus.New()
46 | }
47 |
48 | return storageBus
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/iox/channelwriter.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package iox
18 |
19 | import (
20 | "io"
21 | "strings"
22 | )
23 |
24 | type channelWriter struct {
25 | out chan<- string
26 | }
27 |
28 | func (cw channelWriter) Write(bytes []byte) (int, error) {
29 | lines := strings.Split(string(bytes), "\n")
30 |
31 | for _, line := range lines {
32 | if strings.TrimSpace(line) == "" {
33 | continue
34 | }
35 | cw.out <- line
36 | }
37 |
38 | return len(bytes), nil
39 | }
40 |
41 | func NewChannelWriter(channel chan<- string) io.Writer {
42 | return channelWriter{
43 | out: channel,
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/netx/freeport.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package netx
18 |
19 | import (
20 | "bufio"
21 | "os"
22 | "strings"
23 |
24 | "github.com/hashicorp/consul/sdk/freeport"
25 |
26 | "github.com/nitrictech/nitric/core/pkg/logger"
27 | )
28 |
29 | // TakePort is just a wrapper around freeport.TakePort() that changes the
30 | // stderr output to pterm.Debug
31 | func TakePort(n int) ([]int, error) {
32 | r, w, err := os.Pipe()
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | defer r.Close()
38 |
39 | stderr := os.Stderr
40 | os.Stderr = w
41 | ports, err := freeport.Take(n)
42 | os.Stderr = stderr
43 |
44 | w.Close()
45 |
46 | in := bufio.NewScanner(r)
47 |
48 | for in.Scan() {
49 | logger.Debug(strings.TrimPrefix(in.Text(), "[INFO] "))
50 | }
51 |
52 | return ports, err
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/pflagx/string_enum.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package pflagx
18 |
19 | import (
20 | "fmt"
21 | "strings"
22 | )
23 |
24 | type stringEnum struct {
25 | Allowed []string
26 | ValueP *string
27 | }
28 |
29 | // NewStringEnumVar give a list of allowed flag parameters, where the second argument is the default
30 | func NewStringEnumVar(value *string, allowed []string, d string) *stringEnum {
31 | *value = d
32 |
33 | return &stringEnum{
34 | Allowed: allowed,
35 | ValueP: value,
36 | }
37 | }
38 |
39 | func (e *stringEnum) String() string {
40 | return *e.ValueP
41 | }
42 |
43 | func (e *stringEnum) Set(p string) error {
44 | isIncluded := func(opts []string, val string) bool {
45 | for _, opt := range opts {
46 | if val == opt {
47 | return true
48 | }
49 | }
50 |
51 | return false
52 | }
53 |
54 | if !isIncluded(e.Allowed, p) {
55 | return fmt.Errorf("%s is not included in %s", p, strings.Join(e.Allowed, ","))
56 | }
57 |
58 | *e.ValueP = p
59 |
60 | return nil
61 | }
62 |
63 | func (e *stringEnum) Type() string {
64 | return "stringEnumVar"
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/ports/ports.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package ports
18 |
19 | import (
20 | "bufio"
21 | "os"
22 | "strings"
23 |
24 | "github.com/hashicorp/consul/sdk/freeport"
25 |
26 | "github.com/nitrictech/nitric/core/pkg/logger"
27 | )
28 |
29 | // Take is just a wrapper around freeport.Take() that changes the
30 | // stderr output to pterm.Debug
31 | func Take(n int) ([]int, error) {
32 | r, w, err := os.Pipe()
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | defer r.Close()
38 |
39 | stderr := os.Stderr
40 | os.Stderr = w
41 | ports, err := freeport.Take(n)
42 | os.Stderr = stderr
43 |
44 | w.Close()
45 |
46 | in := bufio.NewScanner(r)
47 |
48 | for in.Scan() {
49 | logger.Debug(strings.TrimPrefix(in.Text(), "[INFO] "))
50 | }
51 |
52 | return ports, err
53 | }
54 |
--------------------------------------------------------------------------------
/pkg/preview/feature.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package preview
18 |
19 | type Feature = string
20 |
21 | const (
22 | Feature_DockerProviders Feature = "docker-providers"
23 | Feature_BetaProviders Feature = "beta-providers"
24 | Feature_SqlDatabases Feature = "sql-databases"
25 | Feature_BatchServices Feature = "batch-services"
26 | Feature_Websites Feature = "websites"
27 | )
28 |
--------------------------------------------------------------------------------
/pkg/project/dockerhost/dockerhost.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package dockerhost
18 |
19 | import (
20 | goruntime "runtime"
21 |
22 | "github.com/nitrictech/nitric/core/pkg/env"
23 | )
24 |
25 | func GetInternalDockerHost() string {
26 | dockerHost := "host.docker.internal"
27 |
28 | if goruntime.GOOS == "linux" {
29 | host := env.GetEnv("NITRIC_DOCKER_HOST", "172.17.0.1")
30 |
31 | return host.String()
32 | }
33 |
34 | return dockerHost
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/project/runtime/csharp.dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
2 |
3 | # https://github.com/dotnet/runtime/issues/94909
4 | ENV DOTNET_EnableWriteXorExecute=0
5 |
6 | ARG HANDLER
7 |
8 | WORKDIR /app
9 |
10 | # Copy everything
11 | COPY . ./
12 |
13 | # Build and publish a release
14 | RUN dotnet publish -c Release -o out --self-contained -p:PublishSingleFile=true
15 |
16 | # Build runtime image
17 | FROM mcr.microsoft.com/dotnet/runtime-deps:8.0
18 |
19 | ARG HANDLER
20 |
21 | COPY --from=build /app/out/${HANDLER} /usr/bin/handler
22 |
23 | ENTRYPOINT ["handler"]
--------------------------------------------------------------------------------
/pkg/project/runtime/dart.dockerfile:
--------------------------------------------------------------------------------
1 | FROM dart:stable AS build
2 |
3 | ARG HANDLER
4 | WORKDIR /app
5 |
6 | # Resolve app dependencies.
7 | COPY pubspec.* ./
8 | RUN dart pub get
9 |
10 | # Ensure the ./bin folder exists
11 | RUN mkdir -p ./bin
12 |
13 | # Copy app source code and AOT compile it.
14 | COPY . .
15 | # Ensure packages are still up-to-date if anything has changed
16 | RUN dart pub get --offline
17 | RUN dart compile exe ./${HANDLER} -o bin/main
18 |
19 | # Build a minimal serving image from AOT-compiled `/server` and required system
20 | # libraries and configuration files stored in `/runtime/` from the build stage.
21 | FROM alpine
22 |
23 | COPY --from=build /runtime/ /
24 | COPY --from=build /app/bin/main /app/bin/
25 |
26 | ENTRYPOINT ["/app/bin/main"]
27 |
--------------------------------------------------------------------------------
/pkg/project/runtime/generate_test.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package runtime
18 |
19 | import (
20 | "os"
21 | "testing"
22 |
23 | "github.com/google/go-cmp/cmp"
24 | "github.com/spf13/afero"
25 | )
26 |
27 | func TestGenerate(t *testing.T) {
28 | tsFile, _ := os.ReadFile("typescript.dockerfile")
29 | pythonFile, _ := os.ReadFile("python.dockerfile")
30 | jsFile, _ := os.ReadFile("javascript.dockerfile")
31 | jvmFile, _ := os.ReadFile("jvm.dockerfile")
32 |
33 | fs := afero.NewOsFs()
34 |
35 | tests := []struct {
36 | name string
37 | handler string
38 | wantFwriter string
39 | }{
40 | {
41 | name: "ts",
42 | handler: "functions/list.ts",
43 | wantFwriter: string(tsFile),
44 | },
45 | {
46 | name: "python",
47 | handler: "list.py",
48 | wantFwriter: string(pythonFile),
49 | },
50 | {
51 | name: "js",
52 | handler: "functions/list.js",
53 | wantFwriter: string(jsFile),
54 | },
55 | {
56 | name: "jar",
57 | handler: "outout/fat.jar",
58 | wantFwriter: string(jvmFile),
59 | },
60 | }
61 | for _, tt := range tests {
62 | t.Run(tt.name, func(t *testing.T) {
63 | rt, err := NewBuildContext(tt.handler, "", ".", map[string]string{}, []string{}, fs)
64 | if err != nil {
65 | t.Error(err)
66 | }
67 |
68 | if !cmp.Equal(rt.DockerfileContents, tt.wantFwriter) {
69 | t.Error(cmp.Diff(tt.wantFwriter, rt.DockerfileContents))
70 | }
71 | })
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/pkg/project/runtime/javascript.dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 | FROM node:22.4.1-alpine
3 |
4 | ARG HANDLER
5 | ENV HANDLER=${HANDLER}
6 |
7 | # Python and make are required by certain native package build processes in NPM packages.
8 | RUN --mount=type=cache,sharing=locked,target=/etc/apk/cache \
9 | apk --update-cache add git g++ make py3-pip
10 |
11 | RUN apk update && \
12 | apk add --no-cache ca-certificates && \
13 | update-ca-certificates
14 |
15 | COPY . .
16 |
17 | RUN yarn import || echo Lockfile already exists
18 |
19 | RUN \
20 | set -ex; \
21 | yarn install --production --frozen-lockfile --cache-folder /tmp/.cache; \
22 | rm -rf /tmp/.cache; \
23 | # prisma fix for docker installs: https://github.com/prisma/docs/issues/4365
24 | # TODO: remove when custom dockerfile support is available
25 | test -d ./prisma && npx prisma generate || echo "";
26 |
27 | ENTRYPOINT node --import ./$HANDLER
28 |
--------------------------------------------------------------------------------
/pkg/project/runtime/jvm.dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 | FROM openjdk:22-slim
3 |
4 | ARG HANDLER
5 |
6 | COPY $HANDLER /usr/app/app.jar
7 |
8 | CMD ["java", "-jar", "/usr/app/app.jar"]
9 |
10 |
--------------------------------------------------------------------------------
/pkg/project/runtime/python.dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-slim
2 |
3 | ARG HANDLER
4 |
5 | ENV HANDLER=${HANDLER}
6 | ENV PYTHONUNBUFFERED=TRUE
7 |
8 | RUN apt-get update -y && \
9 | apt-get install -y ca-certificates git && \
10 | update-ca-certificates
11 |
12 | RUN pip install --upgrade pip pipenv
13 |
14 | COPY . .
15 |
16 | # Guarantee lock file if we have a Pipfile and no Pipfile.lock
17 | RUN (stat Pipfile && pipenv lock) || echo "No Pipfile found"
18 |
19 | # Output a requirements.txt file for final module install if there is a Pipfile.lock found
20 | RUN (stat Pipfile.lock && pipenv requirements > requirements.txt) || echo "No Pipfile.lock found"
21 |
22 | RUN pip install --no-cache-dir -r requirements.txt
23 |
24 | ENTRYPOINT python -u $HANDLER
25 |
--------------------------------------------------------------------------------
/pkg/project/runtime/typescript.dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 | FROM node:22.4.1-alpine as build
3 |
4 | ARG HANDLER
5 |
6 | # Python and make are required by certain native package build processes in NPM packages.
7 | RUN --mount=type=cache,sharing=locked,target=/etc/apk/cache \
8 | apk --update-cache add git g++ make py3-pip
9 |
10 | RUN yarn global add typescript @vercel/ncc
11 |
12 | WORKDIR /usr/app
13 |
14 | COPY package.json *.lock *-lock.json ./
15 |
16 | RUN yarn import || echo ""
17 |
18 | RUN --mount=type=cache,sharing=locked,target=/tmp/.yarn_cache \
19 | set -ex && \
20 | yarn install --production --prefer-offline --frozen-lockfile --cache-folder /tmp/.yarn_cache
21 |
22 | RUN test -f tsconfig.json || echo "{\"compilerOptions\":{\"esModuleInterop\":true,\"target\":\"es2015\",\"moduleResolution\":\"node\"}}" > tsconfig.json
23 |
24 | COPY . .
25 |
26 | # make prisma external to bundle - https://github.com/prisma/prisma/issues/16901#issuecomment-1362940774 \
27 | # TODO: remove when custom dockerfile support is available
28 | RUN --mount=type=cache,sharing=private,target=/tmp/ncc-cache \
29 | ncc build ${HANDLER} -o lib/ -e .prisma/client -e @prisma/client -t
30 |
31 | FROM node:22.4.1-alpine as final
32 |
33 | RUN apk update && \
34 | apk add --no-cache ca-certificates && \
35 | update-ca-certificates
36 |
37 | WORKDIR /usr/app
38 |
39 | COPY . .
40 |
41 | COPY --from=build /usr/app/node_modules/ ./node_modules/
42 |
43 | COPY --from=build /usr/app/lib/ ./lib/
44 |
45 | # prisma fix for docker installs: https://github.com/prisma/docs/issues/4365
46 | # TODO: remove when custom dockerfile support is available
47 | RUN test -d ./prisma && npx prisma generate || echo "";
48 |
49 | ENTRYPOINT ["node", "lib/index.js"]
--------------------------------------------------------------------------------
/pkg/project/stack/azure.config.yaml:
--------------------------------------------------------------------------------
1 | # The provider to use and it's published version
2 | # See releases:
3 | # https://github.com/nitrictech/nitric/tags
4 | provider: nitric/azure@{{.Version}}
5 | # The target Azure region to deploy to
6 | # See available regions:
7 | # https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=container-apps
8 | region:
9 |
10 | # Org to associate deployed API Management services with
11 | org:
12 |
13 | # Admin email to associate deployed API Management services with, this can be any email address
14 | adminemail: test@example.com
15 | # Optional configuration below
16 |
17 | # # Configure your deployed functions/services
18 | # config:
19 | # # How functions without a type will be deployed
20 | # default:
21 | # # configure a sample rate for telemetry (between 0 and 1) e.g. 0.5 is 50%
22 | # telemetry: 0
23 | # # configure functions to deploy to Google Cloud Run
24 | # # see: https://learn.microsoft.com/en-us/azure/container-apps/containers#configuration
25 | # containerapps: # Available since v0.26.0
26 | # # set 1/4 vCPU
27 | # cpu: 0.25
28 | # # set 0.5GB of RAM
29 | # memory: 0.5
30 | # # The minimum number of instances to scale down to
31 | # min-replicas: 0
32 | # # The maximum number of instances to scale up to
33 | # max-replicas: 10
34 | # # Additional deployment types
35 | # # You can target these types by setting a `type` in your project configuration
36 | # big-service:
37 | # telemetry: 0
38 | # containerapps:
39 | # memory: 1
40 | # min-replicas: 2
41 | # max-replicas: 100
42 |
--------------------------------------------------------------------------------
/pkg/project/stack/azuretf.config.yaml:
--------------------------------------------------------------------------------
1 | # The provider to use and it's published version
2 | # See releases:
3 | # https://github.com/nitrictech/nitric/tags
4 | provider: nitric/azuretf@{{.Version}}
5 | # The target Azure region to deploy to
6 | # See available regions:
7 | # https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=container-apps
8 | region:
9 |
10 | # Org to associate deployed API Management services with
11 | org:
12 |
13 | # Subscription ID to associate deployed services with
14 | subscription-id:
15 |
16 | # Admin email to associate deployed API Management services with, this can be any email address
17 | adminemail: test@example.com
18 | # Optional configuration below
19 |
20 | # # Configure your deployed functions/services
21 | # config:
22 | # # How functions without a type will be deployed
23 | # default:
24 | # # configure a sample rate for telemetry (between 0 and 1) e.g. 0.5 is 50%
25 | # telemetry: 0
26 | # # configure functions to deploy to Google Cloud Run
27 | # # see: https://learn.microsoft.com/en-us/azure/container-apps/containers#configuration
28 | # containerapps: # Available since v0.26.0
29 | # # set 1/4 vCPU
30 | # cpu: 0.25
31 | # # set 0.5GB of RAM
32 | # memory: 0.5
33 | # # The minimum number of instances to scale down to
34 | # min-replicas: 0
35 | # # The maximum number of instances to scale up to
36 | # max-replicas: 10
37 | # # Additional deployment types
38 | # # You can target these types by setting a `type` in your project configuration
39 | # big-service:
40 | # telemetry: 0
41 | # containerapps:
42 | # memory: 1
43 | # min-replicas: 2
44 | # max-replicas: 100
45 |
--------------------------------------------------------------------------------
/pkg/project/stack/gcp.config.yaml:
--------------------------------------------------------------------------------
1 | # The provider to use and it's published version
2 | # See releases:
3 | # https://github.com/nitrictech/nitric/tags
4 | provider: nitric/gcp@{{.Version}}
5 |
6 | # The target GCP region to deploy to
7 | # See available regions:
8 | # https://cloud.google.com/run/docs/locations
9 | region:
10 |
11 | # # ID of the google cloud project to deploy into
12 | gcp-project-id:
13 | # Optional configuration below
14 |
15 | # # The timezone that deployed schedules will run with
16 | # # Format is in tz identifiers:
17 | # # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
18 | # schedule-timezone: Australia/Sydney # Available since v0.27.0
19 |
20 | # # Configure your deployed functions/services
21 | # config:
22 | # # How functions without a type will be deployed
23 | # default:
24 | # # configure a sample rate for telemetry (between 0 and 1) e.g. 0.5 is 50%
25 | # telemetry: 0
26 | # # configure functions to deploy to Google Cloud Run
27 | # cloudrun: # Available since v0.26.0
28 | # # set 512MB of RAM
29 | # # See cloudrun configuration docs here:
30 | # # https://cloud.google.com/run/docs/configuring/memory-limits
31 | # memory: 512
32 | # # set a timeout of 15 seconds
33 | # # https://cloud.google.com/run/docs/configuring/request-timeout
34 | # timeout: 15
35 | # # The maximum number of instances to scale down to
36 | # # https://cloud.google.com/run/docs/configuring/min-instances
37 | # min-instances: 0
38 | # # The maximum number of instances to scale up to
39 | # # https://cloud.google.com/run/docs/configuring/max-instances
40 | # max-instances: 10
41 | # # Number of concurrent requests that each instance can handle
42 | # # https://cloud.google.com/run/docs/configuring/concurrency
43 | # concurrency: 80
44 | # # Additional deployment types
45 | # # You can target these types by setting a `type` in your project configuration
46 | # big-service:
47 | # telemetry: 0
48 | # cloudrun:
49 | # memory: 1024
50 | # timeout: 60
51 | # min-instances: 2
52 | # max-instances: 100
53 | # concurrency: 1000
54 |
--------------------------------------------------------------------------------
/pkg/project/stack/gcptf.config.yaml:
--------------------------------------------------------------------------------
1 | # The provider to use and it's published version
2 | # See releases:
3 | # https://github.com/nitrictech/nitric/tags
4 | provider: nitric/gcptf@{{.Version}}
5 |
6 | # The target GCP region to deploy to
7 | # See available regions:
8 | # https://cloud.google.com/run/docs/locations
9 | region:
10 |
11 | # # ID of the google cloud project to deploy into
12 | gcp-project-id:
13 | # Optional configuration below
14 |
15 | # # The timezone that deployed schedules will run with
16 | # # Format is in tz identifiers:
17 | # # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
18 | # schedule-timezone: Australia/Sydney # Available since v0.27.0
19 |
20 | # # Configure your deployed functions/services
21 | # config:
22 | # # How functions without a type will be deployed
23 | # default:
24 | # # configure a sample rate for telemetry (between 0 and 1) e.g. 0.5 is 50%
25 | # telemetry: 0
26 | # # configure functions to deploy to Google Cloud Run
27 | # cloudrun: # Available since v0.26.0
28 | # # set 512MB of RAM
29 | # # See cloudrun configuration docs here:
30 | # # https://cloud.google.com/run/docs/configuring/memory-limits
31 | # memory: 512
32 | # # set a timeout of 15 seconds
33 | # # https://cloud.google.com/run/docs/configuring/request-timeout
34 | # timeout: 15
35 | # # The maximum number of instances to scale down to
36 | # # https://cloud.google.com/run/docs/configuring/min-instances
37 | # min-instances: 0
38 | # # The maximum number of instances to scale up to
39 | # # https://cloud.google.com/run/docs/configuring/max-instances
40 | # max-instances: 10
41 | # # Number of concurrent requests that each instance can handle
42 | # # https://cloud.google.com/run/docs/configuring/concurrency
43 | # concurrency: 80
44 | # # Additional deployment types
45 | # # You can target these types by setting a `type` in your project configuration
46 | # big-service:
47 | # telemetry: 0
48 | # cloudrun:
49 | # memory: 1024
50 | # timeout: 60
51 | # min-instances: 2
52 | # max-instances: 100
53 | # concurrency: 1000
54 |
--------------------------------------------------------------------------------
/pkg/project/templates/getter.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package templates
18 |
19 | import "github.com/hashicorp/go-getter"
20 |
21 | // GetterClient exists because go-getter does not have an interface to mock.
22 | type GetterClient interface {
23 | Get() error
24 | }
25 |
26 | type getterConfig struct {
27 | *getter.Client
28 | }
29 |
30 | func NewGetter(c *getter.Client) GetterClient {
31 | return &getterConfig{Client: c}
32 | }
33 |
34 | func (c *getterConfig) Get() error {
35 | return c.Client.Get()
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/system/logs.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package system
18 |
19 | import (
20 | "fmt"
21 | "sync"
22 |
23 | "github.com/asaskevich/EventBus"
24 | )
25 |
26 | // SystemLogsService - An EventBus service for handling logging of events and exceptions
27 | // So they can be subscribed to and displayed in the CLI
28 | type SystemLogsService struct {
29 | bus EventBus.Bus
30 | }
31 |
32 | const logTopic = "system_logs"
33 |
34 | var (
35 | instance *SystemLogsService
36 | once sync.Once
37 | )
38 |
39 | func getInstance() *SystemLogsService {
40 | once.Do(func() {
41 | instance = &SystemLogsService{
42 | bus: EventBus.New(),
43 | }
44 | })
45 |
46 | return instance
47 | }
48 |
49 | func Log(msg string) {
50 | s := getInstance()
51 | s.bus.Publish(logTopic, msg)
52 | }
53 |
54 | func Logf(format string, args ...interface{}) {
55 | Log(fmt.Sprintf(format, args...))
56 | }
57 |
58 | func SubscribeToLogs(subscription func(string)) {
59 | s := getInstance()
60 | _ = s.bus.Subscribe(logTopic, subscription)
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/validation/name.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package validation
18 |
19 | import (
20 | "fmt"
21 | "regexp"
22 |
23 | "github.com/ettle/strcase"
24 | )
25 |
26 | var ResourceName_Rule = &Rule{
27 | name: "Invalid Name",
28 | // TODO: Add docs link for rule when available
29 | docsUrl: "",
30 | }
31 |
32 | var lowerKebabCaseRe, _ = regexp.Compile("^[a-z0-9]+(-[a-z0-9]+)*$")
33 |
34 | func IsValidResourceName(name string) bool {
35 | return lowerKebabCaseRe.Match([]byte(name))
36 | }
37 |
38 | func NewResourceNameViolationError(resourceName string, resourceType string) *RuleViolationError {
39 | return ResourceName_Rule.newError(fmt.Sprintf("'%s' for %s try '%s'", resourceName, resourceType, strcase.ToKebab(resourceName)))
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/validation/rule.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package validation
18 |
19 | import (
20 | "errors"
21 | "fmt"
22 | )
23 |
24 | type Rule struct {
25 | name string
26 | docsUrl string
27 | }
28 |
29 | func (r *Rule) newError(message string) *RuleViolationError {
30 | return &RuleViolationError{
31 | rule: r,
32 | message: message,
33 | }
34 | }
35 |
36 | func (r *Rule) String() string {
37 | return fmt.Sprintf("%s: %s", r.name, r.docsUrl)
38 | }
39 |
40 | type RuleViolationError struct {
41 | rule *Rule
42 | message string
43 | }
44 |
45 | func (r *RuleViolationError) Error() string {
46 | return fmt.Sprintf("%s: %s", r.rule.name, r.message)
47 | }
48 |
49 | func GetRuleViolation(err error) *Rule {
50 | ruleViolation := &RuleViolationError{}
51 |
52 | if errors.As(err, &ruleViolation) {
53 | return ruleViolation.rule
54 | }
55 |
56 | return nil
57 | }
58 |
--------------------------------------------------------------------------------
/pkg/version/version.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package version
18 |
19 | var (
20 | // Raw is the string representation of the version. This will be replaced
21 | // with the calculated version at build time.
22 | // set in the Makefile.
23 | Version = "was not built with version info"
24 |
25 | // Commit is the commit hash from which the software was built.
26 | // Set via LDFLAGS in Makefile.
27 | Commit = "unknown"
28 |
29 | // BuildTime is the string representation of build time.
30 | // Set via LDFLAGS in Makefile.
31 | BuildTime = "unknown"
32 | )
33 |
--------------------------------------------------------------------------------
/pkg/view/tui/commands/project/validators.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package project
18 |
19 | import (
20 | "regexp"
21 |
22 | "github.com/nitrictech/cli/pkg/view/tui/components/validation"
23 | )
24 |
25 | var (
26 | nameRegex = regexp.MustCompile(`^([a-zA-Z0-9-])*$`)
27 | suffixRegex = regexp.MustCompile(`[^-]$`)
28 | prefixRegex = regexp.MustCompile(`^[^-]`)
29 | )
30 |
31 | var projectNameInFlightValidators = []validation.StringValidator{
32 | validation.RegexValidator(prefixRegex, "name can't start with a dash"),
33 | validation.RegexValidator(nameRegex, "name must only contain letters, numbers and dashes"),
34 | }
35 |
36 | var projectNameValidators = append([]validation.StringValidator{
37 | validation.RegexValidator(suffixRegex, "name can't end with a dash"),
38 | validation.NotBlankValidator("name can't be blank"),
39 | }, projectNameInFlightValidators...)
40 |
--------------------------------------------------------------------------------
/pkg/view/tui/commands/stack/validators.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package stack
18 |
19 | import (
20 | "regexp"
21 |
22 | "github.com/nitrictech/cli/pkg/view/tui/components/validation"
23 | )
24 |
25 | var (
26 | nameRegex = regexp.MustCompile(`^([a-zA-Z0-9-])*$`)
27 | suffixRegex = regexp.MustCompile(`[^-]$`)
28 | prefixRegex = regexp.MustCompile(`^[^-]`)
29 | )
30 |
31 | var ProjectNameInFlightValidators = []validation.StringValidator{
32 | validation.RegexValidator(prefixRegex, "name can't start with a dash"),
33 | validation.RegexValidator(nameRegex, "name must only contain letters, numbers and dashes"),
34 | }
35 |
36 | var ProjectNameValidators = append([]validation.StringValidator{
37 | validation.RegexValidator(suffixRegex, "name can't end with a dash"),
38 | validation.NotBlankValidator("name can't be blank"),
39 | }, ProjectNameInFlightValidators...)
40 |
--------------------------------------------------------------------------------
/pkg/view/tui/components/list/list.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package list
18 |
19 | type ListItem interface {
20 | GetItemValue() string
21 | GetItemDescription() string
22 | }
23 |
24 | type StringItem struct {
25 | Value string
26 | }
27 |
28 | func (s StringItem) GetItemValue() string {
29 | return s.Value
30 | }
31 |
32 | func (s StringItem) GetItemDescription() string {
33 | return ""
34 | }
35 |
36 | func StringsToListItems(strings []string) []ListItem {
37 | items := make([]ListItem, len(strings))
38 | for i, str := range strings {
39 | items[i] = StringItem{Value: str}
40 | }
41 |
42 | return items
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/view/tui/components/validation/validation.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package validation
18 |
19 | import (
20 | "errors"
21 | "regexp"
22 | )
23 |
24 | // StringValidator is a function that returns an error if the input is invalid.
25 | type StringValidator func(string) error
26 |
27 | func NotBlankValidator(message string) StringValidator {
28 | return func(value string) error {
29 | if value == "" {
30 | return errors.New(message)
31 | }
32 |
33 | return nil
34 | }
35 | }
36 |
37 | func RegexValidator(regex *regexp.Regexp, message string) StringValidator {
38 | return func(value string) error {
39 | if !regex.MatchString(value) {
40 | return errors.New(message)
41 | }
42 |
43 | return nil
44 | }
45 | }
46 |
47 | func ComposeValidators(validators ...StringValidator) StringValidator {
48 | return func(value string) error {
49 | for _, v := range validators {
50 | if err := v(value); err != nil {
51 | return err
52 | }
53 | }
54 |
55 | return nil
56 | }
57 | }
58 |
59 | // var alphanumOnly = regexValidator(nameRegex, "name must only contain letters, numbers and dashes")
60 |
--------------------------------------------------------------------------------
/pkg/view/tui/components/view/fragment.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package view
18 |
19 | import (
20 | "fmt"
21 |
22 | "github.com/charmbracelet/lipgloss"
23 | )
24 |
25 | // Fragment represents a UI element
26 | type Fragment struct {
27 | content any
28 | styles []lipgloss.Style
29 | }
30 |
31 | // Render this fragment as a string, applying its style
32 | func (f Fragment) Render() string {
33 | rendered := fmt.Sprint(f.content)
34 |
35 | for _, style := range f.styles {
36 | rendered = style.Render(rendered)
37 | }
38 |
39 | return rendered
40 | }
41 |
42 | // String returns the rendered fragment as a string
43 | func (f Fragment) String() string {
44 | return f.Render()
45 | }
46 |
47 | // WithStyle adds a style to this fragment, which will be used when rendering
48 | func (f *Fragment) WithStyle(style lipgloss.Style, styles ...lipgloss.Style) *Fragment {
49 | s := make([]lipgloss.Style, 0, len(styles)+1)
50 | s = append(s, style)
51 | s = append(s, styles...)
52 | f.styles = s
53 |
54 | return f
55 | }
56 |
57 | // NewFragment constructs a new fragment from its un-styled content
58 | func NewFragment(content any) *Fragment {
59 | return &Fragment{
60 | content: content,
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/pkg/view/tui/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package tui
18 |
19 | import (
20 | "os"
21 | )
22 |
23 | func CheckErr(err error) {
24 | if err != nil {
25 | Error.Println(err.Error())
26 | os.Exit(1)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/view/tui/fragments/errorlist.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package fragments
18 |
19 | import (
20 | "github.com/charmbracelet/lipgloss"
21 |
22 | "github.com/nitrictech/cli/pkg/view/tui"
23 | "github.com/nitrictech/cli/pkg/view/tui/components/view"
24 | )
25 |
26 | type ErrorListOptions struct {
27 | heading string
28 | }
29 |
30 | type ErrorListOption = func(*ErrorListOptions) *ErrorListOptions
31 |
32 | // WithCustomHeading sets a custom heading for the error list
33 | func WithCustomHeading(heading string) ErrorListOption {
34 | return func(ol *ErrorListOptions) *ErrorListOptions {
35 | ol.heading = heading
36 | return ol
37 | }
38 | }
39 |
40 | func WithoutHeading(ol *ErrorListOptions) *ErrorListOptions {
41 | ol.heading = ""
42 | return ol
43 | }
44 |
45 | // ErrorList renders a list of errors as a dot point list
46 | func ErrorList(errs []error, opts ...ErrorListOption) string {
47 | v := view.New()
48 |
49 | ol := &ErrorListOptions{
50 | heading: lipgloss.NewStyle().Width(10).Align(lipgloss.Center).Bold(true).Foreground(tui.Colors.White).Background(tui.Colors.Red).Render("Errors"),
51 | }
52 |
53 | for _, opt := range opts {
54 | ol = opt(ol)
55 | }
56 |
57 | for _, err := range errs {
58 | v.Addln(" - %s", err.Error()).WithStyle(lipgloss.NewStyle().Foreground(tui.Colors.Red))
59 | }
60 |
61 | return v.Render()
62 | }
63 |
--------------------------------------------------------------------------------
/pkg/view/tui/fragments/hotkey.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package fragments
18 |
19 | import (
20 | "fmt"
21 |
22 | "github.com/charmbracelet/lipgloss"
23 |
24 | "github.com/nitrictech/cli/pkg/view/tui"
25 | "github.com/nitrictech/cli/pkg/view/tui/components/view"
26 | )
27 |
28 | // Hotkey renders a hotkey fragment e.g. q: quit
29 | func Hotkey(key string, description string) string {
30 | keyView := view.NewFragment(fmt.Sprintf("%s:", key)).WithStyle(lipgloss.NewStyle().Foreground(tui.Colors.Text)).Render()
31 | descriptionView := view.NewFragment(description).WithStyle(lipgloss.NewStyle().Foreground(tui.Colors.TextMuted)).Render()
32 |
33 | return fmt.Sprintf("%s %s", keyView, descriptionView)
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/view/tui/fragments/tag.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package fragments
18 |
19 | import (
20 | "unicode/utf8"
21 |
22 | "github.com/charmbracelet/lipgloss"
23 |
24 | "github.com/nitrictech/cli/pkg/view/tui"
25 | "github.com/nitrictech/cli/pkg/view/tui/components/view"
26 | )
27 |
28 | var width = 0
29 |
30 | // CustomTag renders a tag with the given text, foreground, background and width
31 | // e.g. CustomTag("hello", tui.Colors.White, tui.Colors.Purple, 8)
32 | // Use Tag() for a standard tag.
33 | func CustomTag(text string, foreground lipgloss.CompleteAdaptiveColor, background lipgloss.CompleteAdaptiveColor) string {
34 | if utf8.RuneCountInString(text)+2 > width {
35 | width = utf8.RuneCountInString(text) + 2
36 | }
37 |
38 | tagStyle := lipgloss.NewStyle().Width(width).Align(lipgloss.Center).Foreground(foreground).Background(background)
39 |
40 | f := view.NewFragment(text).WithStyle(tagStyle)
41 |
42 | return f.Render()
43 | }
44 |
45 | // Tag renders a standard tag with the given title
46 | func Tag(text string) string {
47 | return CustomTag(text, tui.Colors.White, tui.Colors.Purple)
48 | }
49 |
50 | // NitricTag renders a standard tag with the title "nitric"
51 | func NitricTag() string {
52 | return CustomTag("nitric", tui.Colors.White, tui.Colors.Blue)
53 | }
54 |
55 | func ErrorTag() string {
56 | return CustomTag("error", tui.Colors.White, tui.Colors.Red)
57 | }
58 |
59 | // TagWidth returns the width of tags, which auto adjusts based on the longest tag rendered
60 | func TagWidth() int {
61 | return width
62 | }
63 |
--------------------------------------------------------------------------------
/pkg/view/tui/keys.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package tui
18 |
19 | import "github.com/charmbracelet/bubbles/key"
20 |
21 | type DefaultKeyMap struct {
22 | Enter key.Binding
23 | Quit key.Binding
24 | Up key.Binding
25 | Down key.Binding
26 | }
27 |
28 | // KeyMap contains our standard keybinding for CLI input
29 | var KeyMap = DefaultKeyMap{
30 | Enter: key.NewBinding(
31 | key.WithKeys("enter"),
32 | key.WithHelp("enter", "submit input"),
33 | ),
34 | Quit: key.NewBinding(
35 | key.WithKeys("esc", "ctrl+c"),
36 | key.WithHelp("esc", "exit"),
37 | ),
38 | Up: key.NewBinding(
39 | key.WithKeys("up"),
40 | key.WithHelp("↑", "up"),
41 | ),
42 | Down: key.NewBinding(
43 | key.WithKeys("down"),
44 | key.WithHelp("↓", "down"),
45 | ),
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/view/tui/printer.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package tui
18 |
19 | import (
20 | "fmt"
21 | "unicode/utf8"
22 |
23 | "github.com/charmbracelet/lipgloss"
24 |
25 | "github.com/nitrictech/cli/pkg/view/tui/components/view"
26 | )
27 |
28 | var (
29 | Debug = TagPrinter{
30 | Prefix: addPrefix("debug", Colors.White, Colors.TextMuted),
31 | }
32 | Error = TagPrinter{
33 | Prefix: addPrefix("error", Colors.White, Colors.Red),
34 | }
35 | Info = TagPrinter{
36 | Prefix: addPrefix("info", Colors.White, Colors.TextMuted),
37 | }
38 | Warning = TagPrinter{
39 | Prefix: addPrefix("warning", Colors.Black, Colors.Yellow),
40 | }
41 |
42 | width = 0
43 | )
44 |
45 | type TagPrinter struct {
46 | Prefix string
47 | }
48 |
49 | func (t *TagPrinter) Println(message string) {
50 | fmt.Println(t.Prefix, message)
51 | }
52 |
53 | func (t *TagPrinter) Printfln(message string, a ...interface{}) {
54 | fmt.Println(t.Prefix, fmt.Sprintf(message, a...))
55 | }
56 |
57 | func addPrefix(text string, foreground lipgloss.CompleteAdaptiveColor, background lipgloss.CompleteAdaptiveColor) string {
58 | if utf8.RuneCountInString(text)+2 > width {
59 | width = utf8.RuneCountInString(text) + 2
60 | }
61 |
62 | tagStyle := lipgloss.NewStyle().Width(width).Align(lipgloss.Center).Foreground(foreground).Background(background)
63 |
64 | f := view.NewFragment(text).WithStyle(tagStyle)
65 |
66 | return f.Render()
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/view/tui/teax/program.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package teax
18 |
19 | import (
20 | "fmt"
21 |
22 | tea "github.com/charmbracelet/bubbletea"
23 | )
24 |
25 | // FullViewProgram is a program that will print the full view for the model as the program terminates.
26 | //
27 | // Bubbletea programs limit the output of the view to the terminal size, which fixes issues with rerendering,
28 | // but results in any off-screen output being lost when the program exits.
29 | type FullViewProgram struct {
30 | *tea.Program
31 | }
32 |
33 | func (p *FullViewProgram) Run() (tea.Model, error) {
34 | model, err := p.Program.Run()
35 |
36 | tea.Batch()
37 |
38 | quittingModel := model.(fullHeightModel)
39 |
40 | quittingModel.quitting = false
41 | fmt.Println(quittingModel.View())
42 |
43 | return quittingModel.Model, err
44 | }
45 |
46 | func NewProgram(model tea.Model, opts ...tea.ProgramOption) *FullViewProgram {
47 | return &FullViewProgram{tea.NewProgram(fullHeightModel{model, false}, opts...)}
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/view/tui/teax/quit.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package teax
18 |
19 | import tea "github.com/charmbracelet/bubbletea"
20 |
21 | type QuitMsg struct{}
22 |
23 | func Quit() tea.Msg {
24 | return QuitMsg{}
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/view/tui/teax/teax.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package teax
18 |
19 | import tea "github.com/charmbracelet/bubbletea"
20 |
21 | type fullHeightModel struct {
22 | tea.Model
23 | quitting bool
24 | }
25 |
26 | var _ tea.Model = fullHeightModel{}
27 |
28 | func (q fullHeightModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
29 | switch msg.(type) {
30 | case QuitMsg:
31 | q.quitting = true
32 |
33 | return q, tea.Quit
34 | }
35 |
36 | var cmd tea.Cmd
37 | q.Model, cmd = q.Model.Update(msg)
38 |
39 | return q, cmd
40 | }
41 |
42 | func (q fullHeightModel) FullView() string {
43 | return q.Model.View()
44 | }
45 |
46 | func (q fullHeightModel) View() string {
47 | if q.quitting {
48 | return ""
49 | }
50 |
51 | return q.Model.View()
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/view/tui/tui.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | package tui
18 |
19 | import (
20 | "os"
21 |
22 | "github.com/mattn/go-isatty"
23 | )
24 |
25 | // IsTerminal returns true if the current process is running in an interactive terminal
26 | func IsTerminal() bool {
27 | return isatty.IsTerminal(os.Stdin.Fd()) && isatty.IsTerminal(os.Stdout.Fd())
28 | }
29 |
--------------------------------------------------------------------------------
/tools/tools.go:
--------------------------------------------------------------------------------
1 | // Copyright Nitric Pty Ltd.
2 | //
3 | // SPDX-License-Identifier: Apache-2.0
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at:
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | //go:build tools
18 | // +build tools
19 |
20 | package tools
21 |
22 | import (
23 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint"
24 | )
25 |
--------------------------------------------------------------------------------
/vhs/.gitignore:
--------------------------------------------------------------------------------
1 | # ignore generated demo images
2 | *.gif
--------------------------------------------------------------------------------
/vhs/new_project.tape:
--------------------------------------------------------------------------------
1 | # Where should we write the GIF?
2 | Output new_project.gif
3 |
4 | Set Framerate 16
5 |
6 | # Set up a 1200x600 terminal with 46px font.
7 | Set FontSize 46
8 | Set Width 1900
9 | Set Height 1400
10 |
11 | # Type a command in the terminal.
12 | Type "../bin/nitric new"
13 |
14 | # Run the command by pressing enter.
15 | Enter
16 |
17 | # Admire the output for a bit.
18 | Sleep 5s
19 |
20 | Type "example-project&"
21 |
22 | Sleep 4s
23 |
24 | Backspace
25 |
26 | Sleep 2s
27 |
28 | Enter
29 |
30 | Sleep 4s
31 |
32 | Down
33 |
34 | Sleep 1s
35 |
36 | Down
37 |
38 | Sleep 1s
39 |
40 | Up
41 |
42 | Sleep 1s
43 |
44 | Enter
45 |
46 | Sleep 15s
--------------------------------------------------------------------------------
/vhs/new_project_non-interactive.tape:
--------------------------------------------------------------------------------
1 | # Where should we write the GIF?
2 | Output new_project_non-interactive.gif
3 |
4 | Set Framerate 16
5 |
6 | # Set up a 1200x600 terminal with 46px font.
7 | Set FontSize 46
8 | Set Width 2000
9 | Set Height 800
10 |
11 | # Type a command in the terminal.
12 | Type `../bin/nitric new my-project "official/TypeScript - Starter"`
13 |
14 | # Run the command by pressing enter.
15 | Enter
16 |
17 | Sleep 15s
--------------------------------------------------------------------------------