├── backend ├── app │ ├── __init__.py │ ├── routes │ │ ├── __init__.py │ │ ├── health.py │ │ ├── statuses.py │ │ ├── world.py │ │ └── lifecycle.py │ ├── schemas │ │ ├── __init__.py │ │ ├── special.py │ │ ├── base.py │ │ ├── custom_fields.py │ │ └── slack_events.py │ ├── services │ │ ├── __init__.py │ │ ├── slack │ │ │ ├── __init__.py │ │ │ ├── commands │ │ │ │ ├── __init__.py │ │ │ │ └── errors.py │ │ │ ├── forms │ │ │ │ ├── __init__.py │ │ │ │ └── base.py │ │ │ ├── interactions │ │ │ │ ├── __init__.py │ │ │ │ ├── create_incident.py │ │ │ │ └── update_incident.py │ │ │ └── renderer │ │ │ │ ├── __init__.py │ │ │ │ └── incident_info_message.py │ │ ├── events.py │ │ └── vercel │ │ │ └── models.py │ ├── repos │ │ ├── base_repo.py │ │ ├── __init__.py │ │ ├── slack_message.py │ │ ├── slack_bookmark.py │ │ ├── announcement_repo.py │ │ └── lifecycle_repo.py │ ├── tasks │ │ ├── base.py │ │ ├── create_slack_message.py │ │ ├── __init__.py │ │ ├── join_channel.py │ │ ├── verify_custom_domain.py │ │ ├── set_channel_topic.py │ │ ├── send_invite.py │ │ ├── send_verification_email.py │ │ └── invite_user_to_channel.py │ ├── models │ │ ├── incident_type_field.py │ │ ├── mixins.py │ │ ├── settings.py │ │ ├── incident_severity.py │ │ ├── lifecycle.py │ │ ├── incident_status.py │ │ ├── slack_bookmark.py │ │ ├── incident_role_assignment.py │ │ ├── organisation_member.py │ │ ├── incident_field_value.py │ │ ├── incident_type.py │ │ ├── announcement.py │ │ └── slack_message.py │ ├── env.py │ └── auth.py ├── .envrc ├── .python-version ├── .flake8 ├── migrations │ ├── README │ ├── script.py.mako │ └── versions │ │ ├── 2024_08_25_1924-7a2a8fd88e5c_rename_positon_rank.py │ │ ├── 2024_02_02_2105-121421196cb8_rename_col.py │ │ ├── 2024_03_09_1310-811b69c66d49_rename_field.py │ │ ├── 2024_02_18_1902-4ab0568af723_add_description_to_incident.py │ │ ├── 2024_06_07_2245-b40753c385bc_add_description_to_timestamps.py │ │ ├── 2024_06_04_2156-12eff6845f01_add_field.py │ │ ├── 2024_07_26_2145-3e7d09e28b97_add_default_to_incident_type.py │ │ ├── 2024_09_11_2053-e83f1525f07e_add_multi_default_value.py │ │ ├── 2024_02_12_2237-a39e48a59d78_add_fields_to_form_field.py │ │ ├── 2024_06_29_1931-cf938c4d09fd_role_columns.py │ │ ├── 2024_02_01_2144-79fd21991098_add_annoucment.py │ │ ├── 2024_05_17_1644-91a050e23239_make_slack_fields_optional.py │ │ ├── 2024_07_03_2110-7e5d07b95d98_add_uniq_for_role_name_and_reference.py │ │ └── 2024_12_24_2308-e4af2032001a_status_page_urls.py ├── tests │ ├── pytest.ini │ ├── schemas │ │ └── test_actions.py │ ├── repo │ │ └── test_organisation_repo.py │ ├── conftest.py │ └── routes │ │ └── test_api.py ├── Makefile ├── .coveragerc ├── data │ ├── types.yaml │ ├── roles.yaml │ ├── severities.yaml │ ├── timestamps.yaml │ ├── fields.yaml │ └── statuses.yaml ├── Dockerfile ├── .env.example ├── .env.preview ├── pyproject.toml ├── scripts │ └── vercel.py └── docker-compose.yml ├── frontend ├── .nvmrc ├── .dockerignore ├── src │ ├── vite-env.d.ts │ ├── assets │ │ ├── empty.png │ │ ├── slack_mark.png │ │ ├── mark_noborder.png │ │ └── icons │ │ │ ├── green-check.svg │ │ │ ├── traffic.svg │ │ │ ├── bolt.svg │ │ │ ├── bars.svg │ │ │ ├── paper-plane.svg │ │ │ ├── folder.svg │ │ │ ├── warning-triangle.svg │ │ │ ├── warning-triangle-red.svg │ │ │ ├── envelope.svg │ │ │ ├── user-plus.svg │ │ │ ├── email.svg │ │ │ ├── arrow-up-right.svg │ │ │ ├── home.svg │ │ │ ├── wrench.svg │ │ │ ├── trash.svg │ │ │ ├── slack.svg │ │ │ ├── puzzle.svg │ │ │ └── gear.svg │ ├── types │ │ ├── utils.ts │ │ ├── special.ts │ │ ├── action.ts │ │ └── core.ts │ ├── utils │ │ ├── date.ts │ │ ├── sort.ts │ │ ├── time.ts │ │ ├── form.ts │ │ ├── storage.ts │ │ └── tests.tsx │ ├── pages │ │ ├── Error │ │ │ └── PageNotFound.tsx │ │ ├── StatusPages │ │ │ ├── components │ │ │ │ ├── ItemRow.tsx │ │ │ │ ├── GroupForm.tsx │ │ │ │ └── ComponentForm.tsx │ │ │ ├── types.ts │ │ │ └── modals │ │ │ │ ├── CreateGroupModal.tsx │ │ │ │ ├── CreateComponentModal.tsx │ │ │ │ ├── EditGroupModal.tsx │ │ │ │ ├── EditComponentModal.tsx │ │ │ │ ├── CreateStatusPageModal.tsx │ │ │ │ └── CreateIncidentModal.tsx │ │ ├── Dashboard │ │ │ └── Dashboard.test.tsx │ │ ├── Auth │ │ │ ├── Success.tsx │ │ │ ├── SlackLogin.tsx │ │ │ ├── styles.tsx │ │ │ ├── EmailLogin.tsx │ │ │ ├── LoginSelector.tsx │ │ │ └── Register.tsx │ │ ├── Settings │ │ │ └── Index.tsx │ │ └── OAuth │ │ │ └── Complete.tsx │ ├── typing.d.ts │ ├── components │ │ ├── Error │ │ │ └── ErrorFallback.tsx │ │ ├── Empty │ │ │ ├── EmptyTable.tsx │ │ │ └── Empty.tsx │ │ ├── Loading │ │ │ └── Loading.tsx │ │ ├── Form │ │ │ ├── ErrorMessage.tsx │ │ │ ├── GeneralError.tsx │ │ │ ├── Switch.tsx │ │ │ ├── Field.tsx │ │ │ ├── MultiSelect.tsx │ │ │ ├── FieldWithSuffix.tsx │ │ │ ├── ExpandableFields.tsx │ │ │ └── Toggle.tsx │ │ ├── Debug │ │ │ └── Debug.tsx │ │ ├── Pagination │ │ │ └── getPages.ts │ │ ├── Guard │ │ │ ├── AdminGuard.tsx │ │ │ └── SlackInstallGuard.tsx │ │ ├── User │ │ │ └── MiniAvatar.tsx │ │ ├── Incident │ │ │ ├── PriorityIcon.tsx │ │ │ └── FormField.tsx │ │ ├── Table │ │ │ └── Table.module.css │ │ ├── Dialog │ │ │ └── Header.tsx │ │ ├── Button │ │ │ └── Button.tsx │ │ ├── Layout │ │ │ ├── DefaultLayout.tsx │ │ │ ├── SettingsLayout.tsx │ │ │ └── StatusPageLayout.tsx │ │ ├── Icon │ │ │ └── Icon.tsx │ │ ├── Invite │ │ │ ├── SentInvite.tsx │ │ │ └── InviteForm.tsx │ │ ├── Sections │ │ │ ├── Breadcrumbs.tsx │ │ │ ├── Header.tsx │ │ │ └── SwitchOrganisationForm.tsx │ │ ├── Timeline │ │ │ └── Timeline.tsx │ │ └── Team │ │ │ └── TeamForm.tsx │ ├── env.d.ts │ ├── App.test.tsx │ ├── main.tsx │ ├── hooks │ │ ├── useLocalStorage.ts │ │ ├── useOrganisationSwitcher.ts │ │ ├── useApi.tsx │ │ └── useOnClickOutside.tsx │ ├── mocks │ │ └── handlers.ts │ └── styles │ │ └── reset.css ├── .env.example ├── tsconfig.node.json ├── Makefile ├── Dockerfile ├── .gitignore ├── .eslintrc.cjs ├── index.html ├── vite.config.ts ├── .prettierrc ├── vitest-setup.js ├── conf │ ├── website.conf │ └── nginx.conf ├── tsconfig.json ├── public │ └── favicon.svg └── README.md ├── status-page ├── .nvmrc ├── .eslintrc.json ├── src │ ├── app │ │ ├── icon.png │ │ ├── assets │ │ │ ├── check.svg │ │ │ ├── circle-info.svg │ │ │ ├── warning-triangle-red.svg │ │ │ └── warning-triangle.svg │ │ ├── StatusPageProvider.tsx │ │ ├── incident │ │ │ └── [slug] │ │ │ │ └── page.tsx │ │ └── page.tsx │ ├── types │ │ └── enums.ts │ ├── components │ │ ├── Layout.tsx │ │ ├── NoCurrentIncident.tsx │ │ ├── ComponentStatusIcon.tsx │ │ ├── Header.tsx │ │ ├── Timeline.tsx │ │ └── Footer.tsx │ ├── lib │ │ ├── registry.tsx │ │ └── api.ts │ └── styles │ │ └── reset.css ├── public │ ├── vercel.svg │ ├── file.svg │ ├── window.svg │ ├── globe.svg │ └── next.svg ├── next.config.ts ├── Makefile ├── .gitignore ├── tsconfig.json └── package.json ├── assets ├── cover.png ├── dashboard.png └── status-page.png ├── .gitignore ├── SECURITY.md ├── .github └── workflows │ └── pr-frontend.yml ├── LICENSE └── incidental.code-workspace /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | v18.18.2 2 | -------------------------------------------------------------------------------- /backend/.envrc: -------------------------------------------------------------------------------- 1 | dotenv .env 2 | -------------------------------------------------------------------------------- /backend/.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /backend/app/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /status-page/.nvmrc: -------------------------------------------------------------------------------- 1 | v18.18.2 2 | -------------------------------------------------------------------------------- /backend/app/services/slack/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /backend/app/services/slack/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/services/slack/forms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 -------------------------------------------------------------------------------- /backend/app/services/slack/interactions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /backend/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | env = 3 | DATABASE_NAME=test-db 4 | -------------------------------------------------------------------------------- /assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incidentalhq/incidental/HEAD/assets/cover.png -------------------------------------------------------------------------------- /assets/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incidentalhq/incidental/HEAD/assets/dashboard.png -------------------------------------------------------------------------------- /assets/status-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incidentalhq/incidental/HEAD/assets/status-page.png -------------------------------------------------------------------------------- /status-page/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /backend/Makefile: -------------------------------------------------------------------------------- 1 | run-dev: 2 | docker-compose up 3 | 4 | build: 5 | docker-compose build --no-cache backend -------------------------------------------------------------------------------- /frontend/src/assets/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incidentalhq/incidental/HEAD/frontend/src/assets/empty.png -------------------------------------------------------------------------------- /status-page/src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incidentalhq/incidental/HEAD/status-page/src/app/icon.png -------------------------------------------------------------------------------- /frontend/src/assets/slack_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incidentalhq/incidental/HEAD/frontend/src/assets/slack_mark.png -------------------------------------------------------------------------------- /frontend/src/assets/mark_noborder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incidentalhq/incidental/HEAD/frontend/src/assets/mark_noborder.png -------------------------------------------------------------------------------- /frontend/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = { 2 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] 3 | } 4 | -------------------------------------------------------------------------------- /backend/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = app 4 | command_line = -m pytest 5 | 6 | [html] 7 | directory = coverage_html_report 8 | -------------------------------------------------------------------------------- /frontend/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export const parseAsUTc = (dateTime: string) => { 2 | return new Date(`${dateTime}+00:00`) // append timezone 3 | } 4 | -------------------------------------------------------------------------------- /status-page/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL=http://localhost:5000 2 | VITE_STATUS_PAGE_DOMAIN=statusbyincidental.com 3 | VITE_STATUS_PAGE_CNAME=statuspage.incidental.dev -------------------------------------------------------------------------------- /backend/data/types.yaml: -------------------------------------------------------------------------------- 1 | types: 2 | - name: Default 3 | description: "Default incident type" 4 | is_deletable: false 5 | is_editable: true 6 | is_default: true -------------------------------------------------------------------------------- /backend/app/repos/base_repo.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | 4 | class BaseRepo: 5 | def __init__(self, session: Session): 6 | self.session = session 7 | -------------------------------------------------------------------------------- /frontend/src/utils/sort.ts: -------------------------------------------------------------------------------- 1 | type ImplementsRank = { 2 | rank: number 3 | } 4 | 5 | export const rankSorter = (a: ImplementsRank, b: ImplementsRank) => { 6 | return a.rank > b.rank ? 1 : -1 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/types/special.ts: -------------------------------------------------------------------------------- 1 | import { IField, IIncidentFieldValue } from './models' 2 | 3 | export interface ICombinedFieldAndValue { 4 | field: IField 5 | value: IIncidentFieldValue | null 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/pages/Error/PageNotFound.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Root = styled.div`` 4 | 5 | const PageNotFound = () => { 6 | return Page not found 7 | } 8 | 9 | export default PageNotFound 10 | -------------------------------------------------------------------------------- /frontend/src/typing.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | import { type ArraySchema } from 'yup' 3 | 4 | declare module 'yup' { 5 | interface ArraySchema { 6 | unique(msg: string): ArraySchema 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /status-page/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | compiler: { 6 | styledComponents: true, 7 | }, 8 | }; 9 | 10 | export default nextConfig; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.py[cod] 3 | *.swp 4 | *._* 5 | *.pytest_cache 6 | .mypy_cache 7 | .ipynb_checkpoints 8 | .coverage 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Packages 14 | build 15 | eggs 16 | 17 | cache/ 18 | mnt/ 19 | .env 20 | 21 | -------------------------------------------------------------------------------- /backend/app/schemas/special.py: -------------------------------------------------------------------------------- 1 | from .base import BaseSchema 2 | from .models import FieldSchema, IncidentFieldValueSchema 3 | 4 | 5 | class CombinedFieldAndValueSchema(BaseSchema): 6 | field: FieldSchema 7 | value: IncidentFieldValueSchema | None 8 | -------------------------------------------------------------------------------- /backend/app/services/slack/renderer/__init__.py: -------------------------------------------------------------------------------- 1 | from .announcement import AnnouncementRenderer 2 | from .form import FormRenderer 3 | from .incident_info_message import IncidentInformationMessageRenderer 4 | from .incident_update import IncidentUpdateRenderer 5 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/Error/ErrorFallback.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | error: Error 3 | } 4 | export function ErrorFallback({ error }: Props) { 5 | return ( 6 |
7 |

Something went wrong:

8 |
{error.message}
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /frontend/Makefile: -------------------------------------------------------------------------------- 1 | run-dev: 2 | . ${HOME}/.nvm/nvm.sh && nvm use && pnpm run dev 3 | 4 | build: 5 | . ${HOME}/.nvm/nvm.sh && nvm use && pnpm run build 6 | 7 | build-image: build 8 | docker build . -t incidental/frontend:latest 9 | 10 | test: 11 | source ${HOME}/.nvm/nvm.sh && nvm use && pnpm run test 12 | -------------------------------------------------------------------------------- /status-page/Makefile: -------------------------------------------------------------------------------- 1 | run-dev: 2 | . ${HOME}/.nvm/nvm.sh && nvm use && pnpm run dev 3 | 4 | build: 5 | . ${HOME}/.nvm/nvm.sh && nvm use && pnpm run build 6 | 7 | build-image: build 8 | docker build . -t incidental/frontend:latest 9 | 10 | test: 11 | source ${HOME}/.nvm/nvm.sh && nvm use && pnpm run test 12 | -------------------------------------------------------------------------------- /backend/app/schemas/base.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ConfigDict 2 | from pydantic.alias_generators import to_camel 3 | 4 | 5 | class BaseSchema(BaseModel): 6 | model_config = ConfigDict( 7 | alias_generator=to_camel, 8 | from_attributes=True, 9 | populate_by_name=True, 10 | extra="forbid", 11 | ) 12 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | WORKDIR /srv 3 | 4 | # install 5 | RUN apt-get update && \ 6 | apt-get install -y \ 7 | curl \ 8 | nginx 9 | 10 | COPY . . 11 | COPY conf/website.conf /etc/nginx/conf.d/default.conf 12 | COPY conf/nginx.conf /etc/nginx/nginx.conf 13 | 14 | EXPOSE 3000 15 | ENTRYPOINT ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /frontend/src/components/Empty/EmptyTable.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react' 2 | import styled from 'styled-components' 3 | 4 | const Root = styled.div` 5 | margin: 0 auto; 6 | padding: 1rem; 7 | ` 8 | 9 | const EmptyTable: React.FC = ({ children }) => { 10 | return {children} 11 | } 12 | 13 | export default EmptyTable 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // https://vitejs.dev/guide/env-and-mode.html#intellisense-for-typescript 4 | interface ImportMetaEnv { 5 | readonly VITE_API_BASE_URL: string 6 | readonly VITE_STATUS_PAGE_DOMAIN: string 7 | readonly VITE_STATUS_PAGE_CNAME: string 8 | } 9 | 10 | interface ImportMeta { 11 | readonly env: ImportMetaEnv 12 | } 13 | -------------------------------------------------------------------------------- /status-page/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /status-page/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import { render, screen } from '@testing-library/react' 3 | 4 | import App from './App' 5 | 6 | describe('', async () => { 7 | it('Should show login', async () => { 8 | render() 9 | 10 | const loginSelector = await screen.findByTestId('login-selector') 11 | expect(loginSelector).toBeInTheDocument() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /backend/app/services/slack/commands/errors.py: -------------------------------------------------------------------------------- 1 | from app.schemas.slack import SlackCommandDataSchema 2 | 3 | 4 | class CommandError(Exception): 5 | pass 6 | 7 | 8 | class InvalidUsageError(CommandError): 9 | def __init__(self, message: str, command: SlackCommandDataSchema, *args: object) -> None: 10 | super().__init__(*args) 11 | 12 | self.message = message 13 | self.command = command 14 | -------------------------------------------------------------------------------- /frontend/src/components/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import spinner from '@/assets/icons/spinner.svg' 4 | import Icon from '@/components/Icon/Icon' 5 | 6 | interface Props { 7 | text?: string 8 | } 9 | 10 | const Loading: React.FC = ({ text = 'Loading...' }) => ( 11 |
12 | {text} 13 |
14 | ) 15 | 16 | export default Loading 17 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import 'react-toastify/dist/ReactToastify.css' 4 | 5 | import App from './App.tsx' 6 | import './styles/index.css' 7 | import './styles/reset.css' 8 | import './styles/variables.css' 9 | 10 | createRoot(document.getElementById('root')!).render( 11 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended'], 5 | ignorePatterns: ['dist', '.eslintrc.cjs'], 6 | parser: '@typescript-eslint/parser', 7 | plugins: ['react-refresh'], 8 | rules: { 9 | 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /status-page/src/app/assets/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | At Incidental, we prioritize the security of our open source project. If you discover a vulnerability, please report it by emailing hello@incidental.dev with a detailed description and steps to reproduce. We kindly request that you avoid public disclosure until we've had a chance to address the issue. 4 | 5 | We aim to acknowledge reports within 48 hours and provide updates weekly. 6 | 7 | Thank you for helping keep Incidental secure. 8 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Incidental - Opensource incident management 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/green-check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc' 2 | import path from 'path' 3 | import { defineConfig } from 'vitest/config' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | '@': path.resolve(__dirname, './src') 11 | } 12 | }, 13 | test: { 14 | globals: true, 15 | setupFiles: ['./vitest-setup.js'], 16 | environment: 'jsdom' 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /backend/app/tasks/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Generic, TypeVar 3 | 4 | from pydantic import BaseModel 5 | from sqlalchemy.orm import Session 6 | 7 | Parameters = TypeVar("Parameters", bound=BaseModel) 8 | 9 | 10 | class BaseTask(ABC, Generic[Parameters]): 11 | def __init__(self, session: Session): 12 | self.session = session 13 | 14 | @abstractmethod 15 | def execute(self, parameters: Parameters): 16 | pass 17 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/traffic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/bolt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/Form/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import { useField } from 'formik' 2 | 3 | interface Props { 4 | name: string 5 | } 6 | 7 | const ErrorMessage: React.FC = (props) => { 8 | const [, meta] = useField(props) 9 | 10 | const showError = meta.touched && meta.error && typeof meta.error === 'string' 11 | 12 | if (showError) { 13 | return
{meta.error}
14 | } 15 | 16 | return null 17 | } 18 | 19 | export default ErrorMessage 20 | -------------------------------------------------------------------------------- /frontend/src/pages/StatusPages/components/ItemRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { FlattenedItem } from '../types' 5 | 6 | type RootProps = { 7 | $depth: number 8 | } 9 | const Root = styled.div`` 10 | 11 | interface Props { 12 | item: FlattenedItem 13 | } 14 | 15 | const ItemRow: React.FC = ({ item }) => { 16 | return {item.data?.name} 17 | } 18 | 19 | export default ItemRow 20 | -------------------------------------------------------------------------------- /frontend/src/components/Form/GeneralError.tsx: -------------------------------------------------------------------------------- 1 | import { useField } from 'formik' 2 | import styled from 'styled-components' 3 | 4 | const Error = styled.div` 5 | background-color: #dc3545; 6 | color: #fff; 7 | padding: 0.5rem; 8 | margin-bottom: 1rem; 9 | border-radius: 0.4rem; 10 | ` 11 | 12 | const GeneralError = () => { 13 | const [, meta] = useField('general') 14 | return <>{meta.error && typeof meta.error === 'string' ? {meta.error} : null} 15 | } 16 | 17 | export default GeneralError 18 | -------------------------------------------------------------------------------- /frontend/src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns' 2 | import { toZonedTime } from 'date-fns-tz' 3 | 4 | export const getLocalTimeZone = (): string => { 5 | return Intl.DateTimeFormat().resolvedOptions().timeZone 6 | } 7 | 8 | export const utcToLocal = (date: string) => { 9 | return toZonedTime(date, getLocalTimeZone()) 10 | } 11 | 12 | export const formatForDateTimeInput = (isoDateTime: string) => { 13 | const localDate = utcToLocal(isoDateTime) 14 | return format(localDate, "yyyy-MM-dd'T'HH:mm") 15 | } 16 | -------------------------------------------------------------------------------- /backend/data/roles.yaml: -------------------------------------------------------------------------------- 1 | roles: 2 | - name: Reporter 3 | kind: REPORTER 4 | description: "The user responsible for first reporting the incident" 5 | slack_reference: reporter 6 | is_deletable: false 7 | is_editable: true 8 | - name: Incident Lead 9 | kind: LEAD 10 | description: "An incident lead coordinates and directs the response to an emergency or crisis, guiding teams to efficiently resolve and mitigate the situation" 11 | slack_reference: lead 12 | is_deletable: false 13 | is_editable: true -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard/Dashboard.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from '@testing-library/react' 2 | 3 | import { withAllTheProviders } from '@/utils/tests' 4 | 5 | import Dashboard from './Dashboard' 6 | 7 | describe('', async () => { 8 | it('should render', async () => { 9 | render(withAllTheProviders()) 10 | 11 | await waitFor(async () => { 12 | const rows = await screen.findAllByTestId('incident-row') 13 | expect(rows.length).toBe(1) 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /status-page/src/app/assets/circle-info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/Debug/Debug.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | import useGlobal from '@/hooks/useGlobal' 4 | 5 | const Root = styled.div` 6 | position: absolute; 7 | left: 0; 8 | bottom: 0; 9 | padding: 1rem; 10 | background-color: var(--color-red-100); 11 | ` 12 | 13 | const Debug = () => { 14 | const { organisation } = useGlobal() 15 | 16 | if (import.meta.env.MODE !== 'development') { 17 | return null 18 | } 19 | 20 | return {organisation?.id} 21 | } 22 | 23 | export default Debug 24 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "endOfLine": "auto", 8 | "importOrderSeparation": true, 9 | "importOrder": [ 10 | "", 11 | "@/(.*)$", 12 | "components/(.*)$", 13 | "containers/(.*)$", 14 | "services/(.*)$", 15 | "^[./](.*)$" 16 | ], 17 | "importOrderCaseInsensitive": true, 18 | "importOrderSortSpecifiers": true, 19 | "plugins": ["@trivago/prettier-plugin-sort-imports"] 20 | } 21 | -------------------------------------------------------------------------------- /status-page/src/types/enums.ts: -------------------------------------------------------------------------------- 1 | export enum StatusPageKind { 2 | PUBLIC = "PUBLIC", 3 | CUSTOMER = "CUSTOMER", 4 | INTERNAL = "INTERNAL", 5 | } 6 | 7 | export enum ComponentStatus { 8 | OPERATIONAL = "OPERATIONAL", 9 | DEGRADED_PERFORMANCE = "DEGRADED_PERFORMANCE", 10 | PARTIAL_OUTAGE = "PARTIAL_OUTAGE", 11 | FULL_OUTAGE = "FULL_OUTAGE", 12 | } 13 | 14 | export enum StatusPageIncidentStatus { 15 | INVESTIGATING = "INVESTIGATING", 16 | IDENTIFIED = "IDENTIFIED", 17 | MONITORING = "MONITORING", 18 | RESOLVED = "RESOLVED", 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/bars.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/pages/Auth/Success.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | import { Box, Content } from '@/components/Theme/Styles' 4 | 5 | const Root = styled.div` 6 | width: 30rem; 7 | margin: 10rem auto; 8 | ` 9 | 10 | const RegisterSuccess: React.FC = () => { 11 | return ( 12 | 13 | 14 | 15 |

