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/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/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/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/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/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/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/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/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/README.md:
--------------------------------------------------------------------------------
1 | # Nitric Local Dashboard
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | CLI for building and deploying nitric apps
11 |
12 |
13 | ## 🚀 Project Structure
14 |
15 | Inside the dashboard project, you'll see the following folders and files:
16 |
17 | ```
18 | /
19 | ├── public/
20 | │ └── favicon.ico
21 | ├── src/
22 | │ ├── components/
23 | │ ├── layouts/
24 | | ├── lib/
25 | │ └── pages/
26 | │ └── index.astro
27 | └── package.json
28 | ```
29 |
30 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
31 |
32 | There's nothing special about `src/components/`, but that's where we like to put any Astro or React components.
33 |
34 | Any static assets, like images, can be placed in the `public/` directory.
35 |
36 | ## 🧞 Commands
37 |
38 | All commands are run from the root of the project, from a terminal:
39 |
40 | | Command | Action |
41 | | :------------------ | :-------------------------------------------------------- |
42 | | `yarn install` | Installs dependencies |
43 | | `yarn dev` | Starts local dev server at `localhost:3000` |
44 | | `yarn build` | Build the production dashboard to `../pkg/dashboard/dist` |
45 | | `yarn preview` | Preview your build locally, before deploying |
46 | | `yarn cypress:open` | Open Cypress for e2e testing |
47 | | `yarn astro ...` | Run CLI commands like `astro add`, `astro check` |
48 | | `yarn astro --help` | Get help using the Astro CLI |
49 |
50 | ## Need help with Nitric?
51 |
52 | Feel free to check out the [Nitric documentation](https://nitric.io/docs) or jump on our [Discord Server](https://nitric.io/chat).
53 |
--------------------------------------------------------------------------------
/.github/workflows/dashboard-run-test.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Dashboard Tests with nitric run
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 | - develop
9 | pull_request:
10 |
11 | concurrency:
12 | group: ci-dash-run-tests-${{ github.ref_name }}
13 | cancel-in-progress: true
14 |
15 | env:
16 | GOPROXY: https://proxy.golang.org
17 | FATHOM_SITE: FAKE1234
18 |
19 | jobs:
20 | nitric-dashboard:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v3
25 | with:
26 | path: cli
27 |
28 | - uses: actions/setup-node@v4
29 | with:
30 | node-version: 22
31 |
32 | - name: Setup Go
33 | uses: actions/setup-go@v3
34 | with:
35 | go-version: 1.22
36 |
37 | - name: Build Nitric
38 | run: |
39 | cd ${{ github.workspace }}/cli
40 | make build
41 | mv bin/nitric $(go env GOPATH)/bin/nitric
42 |
43 | - name: Run nitric run with test-app in the background
44 | run: |
45 | cd ${{ github.workspace }}/cli/pkg/dashboard/frontend/test-app
46 | yarn install
47 | yarn install:websites
48 | nitric run --ci &
49 | sleep 25
50 |
51 | - name: Run Tests
52 | uses: cypress-io/github-action@v5
53 | env:
54 | CYPRESS_NITRIC_TEST_TYPE: "run"
55 | with:
56 | install: false
57 | wait-on: "http://localhost:49152"
58 | # wait for 3 minutes for the server to respond
59 | wait-on-timeout: 180
60 | working-directory: cli/pkg/dashboard/frontend
61 | browser: chrome
62 |
63 | - uses: actions/upload-artifact@v4
64 | if: failure()
65 | with:
66 | name: cypress-screenshots
67 | path: cli/pkg/dashboard/frontend/cypress/screenshots
68 |
69 | - uses: actions/upload-artifact@v4
70 | if: failure()
71 | with:
72 | name: cypress-videos
73 | path: cli/pkg/dashboard/frontend/cypress/videos
74 |
--------------------------------------------------------------------------------
/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/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/cypress/e2e/architecture.cy.ts:
--------------------------------------------------------------------------------
1 | const expectedNodes = [
2 | 'first-api',
3 | 'second-api',
4 | 'socket',
5 | 'socket-2',
6 | 'socket-3',
7 | 'process-tests',
8 | 'process-tests-2',
9 | 'test-collection',
10 | 'connections',
11 | 'test-bucket',
12 | 'subscribe-tests',
13 | 'subscribe-tests-2',
14 | ':',
15 | 'my-db',
16 | 'my-second-db',
17 | 'services/my-test-service.ts',
18 | 'services/my-test-db.ts',
19 | 'services/my-test-secret.ts',
20 | 'my-first-secret',
21 | 'my-second-secret',
22 | 'CDN',
23 | ]
24 |
25 | describe('Architecture Spec', () => {
26 | beforeEach(() => {
27 | cy.viewport('macbook-16')
28 | cy.visit('/architecture')
29 | })
30 |
31 | it('should retrieve correct arch nodes', () => {
32 | cy.wait(500)
33 |
34 | expectedNodes.forEach((content) => {
35 | cy.log(`Checking that node: ${content} exists`)
36 | expect(cy.contains('.react-flow__node', content)).to.exist
37 | })
38 | })
39 |
40 | it('should have correct routes drawer content', () => {
41 | const expected = [
42 | [
43 | 'edge-label-e-first-api-services/my-test-service.ts',
44 | 'DELETE/all-methodsGET/all-methodsOPTIONS/all-methodsPATCH/all-methodsPOST/all-methodsPUT/all-methodsGET/header-testPOST/json-testGET/path-test/{name}GET/query-testGET/schedule-countGET/topic-count',
45 | ],
46 | [
47 | 'edge-label-e-second-api-services/my-test-service.ts',
48 | 'GET/content-type-binaryGET/content-type-cssGET/content-type-htmlGET/content-type-imageGET/content-type-xmlDELETE/image-from-bucketGET/image-from-bucketPUT/image-from-bucketPUT/very-nested-files',
49 | ],
50 | [
51 | 'edge-label-e-my-secret-api-services/my-test-secret.ts',
52 | 'GET/getPOST/setPOST/set-binary',
53 | ],
54 | ['edge-label-e-my-db-api-services/my-test-db.ts', 'GET/get'],
55 | ]
56 |
57 | expected.forEach(([edge, routes]) => {
58 | cy.getTestEl(edge).click({
59 | force: true,
60 | })
61 |
62 | cy.getTestEl('api-routes-list').should('have.text', routes)
63 | })
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------