Skip to content

Commit

Permalink
Merge pull request #72 from Brandawg93/feature-influx
Browse files Browse the repository at this point in the history
InfluxDB v2 Support
  • Loading branch information
Brandawg93 authored Oct 21, 2024
2 parents a37df51 + 059975a commit 3d0d65b
Show file tree
Hide file tree
Showing 50 changed files with 1,716 additions and 724 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/npmbuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@ jobs:
env:
USERNAME: admin
PASSWORD: nut_test
NUT_HOST: localhost
NUT_PORT: 3493
WEB_PORT: 8080
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ scripts

dev

settings.yml

.next
.swc
test-results
test-results
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ WORKDIR /app
COPY --link package.json pnpm-lock.yaml* ./

SHELL ["/bin/ash", "-xeo", "pipefail", "-c"]
ENV CI=true
RUN npm install -g pnpm

RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm fetch | grep -v "cross-device link not permitted\|Falling back to copying packages from store"

RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm install -r --offline
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm install

FROM node:20-alpine AS build

Expand Down
27 changes: 18 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ services:
image: brandawg93/peanut:latest
container_name: PeaNUT
restart: unless-stopped
volumes:
- /path/to/config:/config
ports:
- 8080:8080
environment:
Expand All @@ -55,15 +57,22 @@ More examples can be found in the [examples](https://github.com/Brandawg93/PeaNU

## Environment Variables

| Variable | Default | Description |
| --------- | --------- | ----------------------------- |
| NUT_HOST | localhost | Host of NUT server |
| NUT_PORT | 3493 | Port of NUT server |
| WEB_HOST | localhost | Hostname of web server |
| WEB_PORT | 8080 | Port of web server |
| USERNAME | undefined | Optional but required to edit |
| PASSWORD | undefined | Optional but required to edit |
| BASE_PATH | undefined | Base path for reverse proxy |
_Note:_ Environment variables are not required and used for first time setup only.

| Variable | Default | Description |
| --------------- | --------- | ----------------------------- |
| NUT_HOST | localhost | Host of NUT server |
| NUT_PORT | 3493 | Port of NUT server |
| WEB_HOST | localhost | Hostname of web server |
| WEB_PORT | 8080 | Port of web server |
| USERNAME | undefined | Optional but required to edit |
| PASSWORD | undefined | Optional but required to edit |
| BASE_PATH | undefined | Base path for reverse proxy |
| INFLUX_HOST | undefined | Host for Influx server |
| INFLUX_TOKEN | undefined | Token for Influx server |
| INFLUX_ORG | undefined | Org for influx server |
| INFLUX_BUCKET | undefined | Bucket for influx server |
| INFLUX_INTERVAL | 10 | Inverval for Influx ingestion |

## API

Expand Down
4 changes: 4 additions & 0 deletions __mocks__/ldrs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
helix: { register: jest.fn() },
dotPulse: { register: jest.fn() },
}
6 changes: 6 additions & 0 deletions __tests__/global.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { test as setup } from '@playwright/test'
import { YamlSettings } from '@/server/settings'

setup('create new database', () => {
new YamlSettings('./config/settings.yml')
})
67 changes: 66 additions & 1 deletion __tests__/unit/app/actions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { getAllVarDescriptions, getDevices } from '@/app/actions'
import { DEVICE, VARS } from '@/common/types'
import { Nut } from '@/server/nut'
import {
getAllVarDescriptions,
getDevices,
testConnection,
saveVar,
checkSettings,
getSettings,
setSettings,
deleteSettings,
disconnect,
} from '@/app/actions'
import { YamlSettings } from '@/server/settings'

const vars: VARS = {}