Registration was successful, please check your email to verify your account

16 |
17 |
18 |
19 | ) 20 | } 21 | 22 | export default RegisterSuccess 23 | -------------------------------------------------------------------------------- /backend/app/routes/health.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from sqlalchemy import select, text 3 | 4 | from app.deps import DatabaseSession 5 | 6 | router = APIRouter(tags=["Health"]) 7 | 8 | 9 | @router.get("") 10 | def health_index(request: Request, db: DatabaseSession): 11 | results = db.execute(text("select 1")).scalar() 12 | 13 | return { 14 | "health": "good", 15 | "agent": request.headers.get("user-agent"), 16 | "ip": request.client.host if request.client else None, 17 | "db": results == 1, 18 | } 19 | -------------------------------------------------------------------------------- /backend/tests/schemas/test_actions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import ValidationError 3 | 4 | from app.schemas.actions import PatchIncidentTimestampsSchema 5 | 6 | 7 | def test_update_incident_timezone_schema_invalid(): 8 | with pytest.raises(ValidationError, match="Invalid timezone"): 9 | PatchIncidentTimestampsSchema(timezone="xxx", values={}) 10 | 11 | 12 | def test_update_incident_timezone_schema_valid_timezone(): 13 | schema = PatchIncidentTimestampsSchema(timezone="Europe/London", values={}) 14 | assert schema.timezone is not None 15 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/paper-plane.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/warning-triangle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/vitest-setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest' 2 | import { setupServer } from 'msw/node' 3 | import { afterAll, afterEach, beforeAll } from 'vitest' 4 | 5 | import { handlers } from './src/mocks/handlers' 6 | 7 | const server = setupServer(...handlers) 8 | 9 | // Start server before all tests 10 | beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) 11 | 12 | // Close server after all tests 13 | afterAll(() => server.close()) 14 | 15 | // Reset handlers after each test `important for test isolation` 16 | afterEach(() => server.resetHandlers()) 17 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/warning-triangle-red.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /status-page/src/app/assets/warning-triangle-red.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /status-page/src/app/assets/warning-triangle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/envelope.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const parseJSON = (data: string | null) => { 4 | if (!data) { 5 | return null 6 | } else { 7 | return JSON.parse(data) 8 | } 9 | } 10 | 11 | export const useLocalStorage = (storageKey: string, fallbackState: T) => { 12 | const [value, setValue] = useState(parseJSON(localStorage.getItem(storageKey)) ?? fallbackState) 13 | 14 | useEffect(() => { 15 | localStorage.setItem(storageKey, JSON.stringify(value)) 16 | }, [value, storageKey]) 17 | 18 | return [value, setValue] 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/user-plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/app/repos/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | from .announcement_repo import AnnouncementRepo 3 | from .field_repo import FieldRepo 4 | from .form_repo import FormRepo 5 | from .incident_repo import IncidentRepo 6 | from .invite_repo import InviteRepo 7 | from .lifecycle_repo import LifecycleRepo 8 | from .organisation_repo import OrganisationRepo 9 | from .severity_repo import SeverityRepo 10 | from .slack_bookmark import SlackBookmarkRepo 11 | from .slack_message import SlackMessageRepo 12 | from .status_page_repo import StatusPageRepo 13 | from .timestamp_repo import TimestampRepo 14 | from .user_repo import UserRepo 15 | -------------------------------------------------------------------------------- /frontend/src/types/action.ts: -------------------------------------------------------------------------------- 1 | import { ComponentStatus } from './enums' 2 | 3 | export interface CreateStatusPageIncident { 4 | name: string 5 | message: string 6 | status: string 7 | affectedComponents: Record 8 | } 9 | 10 | export interface PaginationParams { 11 | page: number 12 | size: number 13 | } 14 | 15 | export interface GetStatusPageIncidentsRequest { 16 | id: string 17 | pagination?: PaginationParams 18 | isActive?: boolean 19 | } 20 | 21 | export interface CreateStatusPageIncidentUpdate { 22 | message: string 23 | status: string 24 | affectedComponents: Record 25 | } 26 | -------------------------------------------------------------------------------- /status-page/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /backend/tests/repo/test_organisation_repo.py: -------------------------------------------------------------------------------- 1 | from faker import Faker 2 | from sqlalchemy.orm import Session 3 | 4 | from app.models import Organisation 5 | from app.repos import OrganisationRepo 6 | 7 | 8 | def test_organisation_repo_create_slug(db: Session, faker: Faker): 9 | organisation_repo = OrganisationRepo(session=db) 10 | name = faker.name() 11 | orgs: list[Organisation] = [] 12 | n = 10 13 | 14 | for _ in range(n): 15 | orgs.append( 16 | organisation_repo.create_organisation( 17 | name=name, 18 | ) 19 | ) 20 | 21 | slugs = set([org.slug for org in orgs]) 22 | assert len(slugs) == n 23 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination/getPages.ts: -------------------------------------------------------------------------------- 1 | export const getPages = ( 2 | currentPage: number, 3 | totalPages: number, 4 | leftEdge = 2, 5 | leftCurrent = 2, 6 | rightCurrent = 2, 7 | rightEdge = 2 8 | ) => { 9 | let last = 0 10 | const ret = [] 11 | for (let i = 1; i < totalPages + 1; i++) { 12 | if ( 13 | i <= leftEdge || 14 | (i > currentPage - leftCurrent - 1 && i < currentPage + rightCurrent) || 15 | i > totalPages - rightEdge 16 | ) { 17 | if (last + 1 !== i) { 18 | ret.push(null) 19 | } 20 | ret.push(i) 21 | last = i 22 | } 23 | } 24 | return ret 25 | } 26 | 27 | export default getPages 28 | -------------------------------------------------------------------------------- /frontend/src/utils/form.ts: -------------------------------------------------------------------------------- 1 | import set from 'lodash/set' 2 | 3 | import { APIError } from '@/services/transport' 4 | 5 | export const apiErrorsToFormikErrors = (e: APIError) => { 6 | const formErrors: Record = {} 7 | 8 | if (!e.errors) { 9 | formErrors['general'] = e.detail 10 | return formErrors 11 | } 12 | 13 | // A more detailed validation error from pydantic 14 | for (const field of e.errors) { 15 | let location = [] 16 | if (field.loc[0] == 'body') { 17 | location = field.loc.slice(1) 18 | } else { 19 | location = field.loc 20 | } 21 | set(formErrors, location, field.msg) 22 | } 23 | return formErrors 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/arrow-up-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/wrench.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /status-page/src/app/StatusPageProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { createContext, ReactNode } from "react"; 3 | import { IStatusPage } from "@/types/models"; 4 | 5 | type StatusPageContextType = { 6 | statusPage: IStatusPage; 7 | }; 8 | 9 | export const StatusPageContext = createContext( 10 | null 11 | ); 12 | 13 | const StatusPageProvider = ({ 14 | children, 15 | statusPage, 16 | }: { 17 | children: ReactNode; 18 | statusPage: IStatusPage; 19 | }) => { 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | export default StatusPageProvider; 28 | -------------------------------------------------------------------------------- /frontend/src/pages/Settings/Index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | import { Box, Content, ContentMain, Header, Title } from '@/components/Theme/Styles' 4 | 5 | const Intro = styled.div` 6 | padding: 1rem; 7 | ` 8 | 9 | const SettingsIndex = () => { 10 | return ( 11 | <> 12 | 13 |
14 | Overview 15 |
16 | 17 | 18 | 19 |

