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:
@@ -0,0 +1,10 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
Dockerfile.local
|
||||
.env
|
||||
.gitignore
|
||||
.vs
|
||||
.vscode
|
||||
**/bin
|
||||
**/obj
|
||||
**/.toolstarget
|
||||
+85
-217
@@ -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 they’re 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 #
|
||||
############
|
||||
# it’s 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
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 they’re 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 #
|
||||
############
|
||||
# it’s 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
|
||||
@@ -0,0 +1,38 @@
|
||||
# Strata.KitchenSink
|
||||
|
||||
### SonarQube Metrics
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
|
||||
[](https://sonarqube.sdt.local/dashboard?id=Strata.KitchenSink)
|
||||
[](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"]
|
||||
}
|
||||
+1798
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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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.
|
||||
+18744
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;
|
||||
+74
@@ -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
Reference in New Issue
Block a user