Expand All @@ -26,6 +37,18 @@ beforeAll(() => {
jest.spyOn(Nut.prototype, 'getData').mockResolvedValue(vars)
jest.spyOn(Nut.prototype, 'getRWVars').mockResolvedValue(['battery.charge'])
jest.spyOn(Nut.prototype, 'getVarDescription').mockResolvedValue('test')
jest.spyOn(Nut.prototype, 'setVar').mockResolvedValue()
jest.spyOn(YamlSettings.prototype, 'get').mockImplementation((key: string) => {
const settings = {
NUT_HOST: 'localhost',
NUT_PORT: '3493',
USERNAME: 'user',
PASSWORD: 'pass',
}
return settings[key as keyof typeof settings]
})
jest.spyOn(YamlSettings.prototype, 'set').mockImplementation(() => {})
jest.spyOn(YamlSettings.prototype, 'delete').mockImplementation(() => {})
})

describe('actions', () => {
Expand All @@ -45,4 +68,46 @@ describe('actions', () => {
const data = await getAllVarDescriptions('ups', ['battery.charge'])
expect(data?.data && data.data['battery.charge']).toEqual('test')
})

it('tests connection', async () => {
const data = await testConnection('localhost', 3493)
expect(data.error).toBeUndefined()
})

it('saves variable', async () => {
const data = await saveVar('ups', 'battery.charge', '100')
expect(data).toBeUndefined()
})

it('checks settings', async () => {
const data = await checkSettings()
expect(data).toBe(true)
})

it('gets settings', async () => {
const data = await getSettings('NUT_HOST')
expect(data).toBe('localhost')
})

it('sets settings', async () => {
await setSettings('NUT_HOST', '127.0.0.1')
expect(YamlSettings.prototype.set).toHaveBeenCalledWith('NUT_HOST', '127.0.0.1')
})

it('deletes settings', async () => {
await deleteSettings('NUT_HOST')
expect(YamlSettings.prototype.delete).toHaveBeenCalledWith('NUT_HOST')
})

it('disconnects', async () => {
await disconnect()
expect(YamlSettings.prototype.delete).toHaveBeenCalledWith('NUT_HOST')
expect(YamlSettings.prototype.delete).toHaveBeenCalledWith('NUT_PORT')
expect(YamlSettings.prototype.delete).toHaveBeenCalledWith('USERNAME')
expect(YamlSettings.prototype.delete).toHaveBeenCalledWith('PASSWORD')
expect(YamlSettings.prototype.delete).toHaveBeenCalledWith('INFLUX_HOST')
expect(YamlSettings.prototype.delete).toHaveBeenCalledWith('INFLUX_TOKEN')
expect(YamlSettings.prototype.delete).toHaveBeenCalledWith('INFLUX_ORG')
expect(YamlSettings.prototype.delete).toHaveBeenCalledWith('INFLUX_BUCKET')
})
})
72 changes: 61 additions & 11 deletions __tests__/unit/app/page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,68 @@
import React from 'react'
import { render } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { useQuery } from '@tanstack/react-query'
import Page from '@/app/page'
import { checkSettings } from '@/app/actions'

const queryClient = new QueryClient()
jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn(),
}))

jest.mock('react-chartjs-2', () => ({
Line: () => null,
Doughnut: () => null,
}))

jest.mock('../../../src/app/actions', () => ({
getDevices: jest.fn(),
checkSettings: jest.fn(),
disconnect: jest.fn(),
}))

global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{ name: '1.0.0' }]),
})
) as jest.Mock

