Improved sensors page
This commit is contained in:
parent
4c19e89c1c
commit
badde4a20e
|
|
@ -6,6 +6,7 @@ export type AlertInfo = {
|
||||||
condition: string
|
condition: string
|
||||||
contactPointId: number
|
contactPointId: number
|
||||||
customMessage: string
|
customMessage: string
|
||||||
|
customResolvedMessage: string
|
||||||
triggerInterval: number
|
triggerInterval: number
|
||||||
lastStatus: string
|
lastStatus: string
|
||||||
lastStatusAt: string
|
lastStatusAt: string
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
button {
|
button,
|
||||||
|
.button {
|
||||||
font-family: var(--main-font);
|
font-family: var(--main-font);
|
||||||
background: var(--button-bg-color);
|
background: var(--button-bg-color);
|
||||||
color: var(--button-fg-color);
|
color: var(--button-fg-color);
|
||||||
|
|
@ -10,6 +11,17 @@ button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: var(--button-disabled-bg-color);
|
||||||
|
color: var(--button-disabled-fg-color);
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-disabled-bg-color);
|
||||||
|
color: var(--button-disabled-fg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--button-hover-bg-color);
|
background-color: var(--button-hover-bg-color);
|
||||||
color: var(--button-hover-fg-color);
|
color: var(--button-hover-fg-color);
|
||||||
|
|
@ -28,10 +40,19 @@ button {
|
||||||
color: var(--button-cancel-fg-color);
|
color: var(--button-cancel-fg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
> .svg-icon {
|
> .svg-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
margin-right: 0.2rem;
|
margin-right: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.danger-variant {
|
||||||
|
background-color: var(--button-remove-bg-color);
|
||||||
|
color: var(--button-remove-fg-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.4rem 0.5rem;
|
||||||
|
|
||||||
&.actions {
|
&.actions {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,10 @@ form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.input {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
text-align: right;
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|
@ -24,6 +16,12 @@ form {
|
||||||
background-color: var(--button-cancel-bg-color);
|
background-color: var(--button-cancel-bg-color);
|
||||||
color: var(--button-cancel-fg-color);
|
color: var(--button-cancel-fg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.remove {
|
||||||
|
margin-right: auto;
|
||||||
|
background-color: var(--button-remove-bg-color);
|
||||||
|
color: var(--button-remove-fg-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,3 +40,32 @@ form.horizontal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
border-color: var(--input-border-color);
|
||||||
|
|
||||||
|
.input:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
border: 1px solid var(--input-border-color);
|
||||||
|
|
||||||
|
.input:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
>.body>.hint {
|
||||||
|
color: var(--input-hint-color);
|
||||||
|
font-size: 90%;
|
||||||
|
margin-top: 0rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
select,
|
select,
|
||||||
input {
|
input,
|
||||||
|
textarea {
|
||||||
font-family: var(--main-font);
|
font-family: var(--main-font);
|
||||||
padding: 0.15rem 0.4rem;
|
padding: 0.15rem 0.4rem;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -25,6 +26,32 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.input-suffix {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid var(--input-border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
margin-left: -2px;
|
||||||
|
color: var(--input-appendix-fg-color);
|
||||||
|
background-color: var(--input-appendix-bg-color);
|
||||||
|
|
||||||
|
>span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0.3rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
>button {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox-label {
|
.checkbox-label {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -58,3 +85,68 @@ input {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
|
&.no-margin {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.with-hint {
|
||||||
|
label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: var(--input-hint-color);
|
||||||
|
font-size: 90%;
|
||||||
|
margin-top: 0rem;
|
||||||
|
margin-bottom: 0.50rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-input {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 1rem;
|
||||||
|
|
||||||
|
.min,
|
||||||
|
.max {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5m6 4.125 2.25 2.25m0 0 2.25 2.25M12 13.875l2.25-2.25M12 13.875l-2.25 2.25M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 541 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 244 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 637 B |
|
|
@ -5,49 +5,18 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
margin-left: auto;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
section.content {
|
section.content {
|
||||||
.sensors-list {
|
max-width: 50rem;
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(6, 1fr);
|
|
||||||
overflow: auto;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
}
|
||||||
@media screen and (max-width: 1500px) {
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
.sensor-detail {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
.auth {
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
grid-template-columns: repeat(1, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sensor-item {
|
|
||||||
margin: 0.25rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .name {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .auth {
|
|
||||||
.auth-value {
|
.auth-value {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
|
|
@ -87,7 +56,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .actions {
|
.actions {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
|
@ -96,6 +65,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
--button-cancel-fg-color: #000;
|
--button-cancel-fg-color: #000;
|
||||||
--button-remove-bg-color: transparent;
|
--button-remove-bg-color: transparent;
|
||||||
--button-remove-fg-color: #f00;
|
--button-remove-fg-color: #f00;
|
||||||
|
--button-disabled-bg-color: #91a9b0;
|
||||||
|
--button-disabled-fg-color: #000;
|
||||||
--header-bg-color: #fff;
|
--header-bg-color: #fff;
|
||||||
--header-spacer-color: #ddd;
|
--header-spacer-color: #ddd;
|
||||||
--header-shadow: linear-gradient(
|
--header-shadow: linear-gradient(
|
||||||
|
|
@ -48,8 +50,11 @@
|
||||||
--input-focus-border-color: #3988ff;
|
--input-focus-border-color: #3988ff;
|
||||||
--input-bg-color: #fff;
|
--input-bg-color: #fff;
|
||||||
--input-fg-color: #000;
|
--input-fg-color: #000;
|
||||||
|
--input-appendix-bg-color: #eee;
|
||||||
|
--input-appendix-fg-color: #000;
|
||||||
--input-disabled-fg-color: #000;
|
--input-disabled-fg-color: #000;
|
||||||
--input-disabled-bg-color: #fff;
|
--input-disabled-bg-color: #fff;
|
||||||
|
--input-hint-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
@ -76,6 +81,8 @@
|
||||||
--input-focus-border-color: #666;
|
--input-focus-border-color: #666;
|
||||||
--input-bg-color: #222;
|
--input-bg-color: #222;
|
||||||
--input-fg-color: #ccc;
|
--input-fg-color: #ccc;
|
||||||
|
--input-appendix-bg-color: #333;
|
||||||
|
--input-appendix-fg-color: #ccc;
|
||||||
--input-disabled-fg-color: #bbb;
|
--input-disabled-fg-color: #bbb;
|
||||||
--input-disabled-bg-color: #2a2a2a;
|
--input-disabled-bg-color: #2a2a2a;
|
||||||
--menu-shadow: linear-gradient(
|
--menu-shadow: linear-gradient(
|
||||||
|
|
@ -83,6 +90,9 @@
|
||||||
rgba(0, 0, 0, 0) 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
rgba(0, 0, 0, 0.2) 100%
|
rgba(0, 0, 0, 0.2) 100%
|
||||||
);
|
);
|
||||||
|
--input-hint-color: #aaa;
|
||||||
|
--button-remove-bg-color: transparent;
|
||||||
|
--button-remove-fg-color: rgb(255, 118, 118);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sensor-item .auth .auth-value .label {
|
.sensor-item .auth .auth-value .label {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { cn } from '@/utils/cn'
|
||||||
|
import { ComponentChild } from 'preact'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string
|
||||||
|
name?: string
|
||||||
|
children: ComponentChild
|
||||||
|
hint?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormCheckboxField = ({ label, children, name, hint }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className={cn('input', hint && 'with-hint')}>
|
||||||
|
<label className="checkbox-label" htmlFor={name}>
|
||||||
|
{children} <span>{label}</span>
|
||||||
|
</label>
|
||||||
|
{hint && <div className="hint">{hint}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { cn } from '@/utils/cn'
|
||||||
|
import { ComponentChild } from 'preact'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string
|
||||||
|
name?: string
|
||||||
|
children: ComponentChild
|
||||||
|
hint?: string
|
||||||
|
inputWidth?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormField = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
name,
|
||||||
|
hint,
|
||||||
|
inputWidth,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<div className={cn('input', hint && 'with-hint')}>
|
||||||
|
<label htmlFor={name}>{label}</label>
|
||||||
|
{hint && <div className="hint">{hint}</div>}
|
||||||
|
<div className="control" style={{ width: inputWidth }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { ComponentChild } from 'preact'
|
||||||
|
import { Box } from './Box'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
actions?: ComponentChild
|
||||||
|
children: ComponentChild
|
||||||
|
collapsible?: boolean
|
||||||
|
defaultCollapsed?: boolean
|
||||||
|
hint?: ComponentChild
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormSection = ({
|
||||||
|
title,
|
||||||
|
actions,
|
||||||
|
children,
|
||||||
|
collapsible,
|
||||||
|
defaultCollapsed,
|
||||||
|
hint,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
collapsible={collapsible}
|
||||||
|
header={title}
|
||||||
|
defaultCollapsed={defaultCollapsed}
|
||||||
|
className={'form-section'}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
{hint && <div className="hint">{hint}</div>}
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -6,22 +6,25 @@ type Props = {
|
||||||
children: ComponentChild
|
children: ComponentChild
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateModalProps = {
|
type CreateModalProps<TInput = void> = {
|
||||||
content: ComponentChild
|
content: ComponentChild | ((data: TInput) => ComponentChild)
|
||||||
onConfirm: () => void
|
onConfirm: (data: TInput) => void
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateModalResult = {
|
type CreateModalResult<TInput = void> = {
|
||||||
show: () => void
|
show: TInput extends void ? () => void : (input: TInput) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfirmModalsContextType = {
|
type ConfirmModalsContextType = {
|
||||||
createModal: (props: CreateModalProps) => CreateModalResult
|
createModal: <TInput = void>(
|
||||||
|
props: CreateModalProps<TInput>
|
||||||
|
) => CreateModalResult<TInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModalState = {
|
type ModalState<TInput = void> = {
|
||||||
id: string
|
id: string
|
||||||
|
input: TInput
|
||||||
props: CreateModalProps
|
props: CreateModalProps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,15 +35,23 @@ const ConfirmModalsContext = createContext<ConfirmModalsContextType | null>(
|
||||||
export const ConfirmModalsContextProvider = ({ children }: Props) => {
|
export const ConfirmModalsContextProvider = ({ children }: Props) => {
|
||||||
const [modals, setModals] = useState([] as ModalState[])
|
const [modals, setModals] = useState([] as ModalState[])
|
||||||
|
|
||||||
const createModal = useCallback((props: CreateModalProps) => {
|
const createModal = useCallback(
|
||||||
|
<TInput = void,>(props: CreateModalProps<TInput>) => {
|
||||||
return {
|
return {
|
||||||
show: () =>
|
show: ((input: TInput) =>
|
||||||
setModals((p) => [
|
setModals((p) => [
|
||||||
...p,
|
...p,
|
||||||
{ id: new Date().getTime().toString(), props },
|
{
|
||||||
]),
|
id: new Date().getTime().toString(),
|
||||||
|
props,
|
||||||
|
input,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} as ModalState<any>,
|
||||||
|
])) as TInput extends void ? () => void : (input: TInput) => void,
|
||||||
}
|
}
|
||||||
}, [])
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const handleClose = (modal: ModalState) => {
|
const handleClose = (modal: ModalState) => {
|
||||||
setModals((p) => p.filter((m) => m.id !== modal.id))
|
setModals((p) => p.filter((m) => m.id !== modal.id))
|
||||||
|
|
@ -51,7 +62,7 @@ export const ConfirmModalsContextProvider = ({ children }: Props) => {
|
||||||
const handleConfirm = (modal: ModalState) => {
|
const handleConfirm = (modal: ModalState) => {
|
||||||
setModals((p) => p.filter((m) => m.id !== modal.id))
|
setModals((p) => p.filter((m) => m.id !== modal.id))
|
||||||
|
|
||||||
modal.props.onConfirm()
|
modal.props.onConfirm(modal.input)
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = useMemo(() => ({ createModal }), [createModal])
|
const value = useMemo(() => ({ createModal }), [createModal])
|
||||||
|
|
@ -66,7 +77,9 @@ export const ConfirmModalsContextProvider = ({ children }: Props) => {
|
||||||
onCancel={() => handleClose(m)}
|
onCancel={() => handleClose(m)}
|
||||||
onConfirm={() => handleConfirm(m)}
|
onConfirm={() => handleConfirm(m)}
|
||||||
>
|
>
|
||||||
{m.props.content}
|
{typeof m.props.content === 'function'
|
||||||
|
? m.props.content(m.input)
|
||||||
|
: m.props.content}
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
||||||
))}
|
))}
|
||||||
</ConfirmModalsContext.Provider>
|
</ConfirmModalsContext.Provider>
|
||||||
|
|
@ -83,8 +96,10 @@ export const useConfirmModalsContext = () => {
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useConfirmModal = (modal: CreateModalProps) => {
|
export const useConfirmModal = <TInput = void,>(
|
||||||
|
modal: CreateModalProps<TInput>
|
||||||
|
) => {
|
||||||
const ctx = useConfirmModalsContext()
|
const ctx = useConfirmModalsContext()
|
||||||
|
|
||||||
return ctx.createModal(modal)
|
return ctx.createModal<TInput>(modal)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,7 @@ export { ReactComponent as EyeOffIcon } from '@/assets/icons/eye-off.svg'
|
||||||
export { ReactComponent as EditIcon } from '@/assets/icons/edit.svg'
|
export { ReactComponent as EditIcon } from '@/assets/icons/edit.svg'
|
||||||
export { ReactComponent as ClipboardCopyIcon } from '@/assets/icons/clipboard-copy.svg'
|
export { ReactComponent as ClipboardCopyIcon } from '@/assets/icons/clipboard-copy.svg'
|
||||||
export { ReactComponent as ClipboardCheckIcon } from '@/assets/icons/clipboard-check.svg'
|
export { ReactComponent as ClipboardCheckIcon } from '@/assets/icons/clipboard-check.svg'
|
||||||
export { ReactComponent as ChevronDown } from '@/assets/icons/chevron-down.svg'
|
export { ReactComponent as ChevronDownIcon } from '@/assets/icons/chevron-down.svg'
|
||||||
|
export { ReactComponent as ArchiveIcon } from '@/assets/icons/archive.svg'
|
||||||
|
export { ReactComponent as TrashIcon } from '@/assets/icons/trash.svg'
|
||||||
|
export { ReactComponent as TimesIcon } from '@/assets/icons/times.svg'
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { AlertInfo, deleteAlert, getAlerts } from '@/api/alerts'
|
import { AlertInfo, deleteAlert, getAlerts } from '@/api/alerts'
|
||||||
import { ConfirmModal } from '@/components/ConfirmModal'
|
import { ConfirmModal } from '@/components/ConfirmModal'
|
||||||
import { DataTable } from '@/components/DataTable'
|
import { DataTable } from '@/components/DataTable'
|
||||||
|
import { EditIcon, PlusIcon, TrashIcon } from '@/icons'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import { useMutation, useQuery } from 'react-query'
|
import { useMutation, useQuery } from 'react-query'
|
||||||
import { AlertFormModal } from './components/AlertFormModal'
|
import { AlertFormModal } from './components/AlertFormModal'
|
||||||
import { EditIcon, PlusIcon, RefreshIcon } from '@/icons'
|
|
||||||
|
|
||||||
export const AlertsTable = () => {
|
export const AlertsTable = () => {
|
||||||
const alerts = useQuery('/alerts', () => getAlerts())
|
const alerts = useQuery('/alerts', () => getAlerts(), {
|
||||||
|
refetchInterval: 500,
|
||||||
|
})
|
||||||
|
|
||||||
const deleteMutation = useMutation(deleteAlert, {
|
const deleteMutation = useMutation(deleteAlert, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -25,9 +27,6 @@ export const AlertsTable = () => {
|
||||||
<div className="section-title">
|
<div className="section-title">
|
||||||
<h2>Alerts</h2>
|
<h2>Alerts</h2>
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => alerts.refetch()}>
|
|
||||||
<RefreshIcon /> Refresh
|
|
||||||
</button>{' '}
|
|
||||||
<button onClick={() => setShowNew(true)}>
|
<button onClick={() => setShowNew(true)}>
|
||||||
<PlusIcon /> Add alert
|
<PlusIcon /> Add alert
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -48,14 +47,19 @@ export const AlertsTable = () => {
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
width: '10rem',
|
width: '12rem',
|
||||||
className: 'actions',
|
className: 'actions',
|
||||||
render: (c) => (
|
render: (c) => (
|
||||||
<div>
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDelete(c)}
|
||||||
|
className="danger-variant"
|
||||||
|
>
|
||||||
|
<TrashIcon /> Delete
|
||||||
|
</button>
|
||||||
<button onClick={() => setEdited(c)}>
|
<button onClick={() => setEdited(c)}>
|
||||||
<EditIcon /> Edit
|
<EditIcon /> Edit
|
||||||
</button>{' '}
|
</button>
|
||||||
<button onClick={() => setShowDelete(c)}>Delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { AlertInfo, createAlert, updateAlert } from '@/api/alerts'
|
import { AlertInfo, createAlert, updateAlert } from '@/api/alerts'
|
||||||
import { getContactPoints } from '@/api/contactPoints'
|
import { getContactPoints } from '@/api/contactPoints'
|
||||||
import { getSensors } from '@/api/sensors'
|
import { getSensors } from '@/api/sensors'
|
||||||
|
import { FormField } from '@/components/FormField'
|
||||||
import { Modal } from '@/components/Modal'
|
import { Modal } from '@/components/Modal'
|
||||||
import { useForm } from '@/utils/hooks/useForm'
|
import { useForm } from '@/utils/hooks/useForm'
|
||||||
import { tryParseAlertCondition } from '@/utils/tryParseAlertCondition'
|
import { tryParseAlertCondition } from '@/utils/tryParseAlertCondition'
|
||||||
|
|
@ -28,10 +29,12 @@ export const AlertFormModal = ({ alert, open, onClose }: Props) => {
|
||||||
defaultValue: () => ({
|
defaultValue: () => ({
|
||||||
name: '',
|
name: '',
|
||||||
customMessage: '',
|
customMessage: '',
|
||||||
|
customResolvedMessage: '',
|
||||||
triggerInterval: 0,
|
triggerInterval: 0,
|
||||||
...(alert && {
|
...(alert && {
|
||||||
name: alert.name,
|
name: alert.name,
|
||||||
customMessage: alert.customMessage,
|
customMessage: alert.customMessage,
|
||||||
|
customResolvedMessage: alert.customResolvedMessage,
|
||||||
contactPointId: alert.contactPointId,
|
contactPointId: alert.contactPointId,
|
||||||
triggerInterval: alert.triggerInterval,
|
triggerInterval: alert.triggerInterval,
|
||||||
}),
|
}),
|
||||||
|
|
@ -85,6 +88,7 @@ export const AlertFormModal = ({ alert, open, onClose }: Props) => {
|
||||||
name: v.name,
|
name: v.name,
|
||||||
contactPointId: v.contactPointId,
|
contactPointId: v.contactPointId,
|
||||||
customMessage: v.customMessage,
|
customMessage: v.customMessage,
|
||||||
|
customResolvedMessage: v.customResolvedMessage,
|
||||||
triggerInterval: v.triggerInterval,
|
triggerInterval: v.triggerInterval,
|
||||||
condition,
|
condition,
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +111,7 @@ export const AlertFormModal = ({ alert, open, onClose }: Props) => {
|
||||||
const conditionType = watch('conditionType')
|
const conditionType = watch('conditionType')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onClose={onClose}>
|
<Modal open={open} onClose={onClose} width="30rem">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
|
|
@ -125,20 +129,39 @@ export const AlertFormModal = ({ alert, open, onClose }: Props) => {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input">
|
<FormField
|
||||||
<label>Custom Message</label>
|
name="customMessage"
|
||||||
|
label="Custom Triggered Message"
|
||||||
|
hint="Message sent when alert is triggered"
|
||||||
|
>
|
||||||
<textarea {...register('customMessage')} />
|
<textarea {...register('customMessage')} />
|
||||||
</div>
|
</FormField>
|
||||||
|
|
||||||
<div className="input">
|
<FormField
|
||||||
<label>Trigger After (seconds)</label>
|
name="customResolvedMessage"
|
||||||
|
label="Custom Resolved Message"
|
||||||
|
hint="Message sent when alert is resolved"
|
||||||
|
>
|
||||||
|
<textarea {...register('customResolvedMessage')} />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name="triggerInterval"
|
||||||
|
label="Trigger After"
|
||||||
|
hint="How long to wait when condition is alerting until actual alert is sent out"
|
||||||
|
>
|
||||||
|
<div className="input-container">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
required
|
required
|
||||||
{...register('triggerInterval', { type: 'integer' })}
|
{...register('triggerInterval', { type: 'integer' })}
|
||||||
/>
|
/>
|
||||||
|
<div className="input-suffix">
|
||||||
|
<span>seconds</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Condition Type</label>
|
<label>Condition Type</label>
|
||||||
|
|
@ -165,29 +188,27 @@ export const AlertFormModal = ({ alert, open, onClose }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Condition</label>
|
<label>When last value is</label>
|
||||||
|
<div className="input-container">
|
||||||
<select required {...register('sensorValueCondition')}>
|
<select required {...register('sensorValueCondition')}>
|
||||||
<option value="less">Less</option>
|
<option value="less">Less than</option>
|
||||||
<option value="more">More</option>
|
<option value="more">More than</option>
|
||||||
<option value="equal">Equal</option>
|
<option value="equal">Equal</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input">
|
|
||||||
<label>Value</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
{...register('sensorValueValue', { type: 'number' })}
|
{...register('sensorValueValue', { type: 'number' })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{conditionType === 'sensor_last_contact' && (
|
{conditionType === 'sensor_last_contact' && (
|
||||||
<>
|
<>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Sensor</label>
|
<label>Target Sensor</label>
|
||||||
<select
|
<select
|
||||||
required
|
required
|
||||||
{...register('sensorLastContactSensorId', { type: 'integer' })}
|
{...register('sensorLastContactSensorId', { type: 'integer' })}
|
||||||
|
|
@ -201,16 +222,13 @@ export const AlertFormModal = ({ alert, open, onClose }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Value</label>
|
<label>{"When sensor didn't submit anything for"}</label>
|
||||||
|
<div className="input-container">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
{...register('sensorLastContactValue', { type: 'number' })}
|
{...register('sensorLastContactValue', { type: 'number' })}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="input">
|
|
||||||
<label>Value Unit</label>
|
|
||||||
<select required {...register('sensorLastContactValueUnit')}>
|
<select required {...register('sensorLastContactValueUnit')}>
|
||||||
<option value="s">Seconds</option>
|
<option value="s">Seconds</option>
|
||||||
<option value="m">Minutes</option>
|
<option value="m">Minutes</option>
|
||||||
|
|
@ -218,6 +236,7 @@ export const AlertFormModal = ({ alert, open, onClose }: Props) => {
|
||||||
<option value="d">Days</option>
|
<option value="d">Days</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import {
|
||||||
deleteContactPoint,
|
deleteContactPoint,
|
||||||
getContactPoints,
|
getContactPoints,
|
||||||
} from '@/api/contactPoints'
|
} from '@/api/contactPoints'
|
||||||
|
import { ConfirmModal } from '@/components/ConfirmModal'
|
||||||
|
import { DataTable } from '@/components/DataTable'
|
||||||
|
import { EditIcon, PlusIcon, TrashIcon } from '@/icons'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import { useMutation, useQuery } from 'react-query'
|
import { useMutation, useQuery } from 'react-query'
|
||||||
import { ContactPointFormModal } from './components/ContactPointFormModal'
|
import { ContactPointFormModal } from './components/ContactPointFormModal'
|
||||||
import { DataTable } from '@/components/DataTable'
|
|
||||||
import { ConfirmModal } from '@/components/ConfirmModal'
|
|
||||||
import { PlusIcon } from '@/icons'
|
|
||||||
|
|
||||||
export const ContactPointsTable = () => {
|
export const ContactPointsTable = () => {
|
||||||
const contactPoints = useQuery('/contact-points', () => getContactPoints())
|
const contactPoints = useQuery('/contact-points', () => getContactPoints())
|
||||||
|
|
@ -47,12 +47,16 @@ export const ContactPointsTable = () => {
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
width: '10rem',
|
width: '12rem',
|
||||||
className: 'actions',
|
className: 'actions',
|
||||||
render: (c) => (
|
render: (c) => (
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => setEdited(c)}>Edit</button>
|
<button onClick={() => setShowDelete(c)} className="remove">
|
||||||
<button onClick={() => setShowDelete(c)}>Delete</button>
|
<TrashIcon /> Delete
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setEdited(c)}>
|
||||||
|
<EditIcon /> Edit
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ChevronDown } from '@/icons'
|
import { ChevronDownIcon } from '@/icons'
|
||||||
import { useDashboardContext } from '@/pages/dashboard/contexts/DashboardContext'
|
import { useDashboardContext } from '@/pages/dashboard/contexts/DashboardContext'
|
||||||
import { useFilterIntervals } from '@/pages/dashboard/hooks/useFilterIntervals'
|
import { useFilterIntervals } from '@/pages/dashboard/hooks/useFilterIntervals'
|
||||||
import { cn } from '@/utils/cn'
|
import { cn } from '@/utils/cn'
|
||||||
|
|
@ -22,7 +22,7 @@ export const DashboardHeaderFilterToggle = () => {
|
||||||
<div className="filter-toggle">
|
<div className="filter-toggle">
|
||||||
<div className="current-value" onClick={() => setShow((v) => !v)}>
|
<div className="current-value" onClick={() => setShow((v) => !v)}>
|
||||||
<span>{selected}</span>
|
<span>{selected}</span>
|
||||||
<ChevronDown />
|
<ChevronDownIcon />
|
||||||
</div>
|
</div>
|
||||||
<div className={cn('filter-popup', show && 'show')}>
|
<div className={cn('filter-popup', show && 'show')}>
|
||||||
<div className="box">
|
<div className="box">
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,30 @@
|
||||||
import { getSensors, SensorInfo } from '@/api/sensors'
|
import { deleteSensor, getSensors, SensorInfo } from '@/api/sensors'
|
||||||
import { PlusIcon } from '@/icons'
|
import { DataTable } from '@/components/DataTable'
|
||||||
|
import { EditIcon, EyeIcon, PlusIcon, TrashIcon } from '@/icons'
|
||||||
import { UserLayout } from '@/layouts/UserLayout/UserLayout'
|
import { UserLayout } from '@/layouts/UserLayout/UserLayout'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import { useQuery } from 'react-query'
|
import { useMutation, useQuery, useQueryClient } from 'react-query'
|
||||||
import { SensorFormModal } from './components/SensorFormModal'
|
import { SensorFormModal } from './components/SensorFormModal'
|
||||||
import { SensorItem } from './components/SensorItem'
|
import { useConfirmModal } from '@/contexts/ConfirmModalsContext'
|
||||||
|
import { SensorDetailModal } from './components/SensorDetailModal'
|
||||||
|
|
||||||
export const SensorsPage = () => {
|
export const SensorsPage = () => {
|
||||||
const sensors = useQuery(['/sensors'], getSensors)
|
const sensors = useQuery(['/sensors'], getSensors)
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const deleteMutation = useMutation(deleteSensor, {
|
||||||
|
onSuccess: () => queryClient.invalidateQueries(['/sensors']),
|
||||||
|
})
|
||||||
|
|
||||||
const [showNew, setShowNew] = useState(false)
|
const [showNew, setShowNew] = useState(false)
|
||||||
const [edited, setEdited] = useState<SensorInfo>()
|
const [edited, setEdited] = useState<SensorInfo>()
|
||||||
|
const [shown, setShown] = useState<SensorInfo>()
|
||||||
|
|
||||||
|
const deleteConfirm = useConfirmModal({
|
||||||
|
content: (deleted: SensorInfo) =>
|
||||||
|
`Are you sure you want to delete sensor ${deleted.name}?`,
|
||||||
|
onConfirm: (deleted) => deleteMutation.mutate(deleted.id),
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserLayout
|
<UserLayout
|
||||||
|
|
@ -24,11 +38,38 @@ export const SensorsPage = () => {
|
||||||
}
|
}
|
||||||
className="sensors-page"
|
className="sensors-page"
|
||||||
>
|
>
|
||||||
<div className="sensors-list">
|
<div className="box-shadow">
|
||||||
{sensors.data?.map((i) => (
|
<DataTable
|
||||||
<SensorItem key={i.id} sensor={i} onEdit={setEdited} />
|
data={sensors.data ?? []}
|
||||||
))}
|
columns={[
|
||||||
|
{ key: 'type', title: 'ID', render: (c) => c.id, width: '1rem' },
|
||||||
|
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
title: 'Actions',
|
||||||
|
width: '15rem',
|
||||||
|
className: 'actions',
|
||||||
|
render: (c) => (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="danger-variant"
|
||||||
|
onClick={() => deleteConfirm.show(c)}
|
||||||
|
>
|
||||||
|
<TrashIcon /> Delete
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setEdited(c)}>
|
||||||
|
<EditIcon /> Edit
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setShown(c)}>
|
||||||
|
<EyeIcon /> Detail
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{(showNew || edited) && (
|
{(showNew || edited) && (
|
||||||
<SensorFormModal
|
<SensorFormModal
|
||||||
open
|
open
|
||||||
|
|
@ -39,6 +80,18 @@ export const SensorsPage = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shown && (
|
||||||
|
<SensorDetailModal
|
||||||
|
open
|
||||||
|
sensor={shown}
|
||||||
|
onClose={() => setShown(undefined)}
|
||||||
|
onEdit={() => {
|
||||||
|
setShown(undefined)
|
||||||
|
setEdited(shown)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</UserLayout>
|
</UserLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { SensorInfo } from '@/api/sensors'
|
||||||
|
import { InputWithCopy } from '@/components/InputWithCopy'
|
||||||
|
import { Modal } from '@/components/Modal'
|
||||||
|
import { CancelIcon, EditIcon } from '@/icons'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onEdit: () => void
|
||||||
|
sensor: SensorInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SensorDetailModal = ({ open, onClose, sensor, onEdit }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose} className="sensor-detail">
|
||||||
|
<div className="name">{sensor.name}</div>
|
||||||
|
|
||||||
|
<div className="auth">
|
||||||
|
<div className="auth-value">
|
||||||
|
<div className="label">ID</div>
|
||||||
|
<div className="value">
|
||||||
|
<InputWithCopy value={sensor.id.toString()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="auth-value">
|
||||||
|
<div className="label">KEY</div>
|
||||||
|
<div className="value">
|
||||||
|
<InputWithCopy value={sensor.authKey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<button onClick={() => onClose()} className="cancel">
|
||||||
|
<CancelIcon /> Close
|
||||||
|
</button>
|
||||||
|
<button onClick={() => onEdit()}>
|
||||||
|
<EditIcon /> Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE alerts ADD COLUMN custom_resolved_message TEXT DEFAULT '' NOT NULL;
|
||||||
|
|
@ -73,8 +73,8 @@ func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig stri
|
||||||
if data.SensorValueCondition != nil {
|
if data.SensorValueCondition != nil {
|
||||||
text := fmt.Sprintf("✅ %s is at {value}", data.Sensor.Name)
|
text := fmt.Sprintf("✅ %s is at {value}", data.Sensor.Name)
|
||||||
|
|
||||||
if data.Alert.CustomMessage != "" {
|
if data.Alert.CustomResolvedMessage != "" {
|
||||||
text = data.Alert.CustomMessage
|
text = data.Alert.CustomResolvedMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1)
|
text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ type AlertItem struct {
|
||||||
Name string `json:"name" db:"name"`
|
Name string `json:"name" db:"name"`
|
||||||
Condition string `json:"condition" db:"condition"`
|
Condition string `json:"condition" db:"condition"`
|
||||||
CustomMessage string `json:"customMessage" db:"custom_message"`
|
CustomMessage string `json:"customMessage" db:"custom_message"`
|
||||||
|
CustomResolvedMessage string `json:"customResolvedMessage" db:"custom_resolved_message"`
|
||||||
|
|
||||||
/* how long does the condition have to be true for the alert to go off */
|
/* how long does the condition have to be true for the alert to go off */
|
||||||
TriggerInterval int64 `json:"triggerInterval" db:"trigger_interval"`
|
TriggerInterval int64 `json:"triggerInterval" db:"trigger_interval"`
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ type postAlertsBody struct {
|
||||||
Condition string `json:"condition"`
|
Condition string `json:"condition"`
|
||||||
TriggerInterval int64 `json:"triggerInterval"`
|
TriggerInterval int64 `json:"triggerInterval"`
|
||||||
CustomMessage string `json:"customMessage"`
|
CustomMessage string `json:"customMessage"`
|
||||||
|
CustomResolvedMessage string `json:"customResolvedMessage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type putAlertsBody struct {
|
type putAlertsBody struct {
|
||||||
|
|
@ -22,6 +23,7 @@ type putAlertsBody struct {
|
||||||
Condition string `json:"condition"`
|
Condition string `json:"condition"`
|
||||||
TriggerInterval int64 `json:"triggerInterval"`
|
TriggerInterval int64 `json:"triggerInterval"`
|
||||||
CustomMessage string `json:"customMessage"`
|
CustomMessage string `json:"customMessage"`
|
||||||
|
CustomResolvedMessage string `json:"customResolvedMessage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAlerts(s *app.Server) gin.HandlerFunc {
|
func GetAlerts(s *app.Server) gin.HandlerFunc {
|
||||||
|
|
@ -46,7 +48,7 @@ func PostAlerts(s *app.Server) gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alert, err := s.Services.Alerts.Create(body.ContactPointId, body.Name, body.Condition, body.TriggerInterval, body.CustomMessage)
|
alert, err := s.Services.Alerts.Create(body.ContactPointId, body.Name, body.Condition, body.TriggerInterval, body.CustomMessage, body.CustomResolvedMessage)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
|
@ -73,7 +75,7 @@ func PutAlert(s *app.Server) gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alert, err := s.Services.Alerts.Update(alertId, body.ContactPointId, body.Name, body.Condition, body.TriggerInterval, body.CustomMessage)
|
alert, err := s.Services.Alerts.Update(alertId, body.ContactPointId, body.Name, body.Condition, body.TriggerInterval, body.CustomMessage, body.CustomResolvedMessage)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ func (s *AlertsService) GetList() ([]models.AlertItem, error) {
|
||||||
alerts := []models.AlertItem{}
|
alerts := []models.AlertItem{}
|
||||||
|
|
||||||
err := s.ctx.DB.Select(&alerts, `
|
err := s.ctx.DB.Select(&alerts, `
|
||||||
SELECT id, contact_point_id, name, custom_message, condition, trigger_interval, last_status, last_status_at
|
SELECT id, contact_point_id, name, custom_message, custom_resolved_message, condition, trigger_interval, last_status, last_status_at
|
||||||
FROM alerts
|
FROM alerts
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
`)
|
`)
|
||||||
|
|
@ -30,7 +30,7 @@ func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) {
|
||||||
|
|
||||||
err := s.ctx.DB.Get(&alert,
|
err := s.ctx.DB.Get(&alert,
|
||||||
`
|
`
|
||||||
SELECT id, contact_point_id, name, custom_message, condition, trigger_interval, last_status, last_status_at
|
SELECT id, contact_point_id, name, custom_message, custom_resolved_message, condition, trigger_interval, last_status, last_status_at
|
||||||
FROM alerts
|
FROM alerts
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`,
|
`,
|
||||||
|
|
@ -44,13 +44,14 @@ func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) {
|
||||||
return &alert, nil
|
return &alert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AlertsService) Create(contactPointId int64, name string, condition string, triggerInterval int64, customMessage string) (*models.AlertItem, error) {
|
func (s *AlertsService) Create(contactPointId int64, name string, condition string, triggerInterval int64, customMessage string, customResolvedMessage string) (*models.AlertItem, error) {
|
||||||
alert := models.AlertItem{
|
alert := models.AlertItem{
|
||||||
ContactPointId: contactPointId,
|
ContactPointId: contactPointId,
|
||||||
Name: name,
|
Name: name,
|
||||||
Condition: condition,
|
Condition: condition,
|
||||||
TriggerInterval: triggerInterval,
|
TriggerInterval: triggerInterval,
|
||||||
CustomMessage: customMessage,
|
CustomMessage: customMessage,
|
||||||
|
CustomResolvedMessage: customResolvedMessage,
|
||||||
LastStatus: models.AlertStatusOk,
|
LastStatus: models.AlertStatusOk,
|
||||||
LastStatusAt: time.Now().Unix(),
|
LastStatusAt: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
@ -58,8 +59,8 @@ func (s *AlertsService) Create(contactPointId int64, name string, condition stri
|
||||||
res, err := s.ctx.DB.NamedExec(
|
res, err := s.ctx.DB.NamedExec(
|
||||||
`
|
`
|
||||||
INSERT INTO alerts
|
INSERT INTO alerts
|
||||||
(contact_point_id, name, condition, trigger_interval, last_status, last_status_at, custom_message) VALUES
|
(contact_point_id, name, condition, trigger_interval, last_status, last_status_at, custom_message, custom_resolved_message) VALUES
|
||||||
(:contact_point_id, :name, :condition, :trigger_interval, :last_status, :last_status_at, :custom_message)
|
(:contact_point_id, :name, :condition, :trigger_interval, :last_status, :last_status_at, :custom_message, :custom_resolved_message)
|
||||||
`,
|
`,
|
||||||
alert,
|
alert,
|
||||||
)
|
)
|
||||||
|
|
@ -77,7 +78,7 @@ func (s *AlertsService) Create(contactPointId int64, name string, condition stri
|
||||||
return &alert, nil
|
return &alert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64, customMessage string) (*models.AlertItem, error) {
|
func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64, customMessage string, customResolvedMessage string) (*models.AlertItem, error) {
|
||||||
alert := models.AlertItem{
|
alert := models.AlertItem{
|
||||||
Id: id,
|
Id: id,
|
||||||
ContactPointId: contactPointId,
|
ContactPointId: contactPointId,
|
||||||
|
|
@ -85,6 +86,7 @@ func (s *AlertsService) Update(id int64, contactPointId int64, name string, cond
|
||||||
Condition: condition,
|
Condition: condition,
|
||||||
TriggerInterval: triggerInterval,
|
TriggerInterval: triggerInterval,
|
||||||
CustomMessage: customMessage,
|
CustomMessage: customMessage,
|
||||||
|
CustomResolvedMessage: customResolvedMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.ctx.DB.NamedExec(
|
_, err := s.ctx.DB.NamedExec(
|
||||||
|
|
@ -95,7 +97,8 @@ func (s *AlertsService) Update(id int64, contactPointId int64, name string, cond
|
||||||
contact_point_id = :contact_point_id,
|
contact_point_id = :contact_point_id,
|
||||||
condition = :condition,
|
condition = :condition,
|
||||||
trigger_interval = :trigger_interval,
|
trigger_interval = :trigger_interval,
|
||||||
custom_message = :custom_message
|
custom_message = :custom_message,
|
||||||
|
custom_resolved_message = :custom_resolved_message
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
`,
|
`,
|
||||||
alert,
|
alert,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue