feat: Initialize Strata KitchenSink application boilerplate

This sets up the foundational structure for a .NET Core 6 / React project,
demonstrating common architectural patterns, Docker integration, and key
Strata libraries for API, data access (EF Core), background jobs (Hangfire),
real-time communication (SignalR), and various frontend UI components.
This commit is contained in:
Thom Lamb
2026-06-23 10:58:15 -05:00
parent 1d2c6543dd
commit 0979371a57
125 changed files with 25811 additions and 218 deletions
+10
View File
@@ -0,0 +1,10 @@
.dockerignore
Dockerfile
Dockerfile.local
.env
.gitignore
.vs
.vscode
**/bin
**/obj
**/.toolstarget
+85 -217
View File
@@ -1,12 +1,67 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
###################
# compiled source #
###################
*.com
*.class
*.dll
*.exe
*.pdb
*.dll.config
*.cache
*.suo
# Include dlls if theyre in the NuGet packages directory
!/packages/*/lib/*.dll
# Include dlls if they're in the CommonReferences directory
!*CommonReferences/*.dll
####################
# VS Upgrade stuff #
####################
_UpgradeReport_Files/
###############
# Directories #
###############
bin/
obj/
TestResults/
###################
# Web publish log #
###################
*.Publish.xml
#############
# Resharper #
#############
/_ReSharper.*
*.ReSharper.*
############
# Packages #
############
# its better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
######################
# Logs and databases #
######################
*.log
*.sqlite
# OS generated files #
######################
.DS_Store?
ehthumbs.db
Icon?
Thumbs.db
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
@@ -14,9 +69,6 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
@@ -24,76 +76,47 @@ mono_crash.*
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
build/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
# Visual Studo 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
# NUNIT
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
@@ -108,21 +131,14 @@ _Chutzpah*
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
@@ -135,29 +151,18 @@ _ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
@@ -181,76 +186,50 @@ DocProject/Help/html
# Click-Once directory
publish/
ClickOnce/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
**/packages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
#!**/packages/repositories.config
# Microsoft Azure Build Output
# Windows Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
# Windows Store app package directory
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
!*.[Cc]ache/
# Others
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
bower_components/
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
@@ -261,32 +240,21 @@ _UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
**/node_modules/*
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
@@ -294,109 +262,9 @@ node_modules/
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
# SonarQube files
sonarqube/
.vs/
*.orig
buildlogs/
+106
View File
@@ -0,0 +1,106 @@
ARG PROJECT=Strata.KitchenSink
ARG VERSION=0.0.0
ARG SONARLOGIN=''
##############
# Base image #
##############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.aspnet:6.0 AS base
RUN apt-get update --allow-releaseinfo-change \
&& apt-get install -y \
libc6-dev \
libgdiplus \
libx11-dev \
&& rm -rf /var/lib/apt/lists/*
###############
# Build image #
###############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.sdk:6.0 AS build
ARG PROJECT
ARG VERSION
ARG SONARLOGIN
RUN apt-get update --allow-releaseinfo-change \
&& apt-get install -y \
libc6-dev \
libgdiplus \
libx11-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
# Copy project files and restore
COPY ["${PROJECT}.sln", "${PROJECT}.sln"]
COPY src/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
COPY tests/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p tests/${file%.*}/ && mv $file tests/${file%.*}/; done
RUN dotnet restore "${PROJECT}.sln" \
--source https://api.nuget.org/v3/index.json \
--source https://proget.sdt.local/nuget/nuget/v3/index.json
# Copy files and build
COPY . .
RUN if [ "${SONARLOGIN}" != "" ] ; then dotnet sonarscanner begin \
/k:"${PROJECT}" \
/d:sonar.scm.provider=git \
/d:sonar.host.url="https://sonarqube.sdt.local" \
/d:sonar.login="${SONARLOGIN}" \
/d:sonar.cs.opencover.reportsPaths="/src/cover.xml" \
/d:sonar.dependencyCheck.jsonReportPath=/src/buildlogs/dependency-check-report.json \
/d:sonar.dependencyCheck.htmlReportPath=/src/buildlogs/dependency-check-report.html \
/v:sonar.projectVersion="${VERSION}"; fi
RUN dotnet build "${PROJECT}.sln" \
--configuration Release \
--no-restore
# Run unit tests
RUN dotnet test "${PROJECT}.sln" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
-p:CollectCoverage=true \
-p:CoverletOutputFormat="opencover" \
-p:CoverletOutput=/src/cover.xml
# Publish project
RUN dotnet publish "src/${PROJECT}.Api/${PROJECT}.Api.csproj" \
--configuration Release \
--no-restore \
--no-build \
--output /app
# Pack client project
RUN dotnet pack "src/${PROJECT}.Client/${PROJECT}.Client.csproj" \
--configuration Release \
--no-restore \
--no-build \
--include-symbols \
--include-source \
--output /pack \
-property:PackageVersion=${VERSION}
RUN if [ "${SONARLOGIN}" != "" ] ; then dotnet sonarscanner end /d:sonar.login="${SONARLOGIN}"; fi
###############
# Final image #
###############
FROM base AS final
ARG PROJECT
WORKDIR /app
ARG PROJECT
ENV PROJECTDLL=${PROJECT}.Api.dll
ENV DOTNET_ENVIRONMENT=Development
ENV ASPNETCORE_ENVIRONMENT=Development
COPY --from=build /app /app
COPY --from=build /pack /pack
ENTRYPOINT dotnet ${PROJECTDLL}
+55
View File
@@ -0,0 +1,55 @@
ARG PROJECT=Strata.KitchenSink
ARG VERSION=0.0.0
##############
# Base image #
##############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.aspnet:6.0 AS base
###############
# Build image #
###############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.sdk:6.0 AS build
ARG PROJECT
ARG VERSION
WORKDIR /src
# Copy project files and restore
COPY ["${PROJECT}.sln", "${PROJECT}.sln"]
COPY src/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
COPY tests/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p tests/${file%.*}/ && mv $file tests/${file%.*}/; done
RUN dotnet restore "${PROJECT}.sln" \
--source https://api.nuget.org/v3/index.json \
--source https://proget.sdt.local/nuget/nuget/v3/index.json
# Copy files and build
COPY . .
RUN dotnet build "${PROJECT}.sln" \
--configuration Release \
--no-restore
# Publish project
RUN dotnet publish "src/${PROJECT}.Hangfire.Dashboard/${PROJECT}.Hangfire.Dashboard.csproj" \
--configuration Release \
--no-restore \
--no-build \
--output /app
###############
# Final image #
###############
FROM base AS final
ARG PROJECT
WORKDIR /app
ARG PROJECT
ENV PROJECTDLL=${PROJECT}.Hangfire.Dashboard.dll
ENV DOTNET_ENVIRONMENT=Development
ENV ASPNETCORE_ENVIRONMENT=Development
COPY --from=build /app /app
ENTRYPOINT dotnet ${PROJECTDLL}
+66
View File
@@ -0,0 +1,66 @@
ARG PROJECT=Strata.KitchenSink
ARG VERSION=0.0.0
##############
# Base image #
##############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.aspnet:6.0 AS base
###############
# Build image #
###############
FROM ecr.ops.stratanetwork.net/strata.microsoft.dotnet.sdk:6.0 AS build
ARG PROJECT
ARG VERSION
WORKDIR /src
# Copy project files and restore
COPY ["${PROJECT}.sln", "${PROJECT}.sln"]
COPY src/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p src/${file%.*}/ && mv $file src/${file%.*}/; done
COPY tests/**/*.csproj ./
RUN for file in $(ls *.csproj); do mkdir -p tests/${file%.*}/ && mv $file tests/${file%.*}/; done
RUN dotnet restore "${PROJECT}.sln" \
--source https://api.nuget.org/v3/index.json \
--source https://proget.sdt.local/nuget/nuget/v3/index.json
# Copy files and build
COPY . .
RUN dotnet build "${PROJECT}.sln" \
--configuration Release \
--no-restore
# Run unit tests
RUN dotnet test "${PROJECT}.sln" \
--configuration Release \
--no-restore \
--no-build \
--verbosity=normal \
-p:CollectCoverage=true \
-p:CoverletOutputFormat="opencover" \
-p:CoverletOutput=/src/cover.xml
# Publish project
RUN dotnet publish "src/${PROJECT}.Hangfire/${PROJECT}.Hangfire.csproj" \
--configuration Release \
--no-restore \
--no-build \
--output /app
###############
# Final image #
###############
FROM base AS final
ARG PROJECT
WORKDIR /app
ARG PROJECT
ENV PROJECTDLL=${PROJECT}.Hangfire.dll
ENV DOTNET_ENVIRONMENT=Development
ENV ASPNETCORE_ENVIRONMENT=Development
COPY --from=build /app /app
ENTRYPOINT dotnet ${PROJECTDLL}
+18
View File
@@ -0,0 +1,18 @@
FROM node:12.13.1-stretch
# Set working directory
WORKDIR /app
# Install dependencies
RUN npm config set @strata:registry http://proget.sdt.local/npm/npm
COPY src/Strata.KitchenSink.Web/kitchensink/package*.json ./
RUN npm install
# Copy in rest of app and build
COPY src/Strata.KitchenSink.Web/kitchensink .
RUN npm run build
# Build release container
FROM nginx:alpine
COPY --from=0 /app/build /usr/share/nginx/html
COPY src/Strata.KitchenSink.Web/nginx.conf /etc/nginx/conf.d/default.conf
+10 -1
View File
@@ -1,2 +1,11 @@
# kitchensink
# Strata.KitchenSink
## Project Setup
This project uses AWS resources. You will need the following profile in your AWS .config file. And make sure you update your .credentials file daily.
```
[profile kitchensink]
source_profile = 809820578507_SDT-Dev-Developers
role_arn = arn:aws:iam::809820578507:role/sdt-kitchensink-service-role
region=us-east-1
```
+55
View File
@@ -0,0 +1,55 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29613.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.KitchenSink.Api", "src\Strata.KitchenSink.Api\Strata.KitchenSink.Api.csproj", "{48E937D6-4EBD-4E29-823C-F3BE842FBF09}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.KitchenSink.Test.Unit", "tests\Strata.KitchenSink.Test.Unit\Strata.KitchenSink.Test.Unit.csproj", "{1DB15D23-5AE8-45CE-9F55-81DBC800AFA5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.KitchenSink.Client", "src\Strata.KitchenSink.Client\Strata.KitchenSink.Client.csproj", "{05C061D5-1389-4CC1-A157-591B718B2513}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.KitchenSink.Biz", "src\Strata.KitchenSink.Biz\Strata.KitchenSink.Biz.csproj", "{EB510EA1-0486-4678-B1C4-5EFC16D326BC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strata.KitchenSink.Hangfire", "src\Strata.KitchenSink.Hangfire\Strata.KitchenSink.Hangfire.csproj", "{B9A0136C-F8F1-4CA4-9696-C1661E251A2D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strata.KitchenSink.Hangfire.Dashboard", "src\Strata.KitchenSink.Hangfire.Dashboard\Strata.KitchenSink.Hangfire.Dashboard.csproj", "{698D3CA3-77FD-4CE3-9601-CE68CA623F25}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{48E937D6-4EBD-4E29-823C-F3BE842FBF09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{48E937D6-4EBD-4E29-823C-F3BE842FBF09}.Debug|Any CPU.Build.0 = Debug|Any CPU
{48E937D6-4EBD-4E29-823C-F3BE842FBF09}.Release|Any CPU.ActiveCfg = Release|Any CPU
{48E937D6-4EBD-4E29-823C-F3BE842FBF09}.Release|Any CPU.Build.0 = Release|Any CPU
{1DB15D23-5AE8-45CE-9F55-81DBC800AFA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1DB15D23-5AE8-45CE-9F55-81DBC800AFA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1DB15D23-5AE8-45CE-9F55-81DBC800AFA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1DB15D23-5AE8-45CE-9F55-81DBC800AFA5}.Release|Any CPU.Build.0 = Release|Any CPU
{05C061D5-1389-4CC1-A157-591B718B2513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05C061D5-1389-4CC1-A157-591B718B2513}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05C061D5-1389-4CC1-A157-591B718B2513}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05C061D5-1389-4CC1-A157-591B718B2513}.Release|Any CPU.Build.0 = Release|Any CPU
{EB510EA1-0486-4678-B1C4-5EFC16D326BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB510EA1-0486-4678-B1C4-5EFC16D326BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB510EA1-0486-4678-B1C4-5EFC16D326BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB510EA1-0486-4678-B1C4-5EFC16D326BC}.Release|Any CPU.Build.0 = Release|Any CPU
{B9A0136C-F8F1-4CA4-9696-C1661E251A2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9A0136C-F8F1-4CA4-9696-C1661E251A2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9A0136C-F8F1-4CA4-9696-C1661E251A2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9A0136C-F8F1-4CA4-9696-C1661E251A2D}.Release|Any CPU.Build.0 = Release|Any CPU
{698D3CA3-77FD-4CE3-9601-CE68CA623F25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{698D3CA3-77FD-4CE3-9601-CE68CA623F25}.Debug|Any CPU.Build.0 = Debug|Any CPU
{698D3CA3-77FD-4CE3-9601-CE68CA623F25}.Release|Any CPU.ActiveCfg = Release|Any CPU
{698D3CA3-77FD-4CE3-9601-CE68CA623F25}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AAB94C99-1F37-4893-8029-A66BC5A5098E}
EndGlobalSection
EndGlobal
+48
View File
@@ -0,0 +1,48 @@
version: "3"
services:
kitchensink-api:
build:
context: .
dockerfile: Dockerfile.api
ports:
- "8080:80"
- "8443:443"
environment:
- hangfire__redis__prefix=kitchensink
- hangfire__redis__configOptions=redis:6379
kitchensink-hangfire:
build:
context: .
dockerfile: Dockerfile.hangfire
environment:
- hangfire__redis__prefix=kitchensink
- hangfire__redis__configOptions=redis:6379
kitchensink-hangfire-dashboard:
build:
context: .
dockerfile: Dockerfile.dashboard
ports:
- 8082:80
environment:
- hangfire__redis__prefix=kitchensink
- hangfire__redis__configOptions=redis:6379
redis:
image: redis:alpine
ports:
- 6379:6379
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
redis-commander:
image: rediscommander/redis-commander:latest
environment:
- REDIS_HOSTS=local:redis:6379
- HTTP_USER=root
- HTTP_PASSWORD=pass
ports:
- 8081:8081
depends_on:
- redis
volumes:
redis-data:
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
<add key="defaultPushSource" value="https://proget.ops.stratanetwork.net/nuget/nuget" />
</config>
<packageSources>
<add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
<add key="Strata package source" value="https://proget.ops.stratanetwork.net/nuget/nuget" />
</packageSources>
</configuration>
@@ -0,0 +1,89 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.Excel.Core;
using Strata.KitchenSink.Api.Models;
using Strata.KitchenSink.Biz.Departments;
using Strata.SqlTools.EntityFramework.Pagination;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.KitchenSink.Api.Controllers
{
// Controller should be pluralize. /departments instead of /department
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class DepartmentsController : ControllerBase
{
private readonly IDepartmentRepository _departmentRepository;
public DepartmentsController(IDepartmentRepository departmentRepository)
{
_departmentRepository = departmentRepository;
}
// Return the actual object when the endpoint can only return 200.
// Paging and filtering and searching options should be part of the query string.
// In the case where the filtering can exceed url max length,
// the endpoint has to become a HttpPost with the filtering specified in the body but paging and search should still be on the query string
// In the case where you don't need paging, just return the entire list instead of PagedData
[HttpGet("")]
public async Task<PagedData<Department>> GetAll([FromQuery] DepartmentsLoadOptions options, CancellationToken cancellationToken)
{
return await _departmentRepository.GetAllAsync(options, options.Search, options.DepartmentType, cancellationToken);
}
// Return ActionResult when the endpoint can return multiple http status code. This endpoint can return 200 or 404.
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Department>> GetById(int id, CancellationToken cancellationToken)
{
var department = await _departmentRepository.GetByIdAsync(id, cancellationToken);
if (department == null)
{
return NotFound();
}
return Ok(department);
}
// Entity Framework automatically detects where to do insert or update based on the primary key of the Entity.
// Id = 0 means insert
[HttpPost("")]
public async Task<ActionResult<Department>> Save(Department department, CancellationToken cancellationToken)
{
return await _departmentRepository.SaveAsync(department, cancellationToken);
}
// Delete should return 404 if id doesn't exist. Use Contains instead of loading entire object into memory to check for existence.
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
{
if (!await _departmentRepository.ContainsAsync(id, cancellationToken))
{
return NotFound();
}
await _departmentRepository.DeleteAsync(id, cancellationToken);
return NoContent();
}
// [action] means to use the method name as the route name. In this case, route becomes /departments/export
[HttpGet("[action]")]
public async Task<ExcelContentResult> Export([FromQuery] bool withCoverPage, CancellationToken cancellationToken)
{
return new ExcelContentResult(await _departmentRepository.ExportAsync(withCoverPage, cancellationToken));
}
[HttpPost("[action]")]
public async Task<DepartmentImportResult> ImportAsync(IFormFile file, CancellationToken cancellationToken)
{
return await _departmentRepository.ImportAsync(file, cancellationToken);
}
}
}
@@ -0,0 +1,73 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
namespace Strata.KitchenSink.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ErrorsController : ControllerBase
{
public ErrorsController()
{
}
[HttpGet("404")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult Throw404()
{
return NotFound();
}
[HttpGet("403")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public IActionResult Throw403()
{
return Forbid();
}
[HttpGet("500")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Throw500()
{
// Known exceptions are when you specifically catch an exception from your biz code and return a friendly message to the client.
// This message will be shown in both dev and prod environments
try
{
new FakeService().DoSomethingBad();
return Ok();
}
catch // your code should catch specific exception types if possible
{
// make sure you are sending back a 4xx / 5xx response for errors so in DataDog, we can easily identify all calls that resulted in an error
return StatusCode(500, new { message = "Custom error message from server" });
}
}
[HttpGet("unhandled")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult ThrowUnhandled()
{
// Unhandled exception is usually something thrown in your Biz code and it gets bubbled up to the client.
// In Dev environment, the message and stack trace will be sent to the client
// In Prod environment, a generic "Error has occurred" message will be sent to the client
new FakeService().DoSomethingBad();
return Ok();
}
public class FakeService
{
public void DoSomethingBad()
{
throw new Exception("Custom error message from server that only appears in Dev, not Prod environment");
}
}
}
}
@@ -0,0 +1,29 @@
using Hangfire;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Strata.KitchenSink.Biz;
namespace Strata.KitchenSink.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class JobsController : ControllerBase
{
private readonly IBackgroundJobClient _backgroundJobClient;
public JobsController(IBackgroundJobClient backgroundJobClient)
{
_backgroundJobClient = backgroundJobClient;
}
[HttpPost("")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public IActionResult AddJob()
{
var jobId = _backgroundJobClient.Enqueue<KitchenSinkJob>(job => job.RunKitchenSinkJob());
return Accepted(new {jobId});
}
}
}
@@ -0,0 +1,101 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Strata.KitchenSink.Biz.Hubs;
using System;
using System.Threading.Tasks;
namespace Strata.KitchenSink.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/signalr-hub")]
public class SignalRHubController : ControllerBase
{
private readonly IHubContext<SignalRHub, ISignalRHub> _signalRHubContext;
public SignalRHubController(IHubContext<SignalRHub, ISignalRHub> signalRHubContext)
{
_signalRHubContext = signalRHubContext;
}
[HttpGet("info")]
[ProducesResponseType(200)]
public async Task<IActionResult> Info(string payload)
{
await _signalRHubContext.SendInfo(Guid.NewGuid(), string.IsNullOrEmpty(payload) ? "Info Payload" : payload);
return StatusCode(200);
}
[HttpGet("success")]
[ProducesResponseType(200)]
public async Task<IActionResult> Success(string payload)
{
await _signalRHubContext.SendSuccess(Guid.NewGuid(), string.IsNullOrEmpty(payload) ? "Success Payload" : payload);
return StatusCode(200);
}
[HttpGet("failure")]
[ProducesResponseType(200)]
public async Task<IActionResult> Failure(string payload)
{
await _signalRHubContext.SendFailure(Guid.NewGuid(), string.IsNullOrEmpty(payload) ? "Failure Payload" : payload);
return StatusCode(200);
}
[HttpGet("status")]
[ProducesResponseType(200)]
public async Task<IActionResult> Status(string payload)
{
await _signalRHubContext.SendStatus(SignalRHub.SuccessStatus, Guid.NewGuid(), string.IsNullOrEmpty(payload) ? "Status Payload" : payload);
return StatusCode(200);
}
[HttpGet("infotogroup")]
[ProducesResponseType(200)]
public async Task<IActionResult> InfoToGroup(string group, string payload)
{
CheckIfEmptyGroup(group);
await _signalRHubContext.SendInfoToGroup(group, Guid.NewGuid(), string.IsNullOrEmpty(payload) ? "Info to Group Payload" : payload);
return StatusCode(200);
}
[HttpGet("successtogroup")]
[ProducesResponseType(200)]
public async Task<IActionResult> SuccessToGroup(string group, string payload)
{
CheckIfEmptyGroup(group);
await _signalRHubContext.SendSuccessToGroup(group, Guid.NewGuid(), string.IsNullOrEmpty(payload) ? "Success to Group Payload" : payload);
return StatusCode(200);
}
[HttpGet("failuretogroupwithpayload")]
[ProducesResponseType(200)]
public async Task<IActionResult> FailureToGroupPayload(string group, string payload)
{
CheckIfEmptyGroup(group);
await _signalRHubContext.SendFailureToGroup(group, Guid.NewGuid(), string.IsNullOrEmpty(payload) ? "Failure to Group Payload" : payload);
return StatusCode(200);
}
[HttpGet("failuretogroupwithexception")]
[ProducesResponseType(200)]
public async Task<IActionResult> FailureToGroupException(string group, string payload)
{
CheckIfEmptyGroup(group);
await _signalRHubContext.SendFailureToGroup(group, Guid.NewGuid(), new Exception(string.IsNullOrEmpty(payload) ? "Failure to Group Exception" : payload));
return StatusCode(200);
}
private static void CheckIfEmptyGroup(string group)
{
if (string.IsNullOrEmpty(group))
{
throw new Exception("Group cannot be null or empty");
}
}
}
}
@@ -0,0 +1,18 @@
using Strata.KitchenSink.Biz.Departments;
using Strata.SqlTools.EntityFramework.Pagination;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Strata.KitchenSink.Api.Models
{
/// <summary>
/// Used by DepartmentsController to parse paging and filtering options from query string
/// </summary>
public class DepartmentsLoadOptions: PagingOptions
{
public string Search { get; set; }
public DepartmentType? DepartmentType { get; set; }
}
}
+34
View File
@@ -0,0 +1,34 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Serilog;
using Strata.Logging.DependencyInjection;
namespace Strata.KitchenSink.Api
{
[ExcludeFromCodeCoverage]
public class Program
{
public static void Main(string[] args)
{
try
{
CreateWebHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Error(ex, "There was an unhandled exception that caused the program to crash");
//This is for serilog to ensure that all of the cached logs are flushed when the app crashes
//This is useful so we can capture logs when the application crashes during startup:
Log.CloseAndFlush();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStrataLogging()
.UseStartup<Startup>();
}
}
@@ -0,0 +1,14 @@
{
"profiles": {
"Strata.KitchenSink.Api": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"Hangfire__RunLocally": "true",
"AWS_Profile": "kitchensink",
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:8443"
}
}
}
+108
View File
@@ -0,0 +1,108 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Prometheus;
using Strata.ApiLib.Core.Cors.Extensions;
using Strata.ApiLib.Core.ExceptionHandling.DependencyInjection.Bootstrappers;
using Strata.ApiLib.Core.Logging.Bootstrappers;
using Strata.ApiLib.Core.StrataAuthentication.Bootstrappers;
using Strata.ApiLib.Core.Transformers;
using Strata.KitchenSink.Biz.Configuration;
using Strata.SignalR.Configuration;
using Strata.SwaggerExtensions;
using System.Diagnostics.CodeAnalysis;
using System.Net.Mime;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Strata.KitchenSink.Api
{
[ExcludeFromCodeCoverage]
public class Startup
{
private readonly IConfiguration _configuration;
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddGlobalExceptionMiddleware(_configuration);
services.AddControllers(options =>
{
//Prevent browsers from caching API responses (this addresses an IE11 issue):
options.Filters.Add(
new ResponseCacheAttribute()
{
Location = ResponseCacheLocation.None,
NoStore = false,
Duration = -1
});
options.ReturnHttpNotAcceptable = true;
options.Filters.Add(new ProducesAttribute(MediaTypeNames.Application.Json));
options.Filters.Add(new ConsumesAttribute(MediaTypeNames.Application.Json));
options.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()));
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
// configure hangfire
services.AddStrataHangfire();
services.AddKitchenSinkServices();
services.AddRouting();
services.AddSwagger(_configuration);
services.AddStrataAuthentication(_configuration);
services.AddStrataCors(_configuration);
services.AddHealthChecks();
services.AddStrataSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
app.UseGlobalExceptionMiddleware();
app.UseRouting();
app.UseStrataCorsPolicy(env);
if (!env.IsDevelopment())
{
app.UseHsts();
app.UseHttpsRedirection();
}
app.UseMetricServer();
app.UseHttpMetrics();
app.UseStrataAuthentication();
app.UseClaimsLogging();
app.ConfigureSwagger(env, provider);
#if DEBUG
//app.UseHangfireDashboard("/hangfire", new DashboardOptions
// {
// Authorization = new[] {new AllowAnyoneDashboardAuthorizationFilter()}
// });
#endif
app.UseHealthChecks("/health");
app.UseEndpoints(endpoints => endpoints.MapControllers().RequireAuthorization());
app.UseEndpoints(endpoints => endpoints.MapSignalRHub());
}
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.1.112" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Strata.ApiLib.Core" Version="5.3.0" />
<PackageReference Include="Strata.Excel.Core" Version="0.13.0" />
<PackageReference Include="Strata.Hangfire.AspNetCore" Version="3.0.0" />
<PackageReference Include="Strata.Logging" Version="3.6.0" />
<PackageReference Include="Strata.SignalR" Version="0.3.0" />
<PackageReference Include="Strata.SwaggerExtensions" Version="2.3.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Strata.KitchenSink.Biz\Strata.KitchenSink.Biz.csproj" />
<ProjectReference Include="..\Strata.KitchenSink.Client\Strata.KitchenSink.Client.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,22 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"EndPoints": {
"IdentityServer": "https://identity.dev.stratanetwork.net"
},
"Shared": {
"Cors": {
"WildcardSubdomains": [
"https://*.sdt.local",
"https://*.dev.stratanetwork.net"
]
}
}
}
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"Version": "0.0.0"
}
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Strata.KitchenSink.Biz.Hubs;
namespace Strata.KitchenSink.Biz.Configuration
{
public static class HubEndpointRouteBuilderExtensions
{
public static HubEndpointConventionBuilder MapSignalRHub(this IEndpointRouteBuilder endpoints)
{
return endpoints.MapHub<SignalRHub>("/api/SignalRHub", configureOptions: null);
}
}
}
@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Strata.KitchenSink.Biz.Departments;
namespace Strata.KitchenSink.Biz.DbContexts
{
// This does not use the MultiTenant DbContext. For more information on how to use it please
// see this link: https://confluence.sdt.local/pages/viewpage.action?pageId=37335971#SQL(withEntityFrameworkCore)-Multitenancy
// and the following sample: https://git.sdt.local/projects/DOCKER/repos/strata.demos.microservices.reviews/browse
public class KitchenSinkDbContext : DbContext
{
public DbSet<Department> Departments { get; set; }
public KitchenSinkDbContext(DbContextOptions<KitchenSinkDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
}
}
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Strata.KitchenSink.Biz.Departments
{
public class Department
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int DepartmentId { get; set; }
public string DepartmentCode { get; set; }
public string Description { get; set; }
public DepartmentType DepartmentType { get; set; }
}
}
@@ -0,0 +1,8 @@
namespace Strata.KitchenSink.Biz.Departments
{
public class DepartmentImportResult
{
public int NewDepartmentsCount { get; set; }
public int UpdatedDepartmentsCount { get; set; }
}
}
@@ -0,0 +1,100 @@
using ClosedXML.Excel;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Strata.Excel.Core;
using Strata.Excel.Core.Import;
using Strata.SqlTools.EntityFramework;
using Strata.SqlTools.EntityFramework.Pagination;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using static Strata.Excel.Core.ExportUtils;
namespace Strata.KitchenSink.Biz.Departments
{
public class DepartmentRepository : BaseRepository<Department, DbContexts.KitchenSinkDbContext>, IDepartmentRepository
{
public DepartmentRepository(DbContexts.KitchenSinkDbContext dbContext) : base(dbContext)
{
}
public async Task<PagedData<Department>> GetAllAsync(PagingOptions pagingOptions, string search, DepartmentType? departmentType, CancellationToken cancellationToken)
{
var query = _dbContext.Departments.AsQueryable();
// apply search and filters
if (search != null)
{
var lowerCaseSearch = search.ToLower();
query = query.Where(item => item.Description.ToLower().Contains(lowerCaseSearch) || item.DepartmentCode.ToLower().Contains(lowerCaseSearch));
}
if (departmentType != null)
{
query = query.Where(item => item.DepartmentType == departmentType);
}
// apply pagination
return await query.ToPagedAsync(pagingOptions, cancellationToken);
}
public async Task<IXLWorkbook> ExportAsync(bool withCoverPage, CancellationToken cancellationToken)
{
var departments = await this.GetAllAsync(cancellationToken);
var departmentExportData = departments.Select(x => (DepartmentExport)x);
// options also include title and subtitle if you want to add a separate cover page worksheet to the workbook
ExportOptions options = new ExportOptions
{
EmptyMessage = "No departments"
};
if (withCoverPage)
{
options.Title = "Departments";
options.SubTitle = "Strata Decision Technology®";
}
return CreateExcelWorkbook(departmentExportData, options);
}
public async Task<DepartmentImportResult> ImportAsync(IFormFile file, CancellationToken cancellationToken)
{
await using var memoryStream = new MemoryStream();
file.CopyTo(memoryStream);
var result = new DepartmentImportResult();
var workbook = new ClosedXML.Excel.XLWorkbook(memoryStream);
var worksheet = workbook.Worksheets.First();
var departments = worksheet.GetDataFromExcel<DepartmentExport>();
// simple example of iterating through rows and checking if department already exists.
// this is example code and assumes a small number of departments to upload
// for your use case, you should consider batching the lookup and doing this in a hangfire task
foreach (var row in departments)
{
if (string.IsNullOrEmpty(row.DepartmentCode)) { continue; }
var departmentCode = row.DepartmentCode;
var description = row.Description;
var departmentType = Enum.Parse<DepartmentType>(row.DepartmentType);
var department = await _dbContext.Departments.SingleOrDefaultAsync(x => x.DepartmentCode == departmentCode);
if (department == null)
{
department = new Department() { DepartmentCode = departmentCode };
result.NewDepartmentsCount += 1;
}
else
{
result.UpdatedDepartmentsCount += 1;
}
department.Description = description;
department.DepartmentType = departmentType;
_dbContext.Update(department);
}
await _dbContext.SaveChangesAsync(cancellationToken);
return result;
}
}
}
@@ -0,0 +1,9 @@
namespace Strata.KitchenSink.Biz.Departments
{
public enum DepartmentType
{
Excluded = 0,
Overhead = 1,
Revenue = 2
}
}
@@ -0,0 +1,23 @@
namespace Strata.KitchenSink.Biz.Departments
{
internal class DepartmentExport
{
public string DepartmentCode { get; set; }
public string Description { get; set; }
public string DepartmentType { get; set; }
public DepartmentExport()
{
}
public DepartmentExport(Department x)
{
DepartmentCode = x.DepartmentCode;
Description = x.Description;
DepartmentType = x.DepartmentType.ToString();
}
public static explicit operator DepartmentExport(Department x) => new DepartmentExport(x);
}
}
@@ -0,0 +1,18 @@
using ClosedXML.Excel;
using Microsoft.AspNetCore.Http;
using Strata.SqlTools.EntityFramework;
using Strata.SqlTools.EntityFramework.Pagination;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.KitchenSink.Biz.Departments
{
public interface IDepartmentRepository : IBaseRepository<Department>
{
Task<PagedData<Department>> GetAllAsync(PagingOptions pagingOptions, string search, DepartmentType? departmentType, CancellationToken cancellationToken);
Task<IXLWorkbook> ExportAsync(bool withCoverPage, CancellationToken cancellationToken);
Task<DepartmentImportResult> ImportAsync(IFormFile file, CancellationToken cancellation);
}
}
@@ -0,0 +1,82 @@
using Microsoft.AspNetCore.SignalR;
using System;
using System.Text.Json;
using System.Threading.Tasks;
namespace Strata.KitchenSink.Biz.Hubs
{
public interface ISignalRHub
{
Task SendStatus(string status, string objectId, string payload);
}
public class SignalRHub : Hub<ISignalRHub>
{
public const string ErrorStatus = "error";
public const string SuccessStatus = "success";
public const string InfoStatus = "info";
public async Task Register(string topic)
{
await Groups.AddToGroupAsync(Context.ConnectionId, topic);
}
public async Task Remove(string topic)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, topic);
}
}
public static class SignalRHubContextExtensions
{
public static Task SendInfo(this IHubContext<SignalRHub, ISignalRHub> context, object objectId, object payload)
{
return context.SendStatus(SignalRHub.InfoStatus, objectId, payload);
}
public static Task SendSuccess(this IHubContext<SignalRHub, ISignalRHub> context, object objectId, object payload)
{
return context.SendStatus(SignalRHub.SuccessStatus, objectId, payload);
}
public static Task SendFailure(this IHubContext<SignalRHub, ISignalRHub> context, object objectId, Exception exception)
{
return context.SendStatus(SignalRHub.ErrorStatus, objectId, exception.Message);
}
public static Task SendFailure(this IHubContext<SignalRHub, ISignalRHub> context, object objectId, object payload)
{
return context.SendStatus(SignalRHub.ErrorStatus, objectId, payload);
}
public static Task SendStatus(this IHubContext<SignalRHub, ISignalRHub> context, string status, object objectId, object payload)
{
return context.Clients.All.SendStatus(status, JsonSerializer.Serialize(objectId), JsonSerializer.Serialize(payload));
}
public static Task SendInfoToGroup(this IHubContext<SignalRHub, ISignalRHub> context, string topic, object objectId, object payload)
{
return context.SendStatusToGroup(topic, SignalRHub.InfoStatus, objectId, payload);
}
public static Task SendSuccessToGroup(this IHubContext<SignalRHub, ISignalRHub> context, string topic, object objectId, object payload)
{
return context.SendStatusToGroup(topic, SignalRHub.SuccessStatus, objectId, payload);
}
public static Task SendFailureToGroup(this IHubContext<SignalRHub, ISignalRHub> context, string topic, object objectId, Exception exception)
{
return context.SendStatusToGroup(topic, SignalRHub.ErrorStatus, objectId, exception.Message);
}
public static Task SendFailureToGroup(this IHubContext<SignalRHub, ISignalRHub> context, string topic, object objectId, object payload)
{
return context.SendStatusToGroup(topic, SignalRHub.ErrorStatus, objectId, payload);
}
public static Task SendStatusToGroup(this IHubContext<SignalRHub, ISignalRHub> context, string topic, string status, object objectId, object payload)
{
return context.Clients.Group(topic).SendStatus(status, JsonSerializer.Serialize(objectId), JsonSerializer.Serialize(payload));
}
}
}
@@ -0,0 +1,12 @@
using System;
namespace Strata.KitchenSink.Biz
{
public class KitchenSinkJob
{
public void RunKitchenSinkJob()
{
Console.WriteLine("Hello World!");
}
}
}
@@ -0,0 +1,69 @@
using System;
using Amazon.SecretsManager;
using Amazon.SecretsManager.Extensions.Caching;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Npgsql;
using Strata.Hangfire.Configuration;
using Strata.KitchenSink.Biz.DbContexts;
using Strata.KitchenSink.Biz.Departments;
using Strata.SqlTools.Configuration.Common;
using Strata.SqlTools.Configuration.Postgres;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class KitchenSinkServiceCollectionExtensions
{
public static IServiceCollection AddKitchenSinkServices(this IServiceCollection services)
{
//Aws
services
.AddSingleton<IAmazonSecretsManager, AmazonSecretsManagerClient>()
.AddSingleton<SecretsManagerCache>();
services.AddPostgres<KitchenSinkDbContext>((options, provider) =>
{
var secretsManager = provider.GetRequiredService<SecretsManagerCache>();
var connectionString = secretsManager.GetSecretString("common/rds/sdt-postgres-microservices-shared").GetAwaiter().GetResult();
var cBuilder = new NpgsqlConnectionStringBuilder(connectionString)
{
Database = "kitchensink"
};
options.ConnectionString = cBuilder.ToString();
});
services.AddScoped<IDepartmentRepository, DepartmentRepository>();
services.AddCachedSmcServiceClient();
services.AddHttpContextAccessor();
services.AddClaimsPrincipalAccessor();
services.ConfigureHangfireOptionsFromAws(options =>
{
options.Schema = "kitchensink";
});
return services;
}
public static IServiceCollection AddPostgres<T>(
this IServiceCollection services,
Action<PostgresOptions, IServiceProvider> builder)
where T : DbContext
{
services.AddOptions<PostgresOptions>().Configure(builder);
services.AddDbContext<T>((context, optionsBuilder) =>
{
var options = context.GetService<IOptions<PostgresOptions>>().Value;
optionsBuilder.AddDefaultOptions(context, options.SqlOptions).AddDefaultPostgresOptions<T>(options);
var dbContextOptions = options.AdditionalDbContextOptions;
if (dbContextOptions == null)
return;
dbContextOptions(optionsBuilder);
});
return services;
}
}
}
@@ -0,0 +1,51 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Strata.KitchenSink.Biz.DbContexts;
namespace Strata.KitchenSink.Biz.Migrations
{
[DbContext(typeof(KitchenSinkDbContext))]
[Migration("20211019191337_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "5.0.4")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("Strata.KitchenSink.Biz.Departments.Department", b =>
{
b.Property<int>("DepartmentId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("department_id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("DepartmentCode")
.HasColumnType("text")
.HasColumnName("department_code");
b.Property<int>("DepartmentType")
.HasColumnType("integer")
.HasColumnName("department_type");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.HasKey("DepartmentId")
.HasName("pk_departments");
b.ToTable("departments");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Strata.KitchenSink.Biz.Migrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "departments",
columns: table => new
{
department_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
department_code = table.Column<string>(type: "text", nullable: true),
description = table.Column<string>(type: "text", nullable: true),
department_type = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_departments", x => x.department_id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "departments");
}
}
}
@@ -0,0 +1,49 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Strata.KitchenSink.Biz.DbContexts;
namespace Strata.KitchenSink.Biz.Migrations
{
[DbContext(typeof(KitchenSinkDbContext))]
partial class KitchenSinkDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Relational:MaxIdentifierLength", 63)
.HasAnnotation("ProductVersion", "5.0.4")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
modelBuilder.Entity("Strata.KitchenSink.Biz.Departments.Department", b =>
{
b.Property<int>("DepartmentId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("department_id")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("DepartmentCode")
.HasColumnType("text")
.HasColumnName("department_code");
b.Property<int>("DepartmentType")
.HasColumnType("integer")
.HasColumnName("department_type");
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.HasKey("DepartmentId")
.HasName("pk_departments");
b.ToTable("departments");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.SecretsManager" Version="3.7.100.33" />
<PackageReference Include="AWSSDK.SecretsManager.Caching" Version="1.0.5" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="6.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Strata.Excel.Core" Version="0.13.0" />
<PackageReference Include="Strata.Hangfire" Version="3.0.0" />
<PackageReference Include="ClosedXML.Signed" Version="0.95.4" />
<PackageReference Include="Strata.SignalR" Version="0.3.0" />
<PackageReference Include="Strata.SqlTools.Configuration.Postgres" Version="5.2.0" />
<PackageReference Include="Strata.SMC.Client" Version="2.3.0" />
<PackageReference Include="Strata.SqlTools.EntityFramework" Version="5.2.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Configuration\" />
</ItemGroup>
</Project>
@@ -0,0 +1,10 @@
using System;
namespace Strata.KitchenSink.Client.Dtos
{
public class KitchenSinkDto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
}
@@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;
using Strata.KitchenSink.Client.Dtos;
namespace Strata.KitchenSink.Client
{
public interface IKitchenSinkServiceClient
{
Task<KitchenSinkDto> GetAsync(CancellationToken cancellationToken = default);
}
}
@@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Strata.ApiCommunication.Http;
using Strata.KitchenSink.Client.Dtos;
namespace Strata.KitchenSink.Client
{
[ExcludeFromCodeCoverage]
public class KitchenSinkServiceClient : IKitchenSinkServiceClient
{
private readonly HttpClient _httpClient;
public KitchenSinkServiceClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<KitchenSinkDto> GetAsync(CancellationToken cancellationToken = default)
{
var request = new HttpRequestMessage(HttpMethod.Get, "api/v1/KitchenSink/Hello");
return await _httpClient.SendAsync<KitchenSinkDto>(request, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,36 @@
#if NETCOREAPP
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Configuration;
using Strata.KitchenSink.Client;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
[ExcludeFromCodeCoverage]
public static class KitchenSinkServiceCollectionExtensions
{
/// <summary>
/// Registers an instance of an <see cref="IKitchenSinkServiceClient"/> to the given <see cref="IServiceCollection"/>
/// </summary>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
public static IHttpClientBuilder AddKitchenSinkServiceClient(this IServiceCollection services)
{
return services.AddHttpClientWithStrataHandlers<IKitchenSinkServiceClient, KitchenSinkServiceClient>((provider, client) =>
{
var config = provider.GetService<IConfiguration>();
var endpoint = config.GetValue<string>("endPoints:kitchensink");
if (string.IsNullOrWhiteSpace(endpoint))
{
throw new InvalidOperationException(
"Endpoint for the KitchenSink service was not found in the configuration with the key of 'endPoints:kitchensink'");
}
client.BaseAddress = new Uri(endpoint);
});
}
}
}
#endif
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;netcoreapp3.1;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Strata.ApiCommunication.Http" Version="6.1.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Strata.Logging.DependencyInjection;
namespace Strata.KitchenSink.Hangfire.Dashboard
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseStrataLogging()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
@@ -0,0 +1,14 @@
{
"profiles": {
"Strata.KitchenSink.Hangfire.Dashboard": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"Hangfire__RunLocally": "true",
"AWS_Profile": "kitchensink",
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}
@@ -0,0 +1,46 @@
using Hangfire;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Strata.Hangfire.AspNetCore.Filters.Dashboard;
namespace Strata.KitchenSink.Hangfire.Dashboard
{
public class Startup
{
private readonly IConfiguration _configuration;
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
// configure hangfire
services.AddStrataHangfire();
services.AddKitchenSinkServices();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHangfireDashboard("", new DashboardOptions
{
Authorization = new[] {new AllowAnyoneDashboardAuthorizationFilter()}
});
app.UseRouting();
}
}
}
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.100.33" />
<PackageReference Include="Strata.Hangfire.AspNetCore" Version="3.0.0" />
<PackageReference Include="Strata.Logging" Version="3.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Strata.KitchenSink.Biz\Strata.KitchenSink.Biz.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
@@ -0,0 +1,40 @@
using System;
using Hangfire;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Strata.Logging.DependencyInjection;
namespace Strata.KitchenSink.Hangfire
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseStrataLogging()
.ConfigureServices((hostContext, services) =>
{
services.Configure<HostOptions>(option => { option.ShutdownTimeout = TimeSpan.FromSeconds(60); });
#region Hangfire
// setup storage - tells hangfire where to send the job
services.AddStrataHangfire();
// setup server - allows application to process background jobs
services.AddHangfireServer(options =>
{
});
services.AddKitchenSinkServices();
#endregion
});
}
}
@@ -0,0 +1,12 @@
{
"profiles": {
"Strata.KitchenSink.Jobs.Service": {
"commandName": "Project",
"environmentVariables": {
"Hangfire__RunLocally": "true",
"DOTNET_ENVIRONMENT": "Development",
"AWS_Profile": "kitchensink"
}
}
}
}
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<UserSecretsId>dotnet-Strata.KitchenSink.Jobs.Service-D8B8B3CF-E629-4481-AAC6-26BC6694D20B</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.100.33" />
<PackageReference Include="Strata.Hangfire.NetCore" Version="3.0.0" />
<PackageReference Include="Strata.Logging" Version="3.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Strata.KitchenSink.Biz\Strata.KitchenSink.Biz.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
+274
View File
@@ -0,0 +1,274 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
###################
# compiled source #
###################
*.com
*.class
*.dll
*.exe
*.pdb
*.dll.config
*.cache
*.suo
# Include dlls if theyre in the NuGet packages directory
!/packages/*/lib/*.dll
# Include dlls if they're in the CommonReferences directory
!*CommonReferences/*.dll
####################
# VS Upgrade stuff #
####################
_UpgradeReport_Files/
###############
# Directories #
###############
bin/
obj/
TestResults/
###################
# Web publish log #
###################
*.Publish.xml
#############
# Resharper #
#############
/_ReSharper.*
*.ReSharper.*
############
# Packages #
############
# its better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
######################
# Logs and databases #
######################
*.log
*.sqlite
# OS generated files #
######################
.DS_Store?
ehthumbs.db
Icon?
Thumbs.db
# User-specific files
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
# Visual Studo 2015 cache/options directory
.vs/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
ClickOnce/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
bower_components/
orleans.codegen.cs
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
**/node_modules/*
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# SonarQube files
sonarqube/
.vs/
*.orig
buildlogs/
# local envrionment files
cypress.development.json
cypress.local.json
+38
View File
@@ -0,0 +1,38 @@
# Strata.KitchenSink
### SonarQube Metrics
[![Quality Gate Status](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=alert_status)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
[![Bugs](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=bugs)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
[![Code Smells](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=code_smells)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
[![Coverage](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=coverage)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
[![Duplicated Lines (%)](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=duplicated_lines_density)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
[![Maintainability Rating](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=sqale_rating)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
[![Reliability Rating](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=reliability_rating)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
[![Security Rating](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=security_rating)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
[![Technical Debt](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=sqale_index)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
[![Vulnerabilities](https://sonarqube.sdt.local/api/project_badges/measure?project=Strata.KitchenSink&metric=vulnerabilities)](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
### TeamCity Build Status
<a href="https://teamcity.sdt.local/viewType.html?buildTypeId=Services_StrataKitchenSink_01Build&guest=1">
<img src="https://teamcity.sdt.local/app/rest/builds/buildType(id:Services_StrataKitchenSink_01Build)/statusIcon"/>
</a>
## Branching and Versioning
|branch|version format|example|
|---|---|---|
|master|#.#.#|1.2.3|
|feature/*|#.#+1.0-featureName.#|1.3.0-newfeat.1|
|fix/*|#.#.#+1-fixName.#|1.2.4-bug.1|
See our [confluence page](https://confluence.sdt.local/display/DOP/Branching+and+Versioning) for more information
## Usage
### Building
```
docker build -t strata.kitchensink.web:local .
```
### Running
```
docker run --rm -it -p 8080:80 -p 8443:443 strata.kitchensink.web:local
```
@@ -0,0 +1,16 @@
//Create different environment.json files for each environment you want to run the tests. This file works as a template.
//Example : cypress.development.json, cypress.local.json
{
"baseUrl": "https://kitchensink.dev.stratanetwork.net",
"env": {
"username": "memetest",
"password": "Password1",
"orgPin": "0436",
"dbguid": "b8018250-af34-403e-bdcc-1770714042b2",
"clientId": "StrataUI",
//update the value of environment, where you want to run the tests. Ex: development, local.
//This is just for reference purpose
"environment": ""
}
}
@@ -0,0 +1,7 @@
{
"baseUrl": "https://kitchensink.dev.stratanetwork.net",
"requestTimeout": 8000,
"defaultCommandTimeout": 5000,
"trashAssetsBeforeRuns": true,
"downloadsFolder": "cypress/fixtures"
}
@@ -0,0 +1,54 @@
import importExportPage from '../pageObjects/importExportPage'
import welcomePage from '../pageObjects/welcomePage'
const iePage = new importExportPage()
const wcPage = new welcomePage()
before(function() {
// # Login with Identity Server Authentication
cy.login(Cypress.env('username'), Cypress.env('password'), Cypress.env('dbguid'), Cypress.env('clientId'))
// # Verify that we've logined in right user
cy.findByText(Cypress.env('username'), {exact: true}).should("be.visible")
// # verify url in correct
cy.url().should('eql', 'https://kitchensink.dev.stratanetwork.net/')
// # verify page title
cy.title().should('eq', 'KitchenSink')
})
describe('Import and Export Page Tests', () => {
beforeEach(function() {
// # click the menu item
iePage.getImportExportMenu().should("be.visible").click()
// # verify page url
cy.url().should('eql', 'https://kitchensink.dev.stratanetwork.net/api/import-export')
// # verify the page heading
iePage.getPageHeading().should("be.visible")
})
it('Verify export tests', () => {
const filePath = "cypress/fixtures/Departments_Export_" + new Date().toJSON().slice(0,10) + ".xlsx"
const data = [
"DepartmentCode",
"Description",
"DepartmentType"
]
iePage.clickExport()
iePage.verifyExport(filePath, data)
})
it('Verify import tests', () => {
iePage.importFile("Departments_Export_" + new Date().toJSON().slice(0,10) + ".xlsx")
iePage.verifyImport()
})
afterEach(function() {
wcPage.getWelcomeMenu().click().should("be.visible")
})
})
after(function() {
// # logout
cy.logout()
})
@@ -0,0 +1,214 @@
import { loginLocally, loginInteractively } from '../pageObjects/login'
import scoreMemberTreePage from '../pageObjects/scoreMemberTreePage'
import welcomePage from '../pageObjects/welcomePage'
const smtPage = new scoreMemberTreePage()
const wcPage = new welcomePage()
before(function() {
// # Login with Identity Server Authentication
if (Cypress.env('environment') === 'local') {
loginLocally();
}
})
describe('Score Member Tree Page Tests', () => {
beforeEach(function() {
if (Cypress.env('environment') !== 'local') {
loginInteractively();
}
// # click the menu item
smtPage.getScoreMemberTreeMenu().should("be.visible").click()
// # verify page url
cy.url().should('eql', 'https://kitchensink.dev.stratanetwork.net/score/score-member-tree/modal')
// # verify the page heading
smtPage.getPageHeading().should("be.visible")
})
it('Verify modal select of score members', () => {
smtPage.chooseTab("Modal")
smtPage.getSelectedTab("Modal").should("exist")
smtPage.clickOpen("Open Modal").should("be.visible")
smtPage.selectHierarchy("Department").should("be.visible")
cy.expandTreeNode(["Northern Valley Health System (6552)", "01 - Metro (3672)", "100 - Central Metro (3040)", "50 - NVMG - Medical Center Metro Central (385)"], "multi")
cy.checkTreeNodes(["10 - Northern Valley Medical Center (487)", "50 - 0000 - BALANCE SHEET FOR FINANCE USE"])
smtPage.clickSave()
smtPage.verifySavedMembers(["10 - Northern Valley Medical Center (487)", "50 - 0000 - BALANCE SHEET FOR FINANCE USE"])
smtPage.clickOpen("Open Modal")
smtPage.getSelectedHierarchy().should("have.text", 'Department').should("be.visible")
cy.expandTreeNode(["Northern Valley Health System (6552)", "01 - Metro (3672)", "100 - Central Metro (3040)", "50 - NVMG - Medical Center Metro Central (385)"], "multi")
smtPage.verifyCheckedNodes(["10 - Northern Valley Medical Center (487)", "50 - 0000 - BALANCE SHEET FOR FINANCE USE"])
smtPage.clickCancel()
})
it('Verify drawer select of score members', () => {
smtPage.chooseTab("Drawer")
smtPage.getSelectedTab("Drawer").should("exist")
smtPage.clickOpen("Open Drawer").should("be.visible")
smtPage.selectHierarchy("DSS Department Rollup 1").should("be.visible")
cy.expandTreeNode(["Hospital (632)"], "single")
cy.selectTreeNode("10 - 0200 - SALVATION ARMY GRANT").should("be.visible")
smtPage.clickSave()
smtPage.verifySavedMembers(["10 - 0200 - SALVATION ARMY GRANT"])
})
it('Verify member select of score members', () => {
cy.intercept('GET', 'https://schema-api.dev.stratanetwork.net/api/v2/dimensions/Entity/members?columns=id&columns=name').as('MemberList')
smtPage.chooseTab("Member")
smtPage.getSelectedTab("Member").should("exist")
cy.wait('@MemberList').then((intercept) => {
smtPage.selectName("60 - Seaside Medical Clinic").should("be.visible")
})
smtPage.verifySelectedName("60 - Seaside Medical Clinic").should("be.visible")
smtPage.verifySelectedVAlue("7").should("be.visible")
})
it('Verify Select All of score members', () => {
smtPage.chooseTab("Modal")
smtPage.clickOpen("Open Modal").should("be.visible")
smtPage.selectHierarchy("CAP Department").should("be.visible")
cy.expandTreeNode(["Northern Valley Health System (4068)", "03 - North (164)", "310 - Bakersfield North (54)"], "multi")
cy.wait(1000)
//TODO: try this instead of the wait
/*
smtPage.getHierarchyNodeList();
cy.wait('@DepartmentNodeList').then(() => {});
smtPage.selectHierarchy("CAP Department").should("be.visible")
cy.wait('@CAPDepartmentNodeList').then(() => {});
*/
cy.findAllByRole("button").eq(2).click().should("be.visible")
cy.findByRole("menuitem", {name: "Select All"}).should("be.visible").click()
const nodesExpectedToBeChecked = ["Northern Valley Health System (4068)", "0 - Not Specified (1)",
"01 - Metro (2844)", "02 - Central (538)", "03 - North (164)",
"310 - Bakersfield North (54)", "70 - Northern Valley Immediate Care (18)", "80 - Northern Valley Visiting Nurses Association (14)"];
smtPage.verifyCheckedNodes(nodesExpectedToBeChecked)
cy.expandTreeNode(["70 - Northern Valley Immediate Care (18)"], "multi")
smtPage.verifyUncheckedNodes(["70 - 0767 - FACILITIES AND MAINTENANCE"]);
cy.findAllByRole("button").eq(1).click().should("be.visible")
})
it('Verify Clear Selection of score members', () => {
smtPage.chooseTab("Modal")
smtPage.clickOpen("Open Modal").should("be.visible")
smtPage.selectHierarchy("DSS Department").should("be.visible")
cy.expandTreeNode(["Clinic (1698)", "Not Specified (1698)", "IMAGING SERVICES (68)"], "multi")
cy.findAllByRole("button").eq(2).click().should("be.visible")
cy.findByRole("menuitem", {name: "Select All"}).should("be.visible").click()
cy.findAllByRole("button").eq(2).click().should("be.visible")
cy.findByRole("menuitem", {name: "Clear Selection"}).should("be.visible").click()
const nodesExpectedToBeUnchecked = ["Clinic (1698)", "Not Specified (1698)", "IMAGING SERVICES (68)",
"Not Specified (1630)", "Not Specified (4240)", "Hospital (632)"]
smtPage.verifyUncheckedNodes(nodesExpectedToBeUnchecked)
cy.findAllByRole("button").eq(1).click().should("be.visible")
})
it('should clear checked but not visible score tree members', () => {
smtPage.chooseTab("Modal");
smtPage.clickOpen("Open Modal").should("be.visible");
smtPage.selectHierarchy("DSS Department").should("be.visible");
const targetTitles = ["Northern Valley Health System (6552)", "01 - Metro (3672)", "100 - Central Metro (3040)", "50 - NVMG - Medical Center Metro Central (385)"];
const targetValues = ["10 - Northern Valley Medical Center (487)", "50 - 0000 - BALANCE SHEET FOR FINANCE USE"];
cy.expandTreeNode(targetTitles, "multi");
cy.checkTreeNodes(targetValues);
smtPage.clickSave();
smtPage.verifySavedMembers(targetValues);
smtPage.clickOpen("Open Modal");
smtPage.getSelectedHierarchy().should("have.text", 'Department').should("be.visible");
cy.findByLabelText('border').click();
cy.findByRole("menuitem", { name: "Clear Selection" }).click();
smtPage.clickSave();
smtPage.verifyNodesDoNotExist(targetValues);
});
it('Verify View All score members', () => {
smtPage.chooseTab("Modal")
smtPage.clickOpen("Open Modal").should("be.visible")
smtPage.selectHierarchy("Department").should("be.visible")
cy.expandTreeNode(["Northern Valley Health System (6552)"], "multi")
cy.checkTreeNodes(["03 - North (163)", "09 - Other Affiliates (519)"])
cy.findByRole("button", {name: "View"}).click().should("be.visible")
cy.findByRole("menuitem", {name: "All"}).click().should("be.visible")
const nodesExpectedToBeVisible = ["Northern Valley Health System (6552)", "03 - North (163)",
"09 - Other Affiliates (519)", "02 - Central (2194)", "0 - Not Specified (4)", "01 - Metro (3672)"]
smtPage.verifyNodesVisible(nodesExpectedToBeVisible)
cy.findAllByRole("button").eq(1).click().should("be.visible")
})
it("Verify View Selected score members", () => {
//conditionally turn off exception handling for this specific error, in this specific test
//kitchensink has ErrorBoundary to catch unhandled Javacript errors, but it's not catching this
cy.on("uncaught:exception", (err, runnable) => {
if(err.message.includes("ResizeObserver loop limit exceeded")) {
return false
}
})
smtPage.chooseTab("Modal")
smtPage.clickOpen("Open Modal").should("be.visible")
smtPage.selectHierarchy("Department").should("be.visible")
cy.expandTreeNode(["Northern Valley Health System (6552)"], "multi")
cy.checkTreeNodes(["03 - North (163)", "09 - Other Affiliates (519)"])
cy.findByRole("button", {name: "View"}).click().should("be.visible")
cy.findByRole("menuitem", {name: "Selected"}).click().should("be.visible")
const nodesExpectedToBeVisible = ["03 - North (163)", "09 - Other Affiliates (519)"]
smtPage.verifyNodesVisible(nodesExpectedToBeVisible)
const nodesNotExpectedToBeVisible = ["Northern Valley Health System (6552)", "02 - Central (2194)",
"0 - Not Specified (4)", "01 - Metro (3672)"]
smtPage.verifyNodesDoNotExist(nodesNotExpectedToBeVisible)
cy.findAllByRole("button").eq(1).click().should("be.visible")
})
it('Verify Picker filter', () => {
smtPage.chooseTab("Picker")
smtPage.clickOpen("Edit").should("be.visible")
smtPage.selectHierarchy("Department").should("be.visible")
smtPage.verifyNodesVisible(["Northern Valley Health System (6552)"])
cy.findAllByRole("button").eq(3).click().should("be.visible")
cy.findByRole("radio", {name: "DepartmentType: Revenue"}).click()
smtPage.clickOpen("Edit").should("be.visible")
smtPage.verifyNodesVisible(["Northern Valley Health System (1302)"])
cy.findAllByRole("button").eq(3).click().should("be.visible")
})
// afterEach(function() {
// wcPage.getWelcomeMenu().click().should("be.visible")
// })
})
after(function() {
// # logout
if (Cypress.env('environment') === 'local') {
cy.logout()
}
})
@@ -0,0 +1,36 @@
import welcomePage from '../pageObjects/welcomePage'
const wcPage = new welcomePage()
describe('Welcome Page Tests', () => {
beforeEach(function() {
// # Login with Identity Server Authentication
cy.login(Cypress.env('username'), Cypress.env('password'), Cypress.env('dbguid'), Cypress.env('clientId'))
// # Verify that we've logined in right user
cy.findByText(Cypress.env('username'), {exact: true}).should("be.visible")
// # verify url in correct
cy.url().should('eql', Cypress.config('baseUrl') + '/')
// # verify page title
cy.title().should('eq', 'KitchenSink')
// # verify the page heading
cy.findByRole("heading", {name: 'Welcome'}).should("be.visible")
})
it('Verify welcome page', () => {
// # verify the page heading
wcPage.getWelcomeMenu().should("be.visible").click()
// # verify page url
cy.url().should('eql', Cypress.config('baseUrl') + '/home')
// # verify the card titles
cy.findByText('Overview', {exact: true}).should("be.visible")
cy.findByText('How to Contribute', {exact: true}).should("be.visible")
})
afterEach(function() {
// # logout
cy.logout()
})
})
@@ -0,0 +1,44 @@
export default class importExportPage {
getImportExportMenu() {
return cy.findByRole("menuitem", {name: 'Import Export'})
}
getPageHeading() {
return cy.findByRole("heading", {name: 'Import Export'})
}
verifyCardTitle(titleName: string) {
return cy.findAllByText(titleName).first()
}
clickExport(){
return cy.findByRole("button", {name: 'Export'}).click()
}
verifyExport(filePath: string, data: string[]){
// intercept the call to avoid using arbitrary wait for the download to complete
cy.intercept('GET', 'https://kitchensink-api.dev.stratanetwork.net/api/v1/departments/export').as('Export')
cy.wait(10000)
// call the parseXlsx task we created above to parse the excel and return data as json
cy.task('parseXlsx', {filePath})
.then(jsonData => {
//the assertion to check if that data matches the data we expected the excel file to have.
expect(jsonData[0].data[0]).to.eqls(data)
})
return
}
importFile(file: string) {
return cy.get("input[type=file]").attachFile("Departments_Export_" + new Date().toJSON().slice(0,10) + ".xlsx")
}
verifyImport(){
cy.intercept('POST', 'https://kitchensink-api.dev.stratanetwork.net/api/v1/departments/import').as ('Import')
cy.wait('@Import').then(() => {
cy.get(".ant-notification").should("contain", "departments updated.")
})
return
}
}
@@ -0,0 +1,22 @@
export function loginLocally() {
cy.login(
Cypress.env('username'),
Cypress.env('password'),
Cypress.env('dbguid'),
Cypress.env('clientId'));
}
export function loginInteractively() {
cy.visit('/');
enterText('Email or Username', Cypress.env('username'));
cy.findAllByRole('button', { name: 'Next' }).click();
enterText('Password', Cypress.env('password'));
enterText('Org PIN', Cypress.env('orgPin'));
cy.findAllByRole('button', { name: 'Next' }).click();
}
function enterText(name: string, value: string) {
cy.findByLabelText(name).type(value);
}
@@ -0,0 +1,144 @@
import { eq } from "cypress/types/lodash"
export default class scoreMemberTreePage {
getScoreMemberTreeMenu() {
return cy.findByRole("menuitem", {name: 'Score Member Tree'})
}
getPageHeading() {
return cy.findByRole("heading", {name: 'Score Member Tree'})
}
chooseTab(tabName: string) {
return cy.findByRole("tab", {name: tabName}).should("be.visible").click()
}
getSelectedTab(tabName: string) {
return cy.findByRole("tabpanel", {name: tabName})
}
selectHierarchy(hierarchyName: string) {
cy.findByRole("combobox").parent().next().click().should("be.visible")
return cy.findAllByTitle(hierarchyName).first().click()
}
expandNode(nodes: string[], level: 'single' | 'multi') {
let len = nodes.length
for(let i = 0; i < len; i++) {
if(level == 'single') {
cy.findByTitle(nodes[i]).prev().click()
} else if(level == 'multi') {
cy.findByTitle(nodes[i]).prev().prev().click()
}
}
return
}
selectNode(nodeName: string) {
return cy.findByTitle(nodeName).click()
}
verifySelectedMember(memberName: string) {
return cy.findByText("Selected: " + memberName)
}
checkNodes(nodeNames: string[]) {
let len = nodeNames.length
for(let i = 0; i < len; i++) {
cy.findByTitle(nodeNames[i]).click()
}
return
}
clickOpen(buttonName: string) {
return cy.findByRole("button", {name: buttonName}).click()
}
clickSave() {
return cy.findByRole("button", {name: "Save"}).click()
}
verifySavedMembers(memberNames: string[]) {
let len = memberNames.length
for(let i = 0; i < len; i++) {
cy.findByText(memberNames[i]).should("be.visible")
}
}
getSelectedHierarchy() {
return cy.findByRole("combobox").parent().next()
}
verifyCheckedNodes(nodeNames: string[]) {
let len = nodeNames.length
for(let i = 0; i < len; i++) {
cy.findByTitle(nodeNames[i]).prev().should("have.attr", "class").and("contain", 'checkbox-checked')
}
return
}
verifyUncheckedNodes(nodeNames: string[]) {
let len = nodeNames.length
for(let i = 0; i < len; i++) {
cy.findByTitle(nodeNames[i]).prev().should("have.attr", "class").and("not.contain", 'checkbox-checked')
}
return
}
verifyNodesVisible(nodeNames: string[]) {
let len = nodeNames.length
for(let i = 0; i < len; i++) {
cy.findByTitle(nodeNames[i]).should("be.visible")
}
}
verifyNodesDoNotExist(nodeNames: string[]) {
let len = nodeNames.length
for(let i = 0; i < len; i++) {
cy.findByTitle(nodeNames[i]).should("not.exist")
}
}
clickCancel() {
return cy.findByRole("button", {name: "Cancel"}).click()
}
selectName(selectedName: string) {
cy.findByRole("combobox").click().should("exist")
return cy.findAllByTitle(selectedName).first().click()
}
verifySelectedName(selectedName: string) {
return cy.findByText("Selected Name: " + selectedName)
}
verifySelectedVAlue(selectedValue: string) {
return cy.findByText("Selected Value: " + selectedValue)
}
getMemberList() {
cy.intercept('GET', 'https://schema-api.dev.stratanetwork.net/api/v2/dimensions/Department/hierarchies').as('Hierarchies')
cy.intercept('GET', 'https://schema-api.dev.stratanetwork.net/api/v2/dimensions/Department/members?columns=id&columns=name').as('MemberList')
cy.intercept('GET', 'https://schema-api.dev.stratanetwork.net/api/v2/dimensions/Department').as('DepartmentList')
return
}
getHierarchyNodeList() {
cy.intercept('POST', 'https://schema-api.dev.stratanetwork.net/api/v2/hierarchies/a28d670c-3c10-4be9-bc2d-1148f240c8ee/nodes?hierarchyPath=').as('DepartmentNodeList')
cy.intercept('POST', 'https://schema-api.dev.stratanetwork.net/api/v2/hierarchies/7c671eb2-c014-4daf-90d5-d87f891a6664/nodes?hierarchyPath=').as('CAPDepartmentNodeList')
return
}
clickAllView() {
return cy.findByText("All").click();
}
clickSelectedView() {
return cy.findByText("Selected").click();
}
}
@@ -0,0 +1,6 @@
export default class welcomePage {
getWelcomeMenu() {
return cy.findByRole("menuitem", {name: 'Welcome'})
}
}
@@ -0,0 +1,56 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
const fs = require('fs-extra');
const path = require('path');
const xlsx = require("node-xlsx").default;
function getConfigurationByFile(file: any) {
const pathToConfigFile = path.resolve(
'..',
'e2e/cypress',
`cypress.${file}.json`
);
return fs.readJson(pathToConfigFile);
}
module.exports = (on: any, config:any) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
on('task', {
parseXlsx({ filePath }) {
return new Promise((resolve, reject) => {
try {
const jsonData = xlsx.parse(fs.readFileSync(filePath));
resolve(jsonData);
} catch (e) {
reject(e);
}
});
}
});
if(config.env.fileconfig != null)
{
return getConfigurationByFile(config.env.fileconfig);
} else {
return null;
}
}
@@ -0,0 +1,42 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import '@testing-library/cypress/add-commands';
import 'cypress-file-upload';
/// <reference types="Cypress" />
declare global{
namespace Cypress {
interface Chainable {
logout(): Chainable<Element>
}
}
}
Cypress.Commands.add('logout', () => {
cy.window().then((win) => {
win.sessionStorage.clear()
})
cy.visit('/')
})
@@ -0,0 +1,26 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import { loginCommand } from '@strata/cypress/lib/login'
import { checkTreeCommand, expandTreeCommand, selectTreeCommand } from '@strata/cypress/lib/tree'
loginCommand()
expandTreeCommand()
selectTreeCommand()
checkTreeCommand()
// Alternatively you can use CommonJS syntax:
// require('./commands')
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"noEmit": true,
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "@testing-library/cypress", "node", "cypress-file-upload"]
},
"include": [
"**/*.ts", "support/commands.ts"]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
{
"name": "kitchensink-e2e",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "cypress open",
"test-local": "cypress open --env fileconfig=local",
"test-dev": "cypress open --env fileconfig=development"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@testing-library/cypress": "^8.0.1",
"cypress": "^8.4.0",
"cypress-file-upload": "^5.0.8",
"node-xlsx": "^0.21.0",
"typescript": "^4.4.3"
},
"dependencies": {
"@strata/cypress": "^1.4.0"
}
}
@@ -0,0 +1,28 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vscode/
# cache
.eslintcache
.cache
@@ -0,0 +1,70 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Getting Started
Verify `public/config.js` has the correct values for your application.
Run `npm install`
Run `npm outdated` to see if you have any outdated dependencies. You should update all dependencies to latest by running `npm update`.
**FOLLOW THE GUIDE HERE FOR ESLINT SETUP:** [https://confluence.sdt.local/pages/viewpage.action?pageId=53429096](https://confluence.sdt.local/pages/viewpage.action?pageId=53429096)
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
## Debugger for Chrome
Add the `Debugger for Chrome` extension from the Visual Studio Code extension store.
Now, click on the bug icon on the left pane of Visual Studio Code, find the gear icon near the top right corner of the left side pane that is now open, and click "Add New Configuration".
Choose the `Chrome: Launch` configuration.
In the launch.json file update the port of the url: `"url": "http://localhost:3000"`.
For more information the configuration should match the set up as described in [Configure the Chrome Debugger](https://code.visualstudio.com/docs/nodejs/reactjs-tutorial).
Setting breakpoints should now work in Visual Studio Code with Chrome.
## React Developer Tools
Add the `React Developer Tools` extension from the Chrome web store as described [here](https://reactjs.org/blog/2015/09/02/new-react-developer-tools.html#installation)
You will get two new tabs in your Chrome DevTools: `⚛️ Components` and `⚛️ Profiler`.
Now you can select/inspect the components & view/edit the current props and state.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
{
"name": "kitchensink",
"version": "0.1.0",
"private": true,
"dependencies": {
"@microsoft/signalr": "^5.0.11",
"@strata/core": "^3.1.0",
"@strata/eslint-config": "^3.0.0",
"@strata/jazz": "^0.4.0",
"@strata/navbar": "0.0.40",
"@strata/score": "^2.0.0",
"@strata/signalr": "^0.5.1",
"@strata/tempo": "^5.18.0",
"@strata/ui.signalr": "^0.2.0",
"@types/jest": "^26.0.23",
"@types/node": "^14.17.3",
"@types/react": "^17.0.11",
"@types/react-dom": "^17.0.8",
"@types/react-router": "^5.1.15",
"@types/react-router-dom": "^5.1.7",
"@types/react-syntax-highlighter": "^13.5.2",
"@types/react-test-renderer": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"eslint": "^7.29.0",
"eslint-plugin-flowtype": "^5.7.2",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^2.3.1",
"react": "^17.0.2",
"react-app-polyfill": "^2.0.0",
"react-dom": "^17.0.2",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"react-syntax-highlighter": "^15.4.4",
"react-test-renderer": "^17.0.2",
"react-to-print": "^2.12.6",
"sass": "^1.37.2",
"source-map-explorer": "^2.5.1",
"typescript": "^4.3.4"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"serve": "npx serve -s build",
"start": "react-scripts --max_old_space_size=8192 start",
"build": "react-scripts --max_old_space_size=8192 build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "@strata"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all",
"ie 11"
],
"development": [
">0.2%",
"not dead",
"not op_mini all",
"ie 11"
]
},
"jest": {
"collectCoverageFrom": [
"**/*.{ts,tsx}",
"!**/coverage/**",
"!**/node_modules/**",
"!**/babel.config.js",
"!**/jest/Setup.js"
]
},
"devDependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^13.1.9"
}
}
@@ -0,0 +1,9 @@
window['config'] = {
appName: 'kitchensink',
apiUrl: 'https://localhost:8443/api/',
identityServerUrl: 'https://identity.dev.stratanetwork.net/',
menuUrl: 'https://menu.dev.stratanetwork.net/api/v1/Menu/',
scoreUrl: 'https://schema-api.dev.stratanetwork.net/api/v2/',
env: 'local',
jazzUrl: 'http://localhost/StrataJazz'
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Strata Kitchen Sink App" />
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>KitchenSink</title>
<script src="%PUBLIC_URL%/config.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a kitchensink.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
describe('<App>', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
});
@@ -0,0 +1,35 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { RouteLogger } from '@strata/logging/lib';
import PageLoader from '@strata/tempo/lib/pageloader';
import ErrorBoundary from '@strata/tempo/lib/errorboundary';
import Unload from '@strata/tempo/lib/unload';
import { AuthProvider } from '@strata/core/lib';
import Navigation from './shared/Navigation';
import SignalRHub from './api/signalr-hub/SignalRHub';
import SignalRNotifier from './api/signalr-hub/SignalRNotifier';
import { KeepAlive } from '@strata/jazz/lib';
const App: React.FC = () => {
return (
<ErrorBoundary disableGlobalJavascriptErrorHandling>
<Unload>
<AuthProvider>
<KeepAlive></KeepAlive>
<SignalRHub.Provider>
<BrowserRouter>
<SignalRNotifier>
<RouteLogger />
<PageLoader>
<Navigation />
</PageLoader>
</SignalRNotifier>
</BrowserRouter>
</SignalRHub.Provider>
</AuthProvider>
</Unload>
</ErrorBoundary>
);
};
export default App;
@@ -0,0 +1,171 @@
import React, { useEffect, useState } from 'react';
import Header from '@strata/tempo/lib/header';
import { Example } from '../../shared';
import Button from '@strata/tempo/lib/button';
import Card from '@strata/tempo/lib/card';
import ActionBar from '@strata/tempo/lib/actionbar';
import DataGrid, { IDataGridPageEvent, IDataGridSortEvent } from '@strata/tempo/lib/datagrid';
import Tooltip from '@strata/tempo/lib/tooltip';
import Input from '@strata/tempo/lib/input';
import DropDown, { DropDownValue } from '@strata/tempo/lib/dropdown';
import Modal from '@strata/tempo/lib/modal';
import { departmentsService, newDepartment, IDepartment, DepartmentType, ILoadOptions } from '../../department/data';
import { SortOrder } from '@strata/core/lib/paging';
import DepartmentModal from './DepartmentModal';
const initialLoadOptions: ILoadOptions = {
sortField: 'departmentCode',
sortOrder: SortOrder.Asc,
first: 0,
rows: 5,
search: ''
};
const Crud: React.FC = () => {
const [departments, setDepartments] = useState<IDepartment[]>([]);
const [loading, setLoading] = useState(false);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [loadOptions, setLoadOptions] = useState<ILoadOptions>(initialLoadOptions);
const [departmentToEdit, setDepartmentToEdit] = useState<IDepartment>(newDepartment());
const [isDepartmentModalVisible, setIsDepartmentModalVisible] = useState<boolean>(false);
useEffect(() => {
const loadData = async () => {
setLoading(true);
const data = await departmentsService.getAllDepartments(loadOptions);
setTotalRecords(data.total);
setDepartments(data.data);
setLoading(false);
};
loadData();
}, [loadOptions]);
const reload = () => {
setLoadOptions({ ...loadOptions });
};
const handleAdd = () => {
setDepartmentToEdit(newDepartment());
setIsDepartmentModalVisible(true);
};
const handleSaved = (department: IDepartment) => {
// since we are doing server side paging, we need to reload the current page in case the department moved to a different page after edit
// if we are not doing server side paging, you can reinsert the department into the in-memory list and avoid a full reload
setIsDepartmentModalVisible(false);
reload();
};
const handleEdit = (department: IDepartment) => {
// best practice to clone the object before you edit it to account for cancel
setDepartmentToEdit({ ...department });
setIsDepartmentModalVisible(true);
};
const handleDelete = async (department: IDepartment) => {
Modal.confirm({
title: `Permanently delete ${department.departmentCode} - ${department.description}?`,
okText: 'Delete Department',
cancelText: 'Keep Department',
confirmType: 'danger',
onOk: async () => {
// since we are doing server side paging, we need to reload the current page to pull in the next record that replace the deleted one
// if we are not doing server side paging, simply remove the department from the in-memory list after calling the delete endpoint
await departmentsService.deleteDepartment(department.departmentId);
reload();
}
});
};
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setLoadOptions({ ...loadOptions, ...{ search: e.target.value } });
};
const handleDepartmentTypeChange = (val: DropDownValue) => {
if (val === '' || val == null) {
setLoadOptions((value) => {
// remove departmentType from object to not filter on it
const { departmentType, ...rest } = value;
return rest;
});
} else {
setLoadOptions({ ...loadOptions, ...{ departmentType: val as DepartmentType } });
}
};
return (
<>
<Header title='CRUD operations' subtitle='Example of creating, reading with paging and filtering, updating, and deleting entities'></Header>
<Example uiSourceCodeFilePath='api/crud/Crud.tsx' apiSourceCodeFilePath='Strata.KitchenSink.Api/Controllers/DepartmentsController.cs'>
<Card title='Departments' removeBodyPadding>
<ActionBar
filters={
<>
<Input width={200} search onChange={handleSearch} debounce />
<DropDown
width={160}
placeholder='Department Type'
allowClear
onChange={handleDepartmentTypeChange}
items={[
{ text: DepartmentType[DepartmentType.Excluded], value: DepartmentType.Excluded },
{ text: DepartmentType[DepartmentType.Revenue], value: DepartmentType.Revenue },
{ text: DepartmentType[DepartmentType.Overhead], value: DepartmentType.Overhead }
]}
/>
</>
}
actions={
<>
<Button icon='Plus' onClick={handleAdd}>
Add Department
</Button>
</>
}
/>
<DataGrid
dataKey='id'
loading={loading}
value={departments}
lazy
sortField={loadOptions.sortField}
sortOrder={loadOptions.sortOrder}
paginator
rows={loadOptions.rows}
first={loadOptions.first}
totalRecords={totalRecords}
onSort={(e: IDataGridSortEvent) => setLoadOptions({ ...loadOptions, ...e })}
onPage={(e: IDataGridPageEvent) => setLoadOptions({ ...loadOptions, ...e })}
>
<DataGrid.RowNumber />
<DataGrid.Column field='departmentCode' header='Department Code' sortable width={120} />
<DataGrid.Column field='description' header='Description' sortable width={200} />
<DataGrid.Column field='departmentType' header='Department Type' sortable width={120} />
<DataGrid.EmptyColumn />
<DataGrid.Column
align='right'
width={80}
body={(department: IDepartment) => (
<>
<Tooltip title='Edit'>
<Button type='link' icon='Edit' onClick={() => handleEdit(department)} />
</Tooltip>
<Tooltip title='Delete'>
<Button type='link' icon='Delete' onClick={() => handleDelete(department)} />
</Tooltip>
</>
)}
/>
</DataGrid>
</Card>
</Example>
<DepartmentModal department={departmentToEdit} isVisible={isDepartmentModalVisible} onCancel={() => setIsDepartmentModalVisible(false)} onSave={handleSaved}></DepartmentModal>
</>
);
};
export default Crud;
@@ -0,0 +1,59 @@
import React, { useEffect, useState } from 'react';
import Modal from '@strata/tempo/lib/modal';
import Form from '@strata/tempo/lib/form';
import Input from '@strata/tempo/lib/input';
import DropDown from '@strata/tempo/lib/dropdown';
import { departmentsService, IDepartment, DepartmentType } from '../../department/data';
export interface IDepartmentModalProps {
department: IDepartment;
isVisible: boolean;
onCancel: () => void;
onSave: (department: IDepartment) => void;
}
const DepartmentModal: React.FC<IDepartmentModalProps> = (props: IDepartmentModalProps) => {
const { department, isVisible, onCancel, onSave } = props;
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(false);
// this useEffect ensures the form is reset when department changes
useEffect(() => {
form.setFieldsValue(department);
}, [department, form]);
const handleSave = async () => {
setLoading(true);
try {
const values = await form.validateFields();
const savedDepartment = await departmentsService.saveDepartment({ ...department, ...values });
onSave(savedDepartment);
} finally {
setLoading(false);
}
};
return (
<Modal title='Save Department' visible={isVisible} loading={loading} onCancel={onCancel} onOk={handleSave} destroyOnClose>
<Form form={form}>
<Form.Item label='Department Code' name='departmentCode' rules={[{ required: true, whitespace: true }]}>
<Input />
</Form.Item>
<Form.Item label='Description' name='description' rules={[{ required: true, whitespace: true }]}>
<Input />
</Form.Item>
<Form.Item label='Department Type' name='departmentType'>
<DropDown
items={[
{ text: DepartmentType[DepartmentType.Excluded], value: DepartmentType.Excluded },
{ text: DepartmentType[DepartmentType.Revenue], value: DepartmentType.Revenue },
{ text: DepartmentType[DepartmentType.Overhead], value: DepartmentType.Overhead }
]}
/>
</Form.Item>
</Form>
</Modal>
);
};
export default DepartmentModal;
@@ -0,0 +1,23 @@
import React from 'react';
import ApiError from './examples/ApiError';
import ReactError from './examples/ReactError';
import Overview from './examples/Overview';
import CustomError from './examples/CustomError';
import TabRoutes, { ITabRoute } from '../../shared/TabRoutes';
const ErrorHandling: React.FC = () => {
const routes: ITabRoute[] = [
{ name: 'Overview', route: 'overview', component: <Overview /> },
{ name: 'React Errors', route: 'react-errors', component: <ReactError /> },
{ name: 'API Errors', route: 'api-errors', component: <ApiError /> },
{ name: 'Custom Errors', route: 'custom-errors', component: <CustomError /> }
];
return (
<>
<TabRoutes tabRoutes={routes} />
</>
);
};
export default ErrorHandling;
@@ -0,0 +1,17 @@
import { appConfig, getSecureService } from '@strata/core/lib';
const { httpGet } = getSecureService(appConfig.apiUrl + 'v1/errors');
export const errorsService = {
throw404: async (): Promise<void> => {
return await httpGet('/404');
},
throw403: async (): Promise<void> => {
return await httpGet('/403');
},
throw500: async (): Promise<void> => {
return await httpGet('/500');
}
};
@@ -0,0 +1,36 @@
import React from 'react';
import Card from '@strata/tempo/lib/card';
import Text from '@strata/tempo/lib/text';
import Button from '@strata/tempo/lib/button';
import Spacing from '@strata/tempo/lib/spacing';
import Example from '../../../shared/Example';
import { errorsService } from '../data/errorsService';
const ApiError: React.FC = () => {
return (
<Example uiSourceCodeFilePath='api/error-handling/examples/ApiError.tsx' apiSourceCodeFilePath='Strata.KitchenSink.Api/Controllers/ErrorsController.cs'>
<Card title='403 Access Denied'>
<Text>
403 errors usually result from lack of permission to a resource or perform an action. By default, 403 error will result in the current content replaced completely by a 403 message.
</Text>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Button onClick={async () => await errorsService.throw403()}>Throw 403</Button>
</Spacing>
</Card>
<Card title='404 Not Found'>
<Text>404 errors usually result from missing resource. By default, 404 error will result in the current content replaced completely by a 404 message.</Text>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Button onClick={async () => await errorsService.throw404()}>Throw 404</Button>
</Spacing>
</Card>
<Card title='Other API Errors'>
<Text>Non 403 / 404 errors from the server will be treated as a generic server error and will result in a Modal alert. Current content will NOT be replaced.</Text>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Button onClick={async () => await errorsService.throw500()}>Throw 500</Button>
</Spacing>
</Card>
</Example>
);
};
export default ApiError;
@@ -0,0 +1,74 @@
/* eslint-disable no-throw-literal */
import React from 'react';
import Card from '@strata/tempo/lib/card';
import Text from '@strata/tempo/lib/text';
import Button from '@strata/tempo/lib/button';
import Spacing from '@strata/tempo/lib/spacing';
import Example from '../../../shared/Example';
import { errorsService } from '../data/errorsService';
import Modal from '@strata/tempo/lib/modal';
import { CodeFormatter } from '../../../shared';
const CustomError: React.FC = () => {
return (
<Example uiSourceCodeFilePath='api/error-handling/examples/CustomError.tsx' apiSourceCodeFilePath='Strata.KitchenSink.Api/Controllers/ErrorsController.cs'>
<Card title='Try Catch'>
<Text>In most cases, you should simply wrap your code in try...catch to provide custom error messages, especially when calling APIs.</Text>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Button
onClick={async () => {
try {
await errorsService.throw500();
} catch (e) {
let msg = 'This is the default custom error message';
if (e instanceof Response) {
// if the server is sending back a more complex object, you can create an interface to map to the response
const errObj = await (e as Response).json();
if (errObj.message) {
msg = errObj.message;
}
}
// show custom message or change state or do whatever you want
Modal.alert({
title: 'Custom Error Alert',
alertType: 'error',
content: msg
});
}
}}
>
Throw 500
</Button>
</Spacing>
</Card>
<Card title='Global Custom Error Handling'>
<Text>
You can override the behavior of ErrorBoundary with your own custom error handling. Simply handle ErrorBoundary onError event and call preventDefault(). This will suppress any error handling
by ErrorBoundary and you can now do whatever you want. However, this does not apply to React rendering errors since that will always be replaced with an error message to avoid infinite
loops. Keep in mind this override is global to your app and all errors.
</Text>
<CodeFormatter language='jsx'>{`const handleGlobalError = (e: ErrorBoundaryEvent) => {
e.preventDefault();
// show a modal or toast or whatever you want
};
<ErrorBoundary onError={handleGlobalError}>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Button
onClick={() => {
throw 'custom error';
}}
>
Throw Custom Error
</Button>
</Spacing>
</ErrorBoundary>`}</CodeFormatter>
</Card>
</Example>
);
};
export default CustomError;
@@ -0,0 +1,69 @@
import React from 'react';
import Card from '@strata/tempo/lib/card';
import Text from '@strata/tempo/lib/text';
import Spacing from '@strata/tempo/lib/spacing';
import { CodeFormatter, Example } from '../../../shared';
const Overview: React.FC = () => {
return (
<Example
uiSourceCodeFilePath='api/error-handling/examples/Overview.tsx'
additionSourceCodeLinks={[
{
name: 'Content ErrorBoundary',
href: 'https://github.com/stratadecision/kitchensink/blob/main/src/Strata.KitchenSink.Web/kitchensink/src/shared/Navigation.tsx'
},
{
name: 'Outer ErrorBoundary',
href: 'https://github.com/stratadecision/kitchensink/blob/main/src/Strata.KitchenSink.Web/kitchensink/src/App.tsx'
}
]}
>
<Card title='ErrorBoundary'>
<Spacing column itemSpacing={12}>
<Text>The default ErrorBoundary from React only catches React component tree errors. Tempo ErrorBoundary is customized to catch unhandled Javascript errors.</Text>
<Text>
Your app should have at least 2 ErrorBoundaries.
<ul>
<li>
Content ErrorBoundary - Wraps the page content. Usually placed in Navigation.tsx file around your routes. When an error happens on your page, navigation components (navbar, side nav,
tabs) will still be shown. Make sure to set the key to the activeUrlKey for your navigation component so when a route changes, ErrorBoundary will reset its state and remove any error
message.
<CodeFormatter language='jsx'>{`<Layout.Content>
<ErrorBoundary key={activeUrlKey + '-errorboundary'}>
<Switch>
<Route...></Route>
...
</Switch>
</ErrorBoundary>
</Layout.Content>`}</CodeFormatter>
</li>
<li>
Outer ErrorBoundary - Wraps the entire app to catch any React errors not caught by the Content ErrorBoundary. Make sure to set disableGlobalJavascriptErrorHandling since the Content
ErrorBoundary is already catching those errors. There should only be ONE ErrorBoundary that catches Javascript errors.
<CodeFormatter language='jsx'>{`const App: React.FC = () => {
return (
<ErrorBoundary disableGlobalJavascriptErrorHandling>
<Unload>
<AuthProvider>
<BrowserRouter>
<RouteLogger />
<PageLoader>
<Navigation />
</PageLoader>
</BrowserRouter>
</AuthProvider>
</Unload>
</ErrorBoundary>
);
};`}</CodeFormatter>
</li>
</ul>
</Text>
</Spacing>
</Card>
</Example>
);
};
export default Overview;
@@ -0,0 +1,46 @@
/* eslint-disable no-throw-literal */
import React, { useState } from 'react';
import Card from '@strata/tempo/lib/card';
import Text from '@strata/tempo/lib/text';
import Button from '@strata/tempo/lib/button';
import Spacing from '@strata/tempo/lib/spacing';
import Example from '../../../shared/Example';
const BrokenComponent = () => {
throw 'Broken Component';
};
const ReactError: React.FC = () => {
const [errorVisible, setErrorVisible] = useState(false);
return (
<Example uiSourceCodeFilePath='api/error-handling/examples/ReactError.tsx'>
<Card title='React Rendering Error'>
<Text>
When an error occurs during React component rendering, the React app can become unstable and can cause infinite loop. So when React error occurs, the page content will be replaced by a
generic error message.
</Text>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Button onClick={() => setErrorVisible(true)}>Throw Rendering Error</Button>
</Spacing>
</Card>
<Card title='React Event Error'>
<Text>
When an error occurs during event handler (button onclick), the React app is already rendered so only an alert will be displayed. There is no need to prevent rest of React component tree
from rendering.
</Text>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Button
onClick={() => {
throw 'React Event Error';
}}
>
Throw Event Error
</Button>
</Spacing>
</Card>
{errorVisible && <BrokenComponent></BrokenComponent>}
</Example>
);
};
export default ReactError;
@@ -0,0 +1,103 @@
import React, { useState } from 'react';
import Header from '@strata/tempo/lib/header';
import { CodeFormatter, Example } from '../../shared';
import Button from '@strata/tempo/lib/button';
import Card from '@strata/tempo/lib/card';
import Spacing from '@strata/tempo/lib/spacing';
import Text from '@strata/tempo/lib/text';
import UploadButton, { UploadChangeParam } from '@strata/tempo/lib/uploadbutton';
import { departmentsService } from '../../department/data/departmentsService';
import Link from '@strata/tempo/lib/link';
import { usePageLoader } from '@strata/tempo/lib/pageloader';
import Toast from '@strata/tempo/lib/toast';
import Checkbox, { CheckboxChangeEvent } from '@strata/tempo/lib/checkbox';
const ImportExport: React.FC = () => {
const { setLoading } = usePageLoader();
const [withCoverPage, setWithCoverPage] = useState<boolean>(false);
const handleExport = () => {
setLoading(true);
try {
departmentsService.exportDepartments(withCoverPage);
} finally {
setLoading(false);
}
};
const handleImport = async (info: UploadChangeParam) => {
setLoading(true);
try {
const result = await departmentsService.importDepartments(info.file as unknown as File);
Toast.show({
toastType: 'success',
message: `${result.newDepartmentsCount} departments added. ${result.updatedDepartmentsCount} departments updated.`
});
} finally {
setLoading(false);
}
};
return (
<>
<Header title='Import Export' subtitle='Import and export Excel files'></Header>
<Example uiSourceCodeFilePath='api/import-export/ImportExport.tsx' apiSourceCodeFilePath='Strata.KitchenSink.Api/Controllers/DepartmentsController.cs'>
<Card title='ClosedXML via Strata.Excel.Core nuget package'>
<Text>
<Link href='https://github.com/closedxml/closedxml' newTab>
ClosedXML
</Link>{' '}
is a free library to create and read Excel files. It uses System.Drawing, which is available on Windows machine but not in Linux machine. After you install the nuget package, you have to
update your Dockerfile.api file with snippet below to download System.Drawing for the build images. You have to copy the snippet TWICE in your file. You can reference the KitchenSink{' '}
<Link href='https://github.com/stratadecision/kitchensink/blob/main/Dockerfile.api' newTab>
Dockerfile.api.
</Link>{' '}
<Link href='https://github.com/stratadecision/excel.core' newTab>
Strata.Excel.Core
</Link>{' '}
provides easy to use helper functions for creating a worksheet from a list of objects, and importing a list of objects from an Excel Worksheet.
</Text>
<CodeFormatter language='jsx'>
{`RUN apt-get update --allow-releaseinfo-change
&& apt-get install -y
libc6-dev
libgdiplus
libx11-dev
&& rm -rf /var/lib/apt/lists/*`}
</CodeFormatter>
</Card>
<Card title='Export'>
<Text>
Export Excel file from server using{' '}
<Link href='https://github.com/closedxml/closedxml' newTab>
ClosedXML
</Link>{' '}
and{' '}
<Link href='https://github.com/stratadecision/excel.core' newTab>
Strata.Excel.Core
</Link>{' '}
libraries.
</Text>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Checkbox onChange={(e: CheckboxChangeEvent) => setWithCoverPage(e.target.checked)}>With CoverPage</Checkbox>
<Button onClick={handleExport}>Export</Button>
</Spacing>
</Card>
<Card title='Import'>
<Text>
Import Excel file using Tempo UploadButton and{' '}
<Link href='https://github.com/stratadecision/excel.core' newTab>
Strata.Excel.Core
</Link>{' '}
libraries.
</Text>
<Spacing hAlign='center' margin='16px 0 0 0'>
<UploadButton accept='.xlsx' onChange={handleImport} buttonText='Import'></UploadButton>
</Spacing>
</Card>
</Example>
</>
);
};
export default ImportExport;
@@ -0,0 +1,32 @@
import React from 'react';
import Header from '@strata/tempo/lib/header';
import Tabs from '@strata/tempo/lib/tabs';
import { Switch, Route, useRouteMatch } from 'react-router-dom';
import Overview from './examples/Overview';
import Simulate from './examples/Simulate';
const SignalRHub: React.FC = () => {
const { path, url } = useRouteMatch();
return (
<>
<Header title='SignalR Hub' subtitle='Handle Data Polling from SignalR'></Header>
<Tabs>
<Tabs.TabPane key='1' tab='Overview' href={url}></Tabs.TabPane>
<Tabs.TabPane key='2' tab='Simulate' href={url + '/simulate'}></Tabs.TabPane>
</Tabs>
<Switch>
<Route exact path={path}>
<Overview />
</Route>
<Route path={`${path}/simulate`}>
<Simulate />
</Route>
</Switch>
</>
);
};
export default SignalRHub;
@@ -0,0 +1,32 @@
import { createHubContext, IConnectionStatus } from '@strata/signalr/lib';
import { appConfig } from '@strata/core/lib';
import { Toast } from '@strata/tempo';
// Need to subscribe to the right event and react to them from API
// Example SendStatus API method here for reference
// Task SendStatus(string status, string objectId, string payload);
export const hubEvents = {
SendStatus: 'SendStatus'
};
const hubContext = createHubContext('SignalRHub', appConfig.apiUrl);
const hub = {
...hubContext,
// These are used to register and remove new groups for messages
register: (connection: IConnectionStatus, successHandler: () => void, group: string): void => {
connection.connection?.send('register', group).then(() => {
Toast.show({ duration: 30, message: `Added connection to group ${group}`, toastType: 'success' });
successHandler();
});
},
remove: (connection: IConnectionStatus, successHandler: () => void, group: string): void => {
connection.connection?.send('remove', group).then(() => {
Toast.show({ duration: 30, message: `Removed connection to group ${group}`, toastType: 'success' });
successHandler();
});
}
};
export default hub;
@@ -0,0 +1,17 @@
import React from 'react';
import signalRHub, { hubEvents } from './SignalRHub';
import Toast from '@strata/tempo/lib/toast';
import useHubEvent from '@strata/signalr/lib/SignalRHub/useHubEvent';
const SignalRNotifier: React.FC = ({ children }) => {
const hub = signalRHub.useConnection();
useHubEvent(hub, hubEvents.SendStatus, (status: 'info' | 'success' | 'error', _id: string, payloadRaw: string) => {
const payload = JSON.parse(payloadRaw);
Toast.show({ duration: 30, message: payload, toastType: status });
});
return <>{children}</>;
};
export default SignalRNotifier;
@@ -0,0 +1,37 @@
import { appConfig, getSecureService } from '@strata/core/lib';
const { httpGet } = getSecureService(appConfig.apiUrl + 'v1/signalr-hub');
export const signalRHubService = {
info: async (payload: string | undefined = undefined): Promise<void> => {
return await httpGet(`/info?payload=${payload}`);
},
success: async (payload: string | undefined = undefined): Promise<void> => {
return await httpGet(`/success?payload=${payload}`);
},
failure: async (payload: string | undefined = undefined): Promise<void> => {
return await httpGet(`/failure?payload=${payload}`);
},
status: async (payload: string | undefined = undefined): Promise<void> => {
return await httpGet(`/status?payload=${payload}`);
},
infotogroup: async (group: string, payload: string | undefined = undefined): Promise<void> => {
return await httpGet(`/infotogroup?group=${group}&payload=${payload}`);
},
successtogroup: async (group: string, payload: string | undefined = undefined): Promise<void> => {
return await httpGet(`/successtogroup?group=${group}&payload=${payload}`);
},
failuretogroupwithpayload: async (group: string, payload: string | undefined = undefined): Promise<void> => {
return await httpGet(`/failuretogroupwithpayload?group=${group}&payload=${payload}`);
},
failuretogroupwithexception: async (group: string, payload: string | undefined = undefined): Promise<void> => {
return await httpGet(`/failuretogroupwithexception?group=${group}&payload=${payload}`);
}
};
@@ -0,0 +1,43 @@
import React from 'react';
import Card from '@strata/tempo/lib/card';
import Text from '@strata/tempo/lib/text';
import Spacing from '@strata/tempo/lib/spacing';
import { CodeFormatter, Example } from '../../../shared';
const Overview: React.FC = () => {
return (
<Example
uiSourceCodeFilePath='api/signalr-hub/examples/Overview.tsx'
additionSourceCodeLinks={[
{
name: 'Content',
href: 'https://github.com/stratadecision/kitchensink/blob/main/src/Strata.KitchenSink.Web/kitchensink/src/App.tsx'
}
]}
>
<Card title='Hub Notifier'>
<Spacing column itemSpacing={12}>
<Text>Wrap your application with the HubProvider and HubNotifier.</Text>
<CodeFormatter language='jsx'>{` <ErrorBoundary disableGlobalJavascriptErrorHandling>
<Unload>
<AuthProvider>
<SignalRHub.Provider>
<BrowserRouter>
<SignalRNotifier>
<RouteLogger />
<PageLoader>
<Navigation />
</PageLoader>
</SignalRNotifier>
</BrowserRouter>
</SignalRHub.Provider>
</AuthProvider>
</Unload>
</ErrorBoundary>`}</CodeFormatter>
</Spacing>
</Card>
</Example>
);
};
export default Overview;
@@ -0,0 +1,118 @@
import React, { useState } from 'react';
import Card from '@strata/tempo/lib/card';
import Button from '@strata/tempo/lib/button';
import Spacing from '@strata/tempo/lib/spacing';
import Text from '@strata/tempo/lib/text';
import Input from '@strata/tempo/lib/input';
import Example from '../../../shared/Example';
import List from '@strata/tempo/lib/list';
import { signalRHubService } from '../data/signalRHubService';
import signalRHub from '../SignalRHub';
const Simulate: React.FC = () => {
const [customPushPayload, setCustomPushPayload] = useState<string>('');
const [customGroupToAdd, setCustomGroupToAdd] = useState<string>('');
const [customGroupToRemove, setCustomGroupToRemove] = useState<string>('');
const [customGroupToPush, setCustomGroupToPush] = useState<string>('');
const [customGroups, setCustomGroups] = useState<string[]>([]);
const [customGroupPushPayload, setCustomGroupPushPayload] = useState<string>('');
const hub = signalRHub.useConnection();
const registerGroupSuccessHandler = () => {
setCustomGroups((groups) => {
return [...groups, customGroupToAdd];
});
};
const removeGroupSuccessHandler = () => {
setCustomGroups((groups) => {
return groups.filter((group) => group !== customGroupToRemove);
});
};
return (
<Example uiSourceCodeFilePath='api/signalr-hub/examples/Simulate.tsx' apiSourceCodeFilePath='Strata.KitchenSink.Api/Controllers/SignalRHubController.cs'>
<Card title='Simulate Push'>
<Spacing hAlign='center' margin='16px 0 0 0' itemSpacing={16}>
<Text strong>Custom Payload</Text>
<Input width={216} placeholder={'Enter custom payload'} onChange={(e) => setCustomPushPayload(e.target.value)} />
</Spacing>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Button onClick={async () => await signalRHubService.info(customPushPayload)}>Simulate Info Push</Button>
<Button onClick={async () => await signalRHubService.success(customPushPayload)}>Simulate Success Push</Button>
<Button onClick={async () => await signalRHubService.failure(customPushPayload)}>Simulate Failure Push</Button>
<Button onClick={async () => await signalRHubService.status(customPushPayload)}>Simulate Status Push</Button>
</Spacing>
</Card>
<Card title='Add current connection to group'>
<Spacing hAlign='center' margin='16px 0 0 0' itemSpacing={16}>
<Text strong>Custom Group</Text>
<Input width={216} placeholder={'Enter custom group name here'} onChange={(e) => setCustomGroupToAdd(e.target.value)} />
<Button
disabled={customGroupToAdd.trim().length === 0}
onClick={async () => {
signalRHub.register(hub, registerGroupSuccessHandler, customGroupToAdd);
}}
>
{`Add connection to custom group ${customGroupToAdd}`}
</Button>
</Spacing>
</Card>
<Card title='Remove current connection from group'>
<Spacing hAlign='center' margin='16px 0 0 0' itemSpacing={16}>
<Text strong>Custom Group</Text>
<Input width={216} placeholder={'Enter custom group name here'} onChange={(e) => setCustomGroupToRemove(e.target.value)} />
<Button
disabled={customGroupToRemove.trim().length === 0}
onClick={async () => {
signalRHub.remove(hub, removeGroupSuccessHandler, customGroupToRemove);
}}
>
{`Remove connection from custom group ${customGroupToRemove}`}
</Button>
</Spacing>
</Card>
<Card title={`Simulate Push to Group ${customGroupToPush}`}>
{customGroups.length > 0 && (
<Spacing hAlign='center' margin='16px 0 0 0' itemSpacing={16}>
<Text strong>Groups Registered</Text>
<List
dataSource={customGroups.map((group) => {
return { title: group };
})}
renderItem={(item) => <List.Item>{item.title}</List.Item>}
></List>
</Spacing>
)}
<Spacing hAlign='center' margin='16px 0 0 0' itemSpacing={16}>
<Text strong>Custom Group</Text>
<Input width={216} placeholder={'Enter custom group name here'} onChange={(e) => setCustomGroupToPush(e.target.value)} />
</Spacing>
<Spacing hAlign='center' margin='16px 0 0 0' itemSpacing={16}>
<Text strong>Custom Payload</Text>
<Input width={216} placeholder={'Enter custom payload'} onChange={(e) => setCustomGroupPushPayload(e.target.value)} />
</Spacing>
<Spacing hAlign='center' margin='16px 0 0 0'>
<Button
disabled={customGroupToPush.trim().length === 0}
onClick={async () => await signalRHubService.infotogroup(customGroupToPush, customGroupPushPayload)}
>{`Simulate Info to Group Push`}</Button>
<Button
disabled={customGroupToPush.trim().length === 0}
onClick={async () => await signalRHubService.successtogroup(customGroupToPush, customGroupPushPayload)}
>{`Simulate Success to Group Push`}</Button>
<Button
disabled={customGroupToPush.trim().length === 0}
onClick={async () => await signalRHubService.failuretogroupwithpayload(customGroupToPush, customGroupPushPayload)}
>{`Simulate Failure to Group with Payload Push`}</Button>
<Button
disabled={customGroupToPush.trim().length === 0}
onClick={async () => await signalRHubService.failuretogroupwithexception(customGroupToPush, customGroupPushPayload)}
>{`Simulate Failure to Group with Exception Push`}</Button>
</Spacing>
</Card>
</Example>
);
};
export default Simulate;
@@ -0,0 +1,5 @@
export enum DepartmentType {
Excluded = 0,
Overhead = 1,
Revenue = 2
}
@@ -0,0 +1,17 @@
import { DepartmentType } from './DepartmentType';
export interface IDepartment {
departmentId: number;
departmentCode: string;
description: string;
departmentType: DepartmentType;
}
export const newDepartment = (): IDepartment => {
return {
departmentId: 0,
departmentCode: '',
description: '',
departmentType: DepartmentType.Excluded
};
};
@@ -0,0 +1,4 @@
export interface IDepartmentImportResult {
newDepartmentsCount: number;
updatedDepartmentsCount: number;
}
@@ -0,0 +1,7 @@
import { IPagingOptions } from '@strata/core/lib/paging';
import { DepartmentType } from './DepartmentType';
export interface ILoadOptions extends IPagingOptions {
search: string;
departmentType?: DepartmentType;
}
@@ -0,0 +1,39 @@
import { appConfig, getSecureService } from '@strata/core/lib';
import { IDepartment } from './IDepartment';
import { IDepartmentImportResult } from './IDepartmentImportResult';
import { IPagedData } from '@strata/core/lib/paging';
import { getQueryString } from '@strata/core/lib/http';
import { ILoadOptions } from './ILoadOptions';
const { httpGet, httpPost, httpDelete, httpUpload, httpDownloadAndDisplayFile } = getSecureService(appConfig.apiUrl + 'v1/departments');
export const departmentsService = {
getDepartment: (id: number): Promise<IDepartment> => {
return httpGet(`/${id}`);
},
getAllDepartments: (loadOptions: ILoadOptions): Promise<IPagedData<IDepartment>> => {
return httpGet(`${getQueryString(loadOptions, true)}`);
},
saveDepartment: (department: IDepartment): Promise<IDepartment> => {
return httpPost('', department);
},
deleteDepartment: (id: number): Promise<void> => {
return httpDelete(`/${id}`);
},
importDepartments: (file: File): Promise<IDepartmentImportResult> => {
const formData = new FormData();
formData.append('file', file);
return httpUpload(`/import`, formData);
},
exportDepartments: (withCoverPage: boolean): void => {
const date = new Date();
const fileName = `Departments_Export_${withCoverPage ? 'WithCoverPage_' : ''}${date.toISOString().split('T')[0]}.xlsx`;
httpDownloadAndDisplayFile(`/export?withCoverPage=${withCoverPage}`, undefined, fileName);
}
};

Some files were not shown because too many files have changed in this diff Show More