describe('Page', () => {
it('renders a heading', () => {
const page = render(
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>
)

expect(page).toBeDefined()
const mockDevicesData = {
devices: [
{
name: 'Device1',
description: 'Test Device 1',
vars: {
'ups.status': { value: 'OL' },
'input.voltage': { value: '230' },
'input.voltage.nominal': { value: '230' },
'output.voltage': { value: '230' },
'ups.realpower': { value: '100' },
'ups.realpower.nominal': { value: '150' },
'ups.load': { value: '50' },
'battery.charge': { value: '80' },
'battery.runtime': { value: '1200' },
'ups.mfr': { value: 'Manufacturer' },
'ups.model': { value: 'Model' },
'device.serial': { value: '123456' },
},
},
],
updated: '2023-10-01T00:00:00Z',
}

beforeEach(() => {
;(useQuery as jest.Mock).mockReturnValue({
isLoading: false,
data: mockDevicesData,
refetch: jest.fn(),
})
;(checkSettings as jest.Mock).mockResolvedValue(true)
})

it('renders a heading', async () => {
render(<Page />)

const wrapper = await screen.findByTestId('wrapper')
expect(wrapper).toBeInTheDocument()
})
})
1 change: 1 addition & 0 deletions __tests__/unit/client/components/navbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('NavBar', () => {
onRefreshClick={() => {}}
onRefetch={() => {}}
onDeviceChange={() => {}}
onDisconnect={() => {}}
disableRefresh={false}
/>
)
Expand Down
83 changes: 73 additions & 10 deletions __tests__/unit/client/components/wrapper.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,82 @@
import React from 'react'
import { render } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

import { render, screen, waitFor } from '@testing-library/react'
import { useQuery } from '@tanstack/react-query'
import Wrapper from '@/client/components/wrapper'
import { LanguageContext } from '@/client/context/language'
import { checkSettings } from '@/app/actions'

jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn(),
}))

jest.mock('../../../../src/app/actions', () => ({
getDevices: jest.fn(),
checkSettings: jest.fn(),
disconnect: jest.fn(),
}))

describe('Wrapper Component', () => {
const mockDevicesData = {
devices: [
{
name: 'Device1',
description: 'Test Device 1',
vars: {
'ups.status': { value: 'OL' },
'input.voltage': { value: '230' },
'input.voltage.nominal': { value: '230' },
'output.voltage': { value: '230' },
'ups.realpower': { value: '100' },
'ups.realpower.nominal': { value: '150' },
'ups.load': { value: '50' },
'battery.charge': { value: '80' },
'battery.runtime': { value: '1200' },
'ups.mfr': { value: 'Manufacturer' },
'ups.model': { value: 'Model' },
'device.serial': { value: '123456' },
},
},
],
updated: '2023-10-01T00:00:00Z',
}

beforeEach(() => {
;(useQuery as jest.Mock).mockReturnValue({
isLoading: false,
data: mockDevicesData,
refetch: jest.fn(),
})
;(checkSettings as jest.Mock).mockResolvedValue(true)
})

it('renders loading state', async () => {
;(useQuery as jest.Mock).mockReturnValue({
isLoading: true,
data: null,
refetch: jest.fn(),
})

render(
<LanguageContext.Provider value='en'>
<Wrapper />
</LanguageContext.Provider>
)

const wrapper = await screen.findByTestId('wrapper')
expect(wrapper).toBeInTheDocument()
})

const queryClient = new QueryClient()
it('renders error state', async () => {
;(checkSettings as jest.Mock).mockResolvedValue(false)

describe('Wrapper', () => {
it('renders', () => {
const { getByTestId } = render(
<QueryClientProvider client={queryClient}>
render(
<LanguageContext.Provider value='en'>
<Wrapper />
</QueryClientProvider>
</LanguageContext.Provider>
)

expect(getByTestId('wrapper')).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText('connect.server')).toBeInTheDocument()
})
})
})
25 changes: 25 additions & 0 deletions __tests__/unit/client/context/theme.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import ThemeProvider from '../../../../src/client/context/theme'

describe('ThemeProvider', () => {
it('should render children correctly', () => {
render(
<ThemeProvider>
<div data-testid='child'>Hello World</div>
</ThemeProvider>
)
expect(screen.getByTestId('child')).toBeInTheDocument()
})

it('should set initial theme based on localStorage', () => {
localStorage.setItem('theme', 'dark')
render(
<ThemeProvider>
<div data-testid='child'>Hello World</div>
</ThemeProvider>
)
expect(document.documentElement.classList.contains('dark')).toBe(true)
})
})
Loading

0 comments on commit 3d0d65b

Please sign in to comment.