Customise your workspace

20 |
21 |
22 |
23 |
24 | 25 | ) 26 | } 27 | 28 | export default SettingsIndex 29 | -------------------------------------------------------------------------------- /status-page/src/app/incident/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import IncidentPage from "@/components/IncidentPage"; 2 | import { getIncident } from "@/lib/api"; 3 | 4 | interface Props { 5 | params: Promise<{ slug: string }>; 6 | } 7 | 8 | export async function generateMetadata({ params }: Props) { 9 | const incident = await getIncident((await params).slug); 10 | return { 11 | title: `Incident - ${incident.name}`, 12 | }; 13 | } 14 | 15 | export default async function IncidentPageRoute(props: { 16 | params: Promise<{ slug: string }>; 17 | }) { 18 | const slug = (await props.params).slug; 19 | const incident = await getIncident(slug); 20 | 21 | return ; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/components/Guard/AdminGuard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import useAuth from '@/hooks/useAuth' 4 | 5 | interface Props { 6 | children: React.ReactElement 7 | } 8 | 9 | const AdminGuard: React.FC = ({ children }) => { 10 | const [redirect, setRedirect] = useState(false) 11 | const { user } = useAuth() 12 | 13 | useEffect(() => { 14 | if (!user) { 15 | setRedirect(true) 16 | return 17 | } 18 | if (!user.isSuperAdmin) { 19 | setRedirect(true) 20 | return 21 | } 22 | }, [user]) 23 | 24 | if (redirect) { 25 | return <>Not authorized 26 | } 27 | 28 | return children 29 | } 30 | 31 | export default AdminGuard 32 | -------------------------------------------------------------------------------- /status-page/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Footer from "@/components/Footer"; 4 | import styled from "styled-components"; 5 | import Header from "./Header"; 6 | 7 | const Root = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | margin-left: auto; 11 | margin-right: auto; 12 | min-height: 100vh; 13 | max-width: 700px; 14 | `; 15 | const Main = styled.div` 16 | flex: 1 1 0%; 17 | `; 18 | 19 | export default function Layout({ 20 | children, // will be a page or nested layout 21 | }: { 22 | children: React.ReactNode; 23 | }) { 24 | return ( 25 | 26 |
27 |
{children}
28 |