├── GhostUI ├── ClientApp │ ├── .browserslistrc │ ├── src │ │ ├── views │ │ │ ├── Form │ │ │ │ ├── index.ts │ │ │ │ └── Form.vue │ │ │ ├── Login │ │ │ │ ├── index.ts │ │ │ │ ├── child-components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── RememberMeInput.vue │ │ │ │ │ ├── UserNameInput.vue │ │ │ │ │ └── PasswordInput.vue │ │ │ │ └── Login.vue │ │ │ ├── Dashboard │ │ │ │ ├── index.ts │ │ │ │ └── Dashboard.vue │ │ │ ├── FetchData │ │ │ │ ├── index.ts │ │ │ │ ├── child-components │ │ │ │ │ ├── index.ts │ │ │ │ │ └── ForecastTable.vue │ │ │ │ └── FetchData.vue │ │ │ └── index.ts │ │ ├── event-bus.ts │ │ ├── shims-vue.d.ts │ │ ├── plugins │ │ │ ├── index.ts │ │ │ └── vue-click-outside.ts │ │ ├── shims-svg.d.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ └── isArrayWithLength.ts │ │ ├── store │ │ │ ├── modules │ │ │ │ ├── form │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── form.module.ts │ │ │ │ ├── weather-forecasts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── weather-forecasts.module.ts │ │ │ │ └── auth │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── auth.module.ts │ │ │ └── index.ts │ │ ├── api │ │ │ ├── index.ts │ │ │ ├── base.service.ts │ │ │ ├── sample.service.ts │ │ │ ├── auth.service.ts │ │ │ └── signalr.service.ts │ │ ├── config │ │ │ ├── index.ts │ │ │ ├── signalr.config.ts │ │ │ ├── vue-snotify.config.ts │ │ │ ├── constants.ts │ │ │ ├── fa.config.ts │ │ │ └── axios.config.ts │ │ ├── assets │ │ │ ├── img │ │ │ │ ├── based-ghost-main.png │ │ │ │ ├── VueCore.svg │ │ │ │ └── BulmaLogo.svg │ │ │ └── style │ │ │ │ └── scss │ │ │ │ ├── main.scss │ │ │ │ ├── scoped │ │ │ │ ├── spinner.scss │ │ │ │ ├── navbar.scss │ │ │ │ ├── authenticator.scss │ │ │ │ └── settings.scss │ │ │ │ ├── components │ │ │ │ ├── footer.scss │ │ │ │ └── dropdown.scss │ │ │ │ └── base │ │ │ │ ├── variables.scss │ │ │ │ ├── transitions.scss │ │ │ │ ├── tool-tip.scss │ │ │ │ └── generic.scss │ │ ├── shims-snotify.d.ts │ │ ├── shims-tsx.d.ts │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── Spinner.vue │ │ │ ├── Footer.vue │ │ │ ├── Authenticator.vue │ │ │ ├── Navbar.vue │ │ │ ├── Settings.vue │ │ │ ├── VDropdown.render.tsx │ │ │ └── VCheckbox.render.tsx │ │ ├── App.vue │ │ ├── registerServiceWorker.ts │ │ ├── main.ts │ │ └── router │ │ │ └── index.ts │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── img │ │ │ └── icons │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── mstile-150x150.png │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon-60x60.png │ │ │ │ ├── apple-touch-icon-76x76.png │ │ │ │ ├── apple-touch-icon-120x120.png │ │ │ │ ├── apple-touch-icon-152x152.png │ │ │ │ ├── apple-touch-icon-180x180.png │ │ │ │ ├── msapplication-icon-144x144.png │ │ │ │ └── safari-pinned-tab.svg │ │ ├── manifest.json │ │ └── index.html │ ├── babel.config.js │ ├── postcss.config.js │ ├── tests │ │ ├── e2e │ │ │ ├── specs │ │ │ │ └── test.js │ │ │ └── custom-assertions │ │ │ │ └── elementCount.js │ │ └── unit │ │ │ ├── Spinner.spec.ts │ │ │ └── VCheckbox.spec.ts │ ├── .eslintrc.js │ ├── jest.config.js │ ├── tsconfig.json │ ├── vue.config.js │ └── package.json ├── Pages │ ├── _ViewImports.cshtml │ ├── Error.cshtml.cs │ └── Error.cshtml ├── HealthChecks │ └── GCInfo │ │ ├── IGCInfoOptions.cs │ │ ├── GCInfoOptions.cs │ │ └── GCInfoHealthCheck.cs ├── appsettings.Development.json ├── Models │ ├── IAuthUser.cs │ ├── ICredentials.cs │ ├── Credentials.cs │ ├── IWeatherForecast.cs │ ├── AuthUser.cs │ ├── WeatherForecast.cs │ └── ExceptionDetails.cs ├── Hubs │ ├── IUsersHub.cs │ └── UsersHub.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.json ├── Extensions │ ├── HealthCheckBuilderExtensions.cs │ └── ServiceCollectionExtensions.cs ├── Controllers │ ├── SampleDataController.cs │ └── AuthController.cs ├── nswag.json ├── GhostUI.csproj ├── wwwroot │ └── docs │ │ └── api-specification.json └── Startup.cs ├── LICENSE ├── solution.sln ├── .gitignore └── README.md /GhostUI/ClientApp/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Form/index.ts: -------------------------------------------------------------------------------- 1 | import Form from './Form.vue'; 2 | 3 | export { Form }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/event-bus.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export const EventBus = new Vue(); -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Login/index.ts: -------------------------------------------------------------------------------- 1 | import Login from './Login.vue'; 2 | 3 | export { Login }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Dashboard/index.ts: -------------------------------------------------------------------------------- 1 | import Dashboard from './Dashboard.vue'; 2 | 3 | export { Dashboard }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/FetchData/index.ts: -------------------------------------------------------------------------------- 1 | import FetchData from './FetchData.vue'; 2 | 3 | export { FetchData }; -------------------------------------------------------------------------------- /GhostUI/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @namespace GhostUI.Pages 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import vClickOutside from './vue-click-outside'; 2 | 3 | export { 4 | vClickOutside 5 | }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/shims-svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg?inline' { 2 | const content: any; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import isArrayWithLength from './isArrayWithLength'; 2 | 3 | export { 4 | isArrayWithLength 5 | }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/FetchData/child-components/index.ts: -------------------------------------------------------------------------------- 1 | import ForecastTable from './ForecastTable.vue'; 2 | 3 | export { ForecastTable }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/favicon.ico -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/store/modules/form/index.ts: -------------------------------------------------------------------------------- 1 | export { FormModule } from './form.module'; 2 | 3 | export type { IDropdownOption, IFormState } from './types'; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthApi } from './auth.service'; 2 | export { SampleApi } from './sample.service'; 3 | export { SignalRApi } from './signalr.service'; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './axios.config'; 3 | export * from './signalr.config'; 4 | export * from './vue-snotify.config'; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/utils/isArrayWithLength.ts: -------------------------------------------------------------------------------- 1 | const isArrayWithLength = (val: unknown): boolean => Array.isArray(val) && !!val.length; 2 | 3 | export default isArrayWithLength; -------------------------------------------------------------------------------- /GhostUI/HealthChecks/GCInfo/IGCInfoOptions.cs: -------------------------------------------------------------------------------- 1 | namespace GhostUI.HealthChecks 2 | { 3 | public interface IGCInfoOptions 4 | { 5 | long Threshold { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/img/based-ghost-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/src/assets/img/based-ghost-main.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/shims-snotify.d.ts: -------------------------------------------------------------------------------- 1 | import { SnotifyService } from 'vue-snotify/SnotifyService' 2 | 3 | declare module 'vue/types/vue' { 4 | interface Vue { 5 | $snotify: SnotifyService 6 | } 7 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/store/modules/weather-forecasts/index.ts: -------------------------------------------------------------------------------- 1 | export { WeatherForecastModule } from './weather-forecasts.module'; 2 | 3 | export type { IWeatherForecast, IWeatherForecastsState } from './types'; -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/based-ghost/aspnet-core-vue-vuex-playground-template/HEAD/GhostUI/ClientApp/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/store/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthModule } from './auth.module'; 2 | export { AuthStatusEnum } from './types'; 3 | 4 | export type { IAuthUser, IAuthState, ICredentials } from './types'; -------------------------------------------------------------------------------- /GhostUI/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GhostUI/HealthChecks/GCInfo/GCInfoOptions.cs: -------------------------------------------------------------------------------- 1 | namespace GhostUI.HealthChecks 2 | { 3 | public class GCInfoOptions : IGCInfoOptions 4 | { 5 | public long Threshold { get; set; } = 1024L * 1024L * 1024L; 6 | } 7 | } -------------------------------------------------------------------------------- /GhostUI/Models/IAuthUser.cs: -------------------------------------------------------------------------------- 1 | namespace GhostUI.Models 2 | { 3 | public interface IAuthUser 4 | { 5 | string Status { get; } 6 | string Token { get; } 7 | string UserName { get; } 8 | } 9 | } -------------------------------------------------------------------------------- /GhostUI/Models/ICredentials.cs: -------------------------------------------------------------------------------- 1 | namespace GhostUI.Models 2 | { 3 | public interface ICredentials 4 | { 5 | string userName { get; set; } 6 | string password { get; set; } 7 | bool rememberMe { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /GhostUI/Hubs/IUsersHub.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace GhostUI.Hubs 4 | { 5 | public interface IUsersHub 6 | { 7 | Task UserLogin(); 8 | Task UserLogout(); 9 | Task CloseAllConnections(string reason); 10 | } 11 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/index.ts: -------------------------------------------------------------------------------- 1 | import { Form } from './Form'; 2 | import { Login } from './Login'; 3 | import { Dashboard } from './Dashboard'; 4 | import { FetchData } from './FetchData'; 5 | 6 | export { 7 | Form, 8 | Login, 9 | Dashboard, 10 | FetchData 11 | }; -------------------------------------------------------------------------------- /GhostUI/Models/Credentials.cs: -------------------------------------------------------------------------------- 1 | namespace GhostUI.Models 2 | { 3 | public class Credentials : ICredentials 4 | { 5 | public string userName { get; set; } 6 | public string password { get; set; } 7 | public bool rememberMe { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/store/modules/form/types.ts: -------------------------------------------------------------------------------- 1 | export type IDropdownOption = Readonly<{ 2 | value: number; 3 | label: string; 4 | }>; 5 | 6 | export type IFormState = { 7 | count: number; 8 | checkboxValue: boolean; 9 | selectedDropdownOption: IDropdownOption; 10 | }; 11 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Login/child-components/index.ts: -------------------------------------------------------------------------------- 1 | import UserNameInput from './UserNameInput.vue'; 2 | import PasswordInput from './PasswordInput.vue'; 3 | import RememberMeInput from './RememberMeInput.vue'; 4 | 5 | export { 6 | UserNameInput, 7 | PasswordInput, 8 | RememberMeInput 9 | }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/store/modules/weather-forecasts/types.ts: -------------------------------------------------------------------------------- 1 | export type IWeatherForecast = Readonly<{ 2 | id: number; 3 | summary: string; 4 | temperatureC: number; 5 | temperatureF: number; 6 | dateFormatted: string; 7 | }>; 8 | 9 | export type IWeatherForecastsState = { 10 | startDateIndex: number; 11 | forecasts: IWeatherForecast[]; 12 | }; 13 | -------------------------------------------------------------------------------- /GhostUI/Models/IWeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace GhostUI.Models 2 | { 3 | public interface IWeatherForecast 4 | { 5 | int Id { get; } 6 | int TemperatureF { get; } 7 | int TemperatureC { get; set; } 8 | string DateFormatted { get; set; } 9 | string Summary { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare global { 4 | namespace JSX { 5 | interface Element extends VNode {} 6 | interface ElementClass extends Vue {} 7 | interface ElementAttributesProperty { 8 | $props: {}; 9 | } 10 | interface IntrinsicElements { 11 | [elem: string]: any; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GhostUI/Models/AuthUser.cs: -------------------------------------------------------------------------------- 1 | namespace GhostUI.Models 2 | { 3 | public class AuthUser : IAuthUser 4 | { 5 | public string Status { get; } 6 | public string Token { get; } 7 | public string UserName { get; } 8 | 9 | public AuthUser(string status, string token, string userName) 10 | { 11 | Status = status; 12 | Token = token; 13 | UserName = userName; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /GhostUI/Hubs/UsersHub.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.SignalR; 3 | 4 | namespace GhostUI.Hubs 5 | { 6 | public class UsersHub : Hub 7 | { 8 | public async Task UserLogin() => await Clients.All.UserLogin(); 9 | public async Task UserLogout() => await Clients.All.UserLogout(); 10 | public async Task CloseAllConnections(string reason) => await Clients.All.CloseAllConnections(reason); 11 | } 12 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': browser => { 6 | browser 7 | .url(process.env.VUE_DEV_SERVER_URL) 8 | .waitForElementVisible('#app', 5000) 9 | .assert.elementPresent('.home-content') 10 | .assert.containsText('h1', 'Full Stack Testing') 11 | .assert.elementCount('img', 1) 12 | .end(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/components/index.ts: -------------------------------------------------------------------------------- 1 | import Navbar from './Navbar.vue'; 2 | import Spinner from './Spinner.vue'; 3 | import AppFooter from './Footer.vue'; 4 | import Settings from './Settings.vue'; 5 | import VCheckbox from './VCheckbox.render'; 6 | import VDropdown from './VDropdown.render'; 7 | import Authenticator from './Authenticator.vue'; 8 | 9 | export { 10 | Navbar, 11 | Spinner, 12 | Settings, 13 | AppFooter, 14 | VCheckbox, 15 | VDropdown, 16 | Authenticator 17 | }; -------------------------------------------------------------------------------- /GhostUI/Models/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GhostUI.Models 4 | { 5 | public class WeatherForecast : IWeatherForecast 6 | { 7 | public int TemperatureC { get; set; } 8 | public string DateFormatted { get; set; } 9 | public string Summary { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | public int Id => Convert.ToInt32(DateFormatted.Replace("/", "")); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GhostUI/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace GhostUI 5 | { 6 | public static class Program 7 | { 8 | public static void Main(string[] args) 9 | => CreateHostBuilder(args).Build().Run(); 10 | 11 | public static IHostBuilder CreateHostBuilder(string[] args) 12 | => Host.CreateDefaultBuilder(args) 13 | .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/main.scss: -------------------------------------------------------------------------------- 1 | // custom scss variables/mixins 2 | @import 'base/variables.scss'; 3 | 4 | // NPM package styles 5 | @import '~bulma/bulma'; 6 | @import '~vue-snotify/styles/material.scss'; 7 | 8 | // Local styles 9 | // .scss modules under /scoped are imported by specific components using -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aspnet-core-vue-vuex-playground-template", 3 | "short_name": "VueNetCoreSpa", 4 | "icons": [ 5 | { 6 | "src": "/img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "background_color": "#fff", 19 | "theme_color": "#209cee" 20 | } 21 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | "plugin:vue/essential", 8 | "eslint:recommended", 9 | "@vue/typescript/recommended", 10 | "@vue/prettier/@typescript-eslint", 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 2020, 14 | }, 15 | rules: { 16 | "@typescript-eslint/no-inferrable-types": "off", 17 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 18 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /GhostUI/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace GhostUI.Pages 6 | { 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | public class ErrorModel : PageModel 9 | { 10 | public string RequestId { get; set; } 11 | 12 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 13 | 14 | public void OnGet() 15 | { 16 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GhostUI 8 | 9 | 10 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/config/signalr.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SignalR hub defaults 3 | * "baseUrl" needs full url or else prerendering fails (can't normalize /hubs/users) 4 | */ 5 | export type SignalRConfig = { 6 | baseUrl: string; 7 | messageTitle: string; 8 | messageDelay: number; 9 | events: Record<'login' | 'logout' | 'closeConnections', string>; 10 | }; 11 | 12 | export const SIGNALR_CONFIG: SignalRConfig = { 13 | messageDelay: 3000, 14 | baseUrl: '/hubs/users', 15 | messageTitle: 'SignalR', 16 | events: { 17 | login: 'UserLogin', 18 | logout: 'UserLogout', 19 | closeConnections: 'CloseAllConnections', 20 | }, 21 | }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/config/vue-snotify.config.ts: -------------------------------------------------------------------------------- 1 | import { SnotifyPosition, SnotifyDefaults, SnotifyStyle } from 'vue-snotify'; 2 | 3 | export const snotifyDefaults: SnotifyDefaults = { 4 | global: { 5 | newOnTop: true, 6 | maxAtPosition: 4, 7 | maxOnScreen: 4, 8 | oneAtTime: false, 9 | preventDuplicates: true 10 | }, 11 | toast: { 12 | position: SnotifyPosition.centerTop, 13 | timeout: 2500, 14 | showProgressBar: true, 15 | closeOnClick: true, 16 | pauseOnHover: true 17 | }, 18 | type: { 19 | [SnotifyStyle.async]: { 20 | closeOnClick: true, 21 | pauseOnHover: true, 22 | timeout: 7500 23 | } 24 | } 25 | }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/config/constants.ts: -------------------------------------------------------------------------------- 1 | import { IDropdownOption } from '@/store/modules/form'; 2 | 3 | /** 4 | * Dropdown test data 5 | */ 6 | export const DROPDOWN_TEST_DATA: IDropdownOption[] = [ 7 | { value: 1, label: 'Option 1' }, 8 | { value: 2, label: 'Option 2' }, 9 | { value: 3, label: 'Option 3' }, 10 | { value: 4, label: 'Option 4' }, 11 | { value: 5, label: 'Option 5' } 12 | ]; 13 | 14 | /** 15 | * HealthChecks/Swagger response path config 16 | */ 17 | export const NUGET_URL_CONFIG = { 18 | HealthUi: 'http://localhost:52530/healthchecks-ui', 19 | HealthJson: 'http://localhost:52530/healthchecks-json', 20 | SwaggerDocs: 'http://localhost:52530/docs' 21 | }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/store/modules/auth/types.ts: -------------------------------------------------------------------------------- 1 | export const AuthStatusEnum = { 2 | FAIL: 'fail', 3 | NONE: 'none', 4 | PROCESS: 'process', 5 | SUCCESS: 'success' 6 | } as const; 7 | 8 | export type AuthStatusEnum = typeof AuthStatusEnum[keyof typeof AuthStatusEnum]; 9 | 10 | export type IAuthUser = Readonly<{ 11 | token?: string; 12 | userName?: string; 13 | status?: AuthStatusEnum; 14 | }>; 15 | 16 | export type ICredentials = { 17 | userName?: string; 18 | password?: string; 19 | rememberMe?: boolean; 20 | }; 21 | 22 | export type IAuthState = { 23 | token: string; 24 | userName: string; 25 | password: string; 26 | rememberMe: boolean; 27 | status: AuthStatusEnum; 28 | }; -------------------------------------------------------------------------------- /GhostUI/ClientApp/tests/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function elementCount (selector, count) { 11 | this.message = `Testing if element <${selector}> has count: ${count}`; 12 | this.expected = count; 13 | this.pass = val => val === count; 14 | this.value = res => res.value; 15 | function evaluator (_selector) { 16 | return document.querySelectorAll(_selector).length; 17 | } 18 | this.command = cb => this.api.execute(evaluator, [selector], cb); 19 | } 20 | -------------------------------------------------------------------------------- /GhostUI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:52530/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "GhostUI": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:52530" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue', 7 | 'ts', 8 | 'tsx' 9 | ], 10 | transform: { 11 | '^.+\\.vue$': 'vue-jest', 12 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 13 | '^.+\\.tsx?$': 'ts-jest' 14 | }, 15 | transformIgnorePatterns: [ 16 | '/node_modules/' 17 | ], 18 | moduleNameMapper: { 19 | '^@/(.*)$': '/src/$1' 20 | }, 21 | snapshotSerializers: [ 22 | 'jest-serializer-vue' 23 | ], 24 | testMatch: [ 25 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 26 | ], 27 | testURL: 'http://localhost/', 28 | globals: { 29 | 'ts-jest': { 30 | babelConfig: true 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Login/child-components/RememberMeInput.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /GhostUI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | }, 9 | "CORS": { 10 | "PolicyName": "AllowAll" 11 | }, 12 | "SPA": { 13 | "SourcePath": "ClientApp" 14 | }, 15 | "Compression": { 16 | "EnableForHttps": true, 17 | "MimeTypes": [ 18 | "text/css", 19 | "text/xml", 20 | "text/html", 21 | "text/plain", 22 | "application/xml", 23 | "application/javascript" 24 | ] 25 | }, 26 | "HealthChecksUI": { 27 | "HealthChecks": [ 28 | { 29 | "Name": "HTTP-Api-Basic and UI", 30 | "Uri": "http://localhost:52530/healthchecks-json" 31 | } 32 | ], 33 | "Webhooks": [], 34 | "EvaluationTimeOnSeconds": 10, 35 | "MinimumSecondsBetweenFailureNotifications": 60 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-console */ 2 | import { register } from 'register-service-worker'; 3 | 4 | if (process.env.NODE_ENV === 'production') { 5 | register(`${process.env.BASE_URL}service-worker.js`, { 6 | ready() { 7 | console.log('Service worker is active.'); 8 | }, 9 | registered() { 10 | console.log('Service worker has been registered.'); 11 | }, 12 | cached() { 13 | console.log('Content has been cached for offline use.'); 14 | }, 15 | updatefound() { 16 | console.log('New content is downloading.'); 17 | }, 18 | updated(reg) { 19 | console.log('New content is available; please refresh.'); 20 | }, 21 | offline() { 22 | console.log('No internet connection found. App is running in offline mode.'); 23 | }, 24 | error(error) { 25 | console.error('Error during service worker registration:', error); 26 | } 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/FetchData/child-components/ForecastTable.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/config/fa.config.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { library } from "@fortawesome/fontawesome-svg-core"; 3 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 4 | import { 5 | faHeart, 6 | faFile, 7 | faUser, 8 | faLock, 9 | faEye, 10 | faCog, 11 | faPlus, 12 | faMinus, 13 | faEyeSlash, 14 | faSignInAlt, 15 | faSignOutAlt, 16 | faAngleDoubleLeft, 17 | faAngleDoubleRight 18 | } from "@fortawesome/free-solid-svg-icons"; 19 | 20 | import { 21 | faGithub, 22 | faMediumM, 23 | faTwitter 24 | } from "@fortawesome/free-brands-svg-icons"; 25 | 26 | library.add( 27 | faHeart, 28 | faFile, 29 | faUser, 30 | faLock, 31 | faCog, 32 | faEye, 33 | faPlus, 34 | faMinus, 35 | faEyeSlash, 36 | faSignInAlt, 37 | faSignOutAlt, 38 | faAngleDoubleLeft, 39 | faAngleDoubleRight, 40 | faGithub, 41 | faMediumM, 42 | faTwitter 43 | ); 44 | 45 | Vue.component("font-awesome-icon", FontAwesomeIcon); 46 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from '@/App.vue'; 3 | import '@/registerServiceWorker'; 4 | import '@/assets/style/scss/main.scss'; 5 | import store from '@/store'; 6 | import router from '@/router'; 7 | import Snotify from 'vue-snotify'; 8 | import { vClickOutside } from '@/plugins'; 9 | import { SignalRApi } from '@/api/signalr.service'; 10 | import { AxiosGlobalConfig, snotifyDefaults } from '@/config'; 11 | import '@/config/fa.config'; 12 | 13 | // Install custom plugins/third-party packages 14 | Vue.use(vClickOutside); 15 | Vue.use(Snotify, snotifyDefaults); 16 | 17 | // In the mounted callback configure Signalr/Axios - wrap in this.$nextTick callback to ensure all children mount as well 18 | new Vue({ 19 | router, 20 | store, 21 | render: (h) => h(App), 22 | mounted() { 23 | this.$nextTick(() => { 24 | AxiosGlobalConfig.setup(); 25 | SignalRApi.startConnection(); 26 | }); 27 | } 28 | }).$mount('#app'); 29 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "noImplicitAny": false, 7 | "noImplicitThis": true, 8 | "importHelpers": true, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true, 12 | "strictPropertyInitialization": false, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "types": [ 17 | "webpack-env", 18 | "jest" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue", 36 | "tests/**/*.ts", 37 | "tests/**/*.tsx" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /GhostUI/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 |

21 |

22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 |

27 | -------------------------------------------------------------------------------- /GhostUI/Extensions/HealthCheckBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using GhostUI.HealthChecks; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Diagnostics.HealthChecks; 5 | 6 | namespace GhostUI.Extensions 7 | { 8 | public static class HealthChecksBuilderExtensions 9 | { 10 | public static IHealthChecksBuilder AddGCInfoCheck( 11 | this IHealthChecksBuilder builder, 12 | string name, 13 | HealthStatus? failureStatus = null, 14 | IEnumerable tags = null, 15 | long? thresholdInBytes = null) 16 | { 17 | builder.AddCheck(name, failureStatus ?? HealthStatus.Degraded, tags); 18 | 19 | if (thresholdInBytes.HasValue) 20 | builder.Services.Configure(name, options => options.Threshold = thresholdInBytes.Value); 21 | 22 | return builder; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/scoped/spinner.scss: -------------------------------------------------------------------------------- 1 | @import "../base/variables.scss"; 2 | 3 | #load-spinner { 4 | position: absolute; 5 | width: 4.75em; 6 | height: 4.75em; 7 | z-index: 9999; 8 | top: 50%; 9 | left: 48%; 10 | 11 | > div { 12 | width: 4.75em; 13 | height: 4.75em; 14 | display: inline-block; 15 | position: absolute; 16 | border: 0.35em solid; 17 | border-color: $cyan transparent transparent transparent; 18 | box-sizing: border-box; 19 | border-radius: 50%; 20 | animation: spin-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 21 | 22 | &:nth-child(1) { 23 | animation-delay: -0.45s; 24 | } 25 | 26 | &:nth-child(2) { 27 | animation-delay: -0.3s; 28 | } 29 | 30 | &:nth-child(3) { 31 | animation-delay: -0.15s; 32 | } 33 | } 34 | } 35 | 36 | @keyframes spin-ring { 37 | 0% { 38 | transform: rotate(0deg); 39 | } 100% { 40 | transform: rotate(360deg); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Login/child-components/UserNameInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/api/sample.service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | import { BaseService } from './base.service'; 3 | import { IWeatherForecast } from '@/store/modules/weather-forecasts'; 4 | 5 | /** 6 | * SampleData API abstraction layer communication via Axios (typescript singleton pattern) 7 | */ 8 | class SampleService extends BaseService { 9 | private static _sampleService: SampleService; 10 | private static _controllerName: string = 'SampleData'; 11 | 12 | private constructor(controllerName: string) { 13 | super(controllerName); 14 | } 15 | 16 | public static get Instance(): SampleService { 17 | return this._sampleService || (this._sampleService = new this(this._controllerName)); 18 | } 19 | 20 | public async getWeatherForecastsAsync(startDateIndex: number): Promise { 21 | const config: AxiosRequestConfig = { params: { startDateIndex } }; 22 | const { data } = await this.$http.get('GetWeatherForecasts', config); 23 | 24 | return data; 25 | } 26 | } 27 | 28 | export const SampleApi = SampleService.Instance; 29 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/components/footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | color: #fff; 3 | background-color: $color-nav-bar; 4 | padding: 3rem 1.5rem 3rem; 5 | font-size: 1.15rem; 6 | width: 100%; 7 | margin: auto; 8 | 9 | @media only screen and (max-width: 769px) { 10 | font-size: 1rem; 11 | } 12 | 13 | .content { 14 | text-align: center; 15 | word-spacing: 0.05rem; 16 | } 17 | 18 | .buttons { 19 | margin-bottom: 0rem; 20 | 21 | > .button { 22 | font-size: 1.25rem; 23 | margin-bottom: 0; 24 | margin-right: 0 !important; 25 | color: #fff; 26 | background-color: transparent; 27 | border-color: transparent; 28 | padding-left: 0.5em; 29 | padding-right: 0.5em; 30 | transition: color 0.2s ease-out; 31 | 32 | &:hover { 33 | color: $cyan; 34 | } 35 | 36 | &:first-child { 37 | margin-left: auto !important; 38 | } 39 | 40 | &:last-child { 41 | margin-right: auto !important; 42 | } 43 | 44 | .icon { 45 | align-items: baseline; 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Matthew Areddia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/base/variables.scss: -------------------------------------------------------------------------------- 1 | /* BULMA COLOR OVERRIDES */ 2 | $red: #e93e60; 3 | $cyan: #09d3ac; 4 | $blue: $cyan; 5 | 6 | /* VUE-SNOTIFY OVERRIDES */ 7 | $snotify-info-bg: #3298dc; 8 | $snotify-info-progressBar: #3298dc; 9 | $snotify-error-bg: $red; 10 | $snotify-error-progressBar: $red; 11 | 12 | /* COLOR */ 13 | $color-body-bg: #f7f7f7; 14 | $color-nav-bar: #33363b; 15 | $color-hero-is-dark: #1f2227; 16 | $color-blue-highlight: $cyan; 17 | 18 | /* AUTHENTICATOR LOADER */ 19 | $color-login-success: $cyan; 20 | $color-login-fail: $red; 21 | $color-login-default: rgba(9, 30, 66, 0.35); 22 | 23 | /* MIXINS */ 24 | @mixin renderTabletNavView { 25 | @media only screen and (max-width: 950px) and (min-width: 600px) { 26 | @content; 27 | } 28 | } 29 | 30 | @mixin renderMobileNavView { 31 | @media only screen and (max-width: 599px) { 32 | @content; 33 | } 34 | } 35 | 36 | @mixin removeNavBarPadding { 37 | @media only screen and (max-width: 1099px) { 38 | @content; 39 | } 40 | } 41 | 42 | @mixin reduceNavBarPadding { 43 | @media only screen and (max-width: 1472px) and (min-width: 1100px) { 44 | @content; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/api/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import { BaseService } from './base.service'; 3 | import { ICredentials, IAuthUser } from '@/store/modules/auth'; 4 | 5 | /** 6 | * Auth API abstraction layer communication via Axios (typescript singleton pattern) 7 | */ 8 | class AuthService extends BaseService { 9 | private static _authService: AuthService; 10 | private static _controllerName: string = 'Auth'; 11 | 12 | private constructor(controllerName: string) { 13 | super(controllerName); 14 | } 15 | 16 | public static get Instance(): AuthService { 17 | return this._authService || (this._authService = new this(this._controllerName)); 18 | } 19 | 20 | public async logout(): Promise { 21 | return await this.$http.post('Logout'); 22 | } 23 | 24 | public async login(userName: string, password: string, rememberMe: boolean): Promise { 25 | const credentials: ICredentials = { userName, password, rememberMe }; 26 | const { data } = await this.$http.post('Login', credentials); 27 | 28 | return data; 29 | } 30 | } 31 | 32 | export const AuthApi = AuthService.Instance; 33 | -------------------------------------------------------------------------------- /GhostUI/Controllers/SampleDataController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using GhostUI.Models; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | 8 | namespace GhostUI.Controllers 9 | { 10 | [ApiController] 11 | [Route("api/[controller]/[action]")] 12 | public class SampleDataController : ControllerBase 13 | { 14 | public static readonly ImmutableArray Summaries = ImmutableArray.Create(new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }); 18 | 19 | [HttpGet] 20 | public IEnumerable GetWeatherForecasts(int startDateIndex) 21 | { 22 | var rng = new Random(); 23 | 24 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 25 | { 26 | DateFormatted = DateTime.Now.AddDays(index + startDateIndex).ToString("d"), 27 | TemperatureC = rng.Next(-20, 55), 28 | Summary = Summaries[rng.Next(Summaries.Length)] 29 | }); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /solution.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GhostUI", "GhostUI\GhostUI.csproj", "{5ADD8BB0-2131-468F-9C2B-A30603439D98}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {5ADD8BB0-2131-468F-9C2B-A30603439D98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {5ADD8BB0-2131-468F-9C2B-A30603439D98}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {5ADD8BB0-2131-468F-9C2B-A30603439D98}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {5ADD8BB0-2131-468F-9C2B-A30603439D98}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {A7CB80E3-A1C8-4D8B-9B02-FCBC5FF8E7B5} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/plugins/vue-click-outside.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_EVENTS: string[] = ['click', 'touchstart']; 2 | 3 | const vClickOutside = { 4 | install(Vue) { 5 | Vue.directive('click-outside', { 6 | bind: function(el, binding, vNode) { 7 | if (typeof binding.value !== 'function') { 8 | const compName = vNode.component; 9 | const warnMsg = `[v-click-outside]: provided expression '${binding.value}' is not a function, but has to be ${compName ? `- Found in component '${compName}` : ''}`.trim(); 10 | console.warn(warnMsg); 11 | } 12 | 13 | el.clickOutsideEvent = function (e) { 14 | e.stopPropagation(); 15 | if (!(el === e.target || el.contains(e.target))) { 16 | binding.value(e); 17 | } 18 | } 19 | 20 | DEFAULT_EVENTS.forEach((type) => document.addEventListener(type, el.clickOutsideEvent)); 21 | }, 22 | 23 | unbind: function(el) { 24 | if (el.clickOutsideEvent) { 25 | DEFAULT_EVENTS.forEach((type) => document.removeEventListener(type, el.clickOutsideEvent)); 26 | } 27 | } 28 | }); 29 | } 30 | }; 31 | 32 | export default vClickOutside; 33 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/store/modules/form/form.module.ts: -------------------------------------------------------------------------------- 1 | import store from '@/store'; 2 | import { IDropdownOption, IFormState } from './types'; 3 | import { DROPDOWN_TEST_DATA } from '@/config/constants'; 4 | import { Module, VuexModule, Mutation, getModule } from 'vuex-module-decorators'; 5 | 6 | const initialState: IFormState = { 7 | count: 0, 8 | checkboxValue: false, 9 | selectedDropdownOption: DROPDOWN_TEST_DATA[0] 10 | }; 11 | 12 | @Module({ 13 | store, 14 | name: 'form', 15 | dynamic: true 16 | }) 17 | class Form extends VuexModule implements IFormState { 18 | public count: number = initialState.count; 19 | public checkboxValue: boolean = initialState.checkboxValue; 20 | public selectedDropdownOption: IDropdownOption = initialState.selectedDropdownOption; 21 | 22 | @Mutation 23 | public UPDATE_COUNT(count: number): void { 24 | this.count = count; 25 | } 26 | 27 | @Mutation 28 | public UPDATE_CHECKBOX_VALUE(checkboxValue: boolean): void { 29 | this.checkboxValue = checkboxValue; 30 | } 31 | 32 | @Mutation 33 | public UPDATE_SELECTED_OPTION(option: IDropdownOption): void { 34 | this.selectedDropdownOption = option; 35 | } 36 | } 37 | 38 | export const FormModule = getModule(Form); 39 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/base/transitions.scss: -------------------------------------------------------------------------------- 1 | .fade-enter-active, 2 | .fade-leave-active { 3 | transition: opacity 0.25s ease; 4 | } 5 | 6 | .fade-enter, 7 | .fade-leave-to { 8 | opacity: 0; 9 | } 10 | 11 | .page-slide-right-enter-active { 12 | animation: page-enter-slideRight both cubic-bezier(0.4, 0, 0, 1.5); 13 | animation-duration: 0.35s; 14 | } 15 | 16 | .page-slide-left-enter-active { 17 | animation: page-enter-slideLeft both cubic-bezier(0.4, 0, 0, 1.5); 18 | animation-duration: 0.35s; 19 | } 20 | 21 | .page-slide-right-leave-active, 22 | .page-slide-left-leave-active { 23 | animation: page-leave-fadeOut ease; 24 | animation-duration: 0.25s; 25 | } 26 | 27 | @keyframes page-leave-fadeOut { 28 | from { 29 | opacity: 1; 30 | } to { 31 | opacity: 0; 32 | } 33 | } 34 | 35 | @keyframes page-enter-slideRight { 36 | from { 37 | opacity: 0; 38 | transform: translate3d(150px, 0, 0); 39 | } to { 40 | opacity: 1; 41 | } 42 | } 43 | 44 | @keyframes page-enter-slideLeft { 45 | from { 46 | opacity: 0; 47 | transform: translate3d(-150px, 0, 0); 48 | } to { 49 | opacity: 1; 50 | } 51 | } 52 | 53 | @keyframes opacityFadeIn { 54 | from { 55 | opacity: 0; 56 | } to { 57 | opacity: 1; 58 | } 59 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/store/modules/weather-forecasts/weather-forecasts.module.ts: -------------------------------------------------------------------------------- 1 | import store from '@/store'; 2 | import { SampleApi } from '@/api'; 3 | import { IWeatherForecast, IWeatherForecastsState } from './types'; 4 | import { Module, VuexModule, MutationAction, getModule } from 'vuex-module-decorators'; 5 | 6 | const initialState: IWeatherForecastsState = { 7 | forecasts: [], 8 | startDateIndex: 0 9 | }; 10 | 11 | @Module({ 12 | store, 13 | dynamic: true, 14 | name: 'forecasts' 15 | }) 16 | class WeatherForecast extends VuexModule implements IWeatherForecastsState { 17 | public startDateIndex: number = initialState.startDateIndex; 18 | public forecasts: IWeatherForecast[] = initialState.forecasts; 19 | 20 | @MutationAction({ mutate: ['forecasts', 'startDateIndex'] }) 21 | public async GetWeatherForecasts(index: number | null): Promise { 22 | try { 23 | const startDateIndex = index || 0; 24 | const forecasts = await SampleApi.getWeatherForecastsAsync(startDateIndex); 25 | 26 | return { 27 | forecasts, 28 | startDateIndex 29 | }; 30 | } catch (e) { 31 | return { ...initialState }; 32 | } 33 | } 34 | } 35 | 36 | export const WeatherForecastModule = getModule(WeatherForecast); 37 | -------------------------------------------------------------------------------- /GhostUI/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using GhostUI.Hubs; 2 | using GhostUI.Models; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.SignalR; 7 | 8 | namespace GhostUI.Controllers 9 | { 10 | [ApiController] 11 | [Route("api/[controller]/[action]")] 12 | public class AuthController : ControllerBase 13 | { 14 | private readonly IHubContext _hubContext; 15 | 16 | public AuthController(IHubContext usersHub) 17 | { 18 | _hubContext = usersHub; 19 | } 20 | 21 | [HttpPost] 22 | [ProducesResponseType(typeof(AuthUser), StatusCodes.Status200OK)] 23 | public async Task Login([FromBody]Credentials request) 24 | { 25 | await _hubContext.Clients.All.SendAsync("UserLogin"); 26 | var authUser = new AuthUser("success", "38595847A485DJSHND94857", request?.userName); 27 | return Ok(authUser); 28 | } 29 | 30 | [HttpPost] 31 | [ProducesResponseType(StatusCodes.Status200OK)] 32 | public async Task Logout() 33 | { 34 | await _hubContext.Clients.All.SendAsync("UserLogout"); 35 | return Ok(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/components/Authenticator.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 46 | 47 | 50 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router, { RouteConfig } from 'vue-router'; 3 | import { Dashboard, FetchData, Form, Login } from '@/views'; 4 | 5 | Vue.use(Router); 6 | 7 | const routes: RouteConfig[] = [ 8 | { 9 | path: '/', 10 | name: 'Login', 11 | component: Login, 12 | meta: { 13 | showInNav: false, 14 | transitionName: 'fade', 15 | icon: 'sign-out-alt' 16 | } 17 | }, 18 | { 19 | path: '/form', 20 | name: 'Form', 21 | component: Form, 22 | meta: { 23 | showInNav: true, 24 | transitionName: 'page-slide-left' 25 | } 26 | }, 27 | { 28 | path: '/dashboard', 29 | name: 'Home', 30 | component: Dashboard, 31 | meta: { 32 | showInNav: true, 33 | transitionName: 'fade' 34 | } 35 | }, 36 | { 37 | path: '/fetchdata', 38 | name: 'Fetch', 39 | component: FetchData, 40 | meta: { 41 | showInNav: true, 42 | transitionName: 'page-slide-right' 43 | } 44 | } 45 | ]; 46 | 47 | export default new Router({ 48 | routes, 49 | mode: 'history', 50 | base: process.env.BASE_URL, 51 | linkExactActiveClass: 'is-active', 52 | scrollBehavior() { 53 | return new Promise((resolve) => { 54 | setTimeout(() => { 55 | resolve({ x: 0, y: 0 }) 56 | }, 250); // Timout delay set to match animation duration of from-page 57 | }); 58 | } 59 | }); -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Login/child-components/PasswordInput.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/base/tool-tip.scss: -------------------------------------------------------------------------------- 1 | [data-tooltip] { 2 | position: relative; 3 | z-index: 2; 4 | cursor: pointer; 5 | 6 | &:before, 7 | &:after { 8 | visibility: hidden; 9 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; 10 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); 11 | opacity: 0; 12 | pointer-events: none; 13 | } 14 | 15 | &:before { 16 | position: absolute; 17 | bottom: 77%; 18 | left: 50%; 19 | margin-bottom: 5px; 20 | margin-left: -80px; 21 | padding: 7px; 22 | width: 145px; 23 | border-radius: 4px; 24 | background-color: #000; 25 | background-color: hsla(0, 0%, 20%, 0.9); 26 | color: white; 27 | content: attr(data-tooltip); 28 | text-align: center; 29 | font-size: 13px; 30 | line-height: 1.5; 31 | } 32 | 33 | &:after { 34 | position: absolute; 35 | bottom: 77%; 36 | left: 50%; 37 | margin-left: -5px; 38 | width: 0; 39 | border-top: 5px solid #000; 40 | border-top: 5px solid hsla(0, 0%, 20%, 0.9); 41 | border-right: 5px solid transparent; 42 | border-left: 5px solid transparent; 43 | content: " "; 44 | font-size: 0; 45 | line-height: 0; 46 | } 47 | 48 | &:hover { 49 | &:before, 50 | &:after { 51 | visibility: visible; 52 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; 53 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); 54 | opacity: 1; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/tests/unit/Spinner.spec.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { shallowMount, ThisTypedShallowMountOptions } from '@vue/test-utils'; 3 | import Spinner from '@/components/Spinner.vue'; 4 | 5 | /** 6 | * Unit Tests For Component: Spinner.vue 7 | */ 8 | describe("Spinner.vue", () => { 9 | const spinnerParentElId = "#load-spinner"; 10 | 11 | const shallowMountSpinner = ( 12 | options?: ThisTypedShallowMountOptions 13 | ) => { 14 | return shallowMount(Spinner, { 15 | ...options 16 | }); 17 | }; 18 | 19 | it("should mount and render properly", async () => { 20 | const wrapper = shallowMountSpinner(); 21 | expect(wrapper).toBeTruthy(); 22 | expect(wrapper.find(spinnerParentElId).exists()).toBe(true); 23 | }); 24 | 25 | it("v-show directive evaluates false AND element is not visible when 'isLoading' prop = false", async () => { 26 | const wrapper = shallowMountSpinner({ 27 | propsData: { 28 | isLoading: false 29 | } 30 | }); 31 | 32 | const spinnerParentEl = wrapper.find(spinnerParentElId).element; 33 | expect(spinnerParentEl).not.toBeVisible(); 34 | }); 35 | 36 | it("v-show directive evaluates true AND element is visible when 'isLoading' prop = true", async () => { 37 | const wrapper = shallowMountSpinner({ 38 | propsData: { 39 | isLoading: true 40 | } 41 | }); 42 | 43 | const spinnerParentEl = wrapper.find(spinnerParentElId).element; 44 | expect(spinnerParentEl).toBeVisible(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/components/dropdown.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | button { 3 | cursor: default; 4 | justify-content: initial; 5 | padding-left: 1em; 6 | font-weight: 400; 7 | box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); 8 | 9 | &:hover .caret-select { 10 | opacity: 1; 11 | } 12 | 13 | &:focus .caret-select { 14 | opacity: 1; 15 | color: $blue; 16 | } 17 | } 18 | 19 | &.normal-width button { 20 | min-width: 14rem; 21 | } 22 | 23 | &.full-width button { 24 | width: 100%; 25 | } 26 | 27 | &.is-medium .dropdown-menu .dropdown-content > li > a { 28 | font-size: 1rem; 29 | } 30 | 31 | &.is-active button .caret-select { 32 | transform: rotate(180deg); 33 | transition: transform 0.2s ease; 34 | } 35 | } 36 | 37 | .caret-select { 38 | opacity: 0.75; 39 | position: absolute; 40 | right: 0.75rem; 41 | display: inline-block; 42 | margin-left: 0.75rem; 43 | vertical-align: middle; 44 | border-top: 7px dashed; 45 | border-right: 7px solid transparent; 46 | border-left: 7px solid transparent; 47 | transform: rotate(0deg); 48 | transition: transform 0.2s ease; 49 | } 50 | 51 | .is-medium .caret-select { 52 | border-top: 9px dashed; 53 | border-right: 9px solid transparent; 54 | border-left: 9px solid transparent; 55 | } 56 | 57 | .dropdown-menu { 58 | right: 0; 59 | } 60 | 61 | .dropdown-content { 62 | box-shadow: 0 8px 8px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); 63 | } 64 | 65 | .selected-option { 66 | color: $blue !important; 67 | font-weight: 500; 68 | } 69 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 56 | 57 | 60 | -------------------------------------------------------------------------------- /GhostUI/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.AspNetCore.ResponseCompression; 6 | 7 | namespace GhostUI.Extensions 8 | { 9 | public static class ServiceCollectionExtensions 10 | { 11 | public static IServiceCollection AddCorsConfig(this IServiceCollection services, string name) 12 | { 13 | services.AddCors(c => c.AddPolicy(name, 14 | options => options.AllowAnyOrigin() 15 | .AllowAnyHeader() 16 | .AllowAnyMethod())); 17 | 18 | return services; 19 | } 20 | 21 | public static IServiceCollection AddResponseCompressionConfig(this IServiceCollection services, IConfiguration config, CompressionLevel compressionLvl = CompressionLevel.Fastest) 22 | { 23 | var enableForHttps = config.GetValue("Compression:EnableForHttps"); 24 | var gzipMimeTypes = config.GetSection("Compression:MimeTypes").Get(); 25 | 26 | services.AddResponseCompression(options => { 27 | options.Providers.Add(); 28 | options.Providers.Add(); 29 | options.EnableForHttps = enableForHttps; 30 | options.MimeTypes = gzipMimeTypes; 31 | }); 32 | 33 | services.Configure(options => options.Level = compressionLvl); 34 | services.Configure(options => options.Level = compressionLvl); 35 | 36 | return services; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false, 3 | 4 | // define port 5 | devServer: { 6 | // proxy: 'http://160.153.250.157:33000', // option A 7 | // host: 'http://localhost', // option B 8 | port: "3001", // option C - recommended 9 | hot: true, 10 | disableHostCheck: true, 11 | }, 12 | 13 | // https://github.com/visualfanatic/vue-svg-loader 14 | chainWebpack: (config) => { 15 | const svgRule = config.module.rule("svg"); 16 | svgRule.uses.clear(); 17 | 18 | svgRule 19 | .use("babel-loader") 20 | .loader("babel-loader") 21 | .end() 22 | .oneOf("inline") 23 | .resourceQuery(/inline/) 24 | .use("vue-svg-loader") 25 | .loader("vue-svg-loader") 26 | .end() 27 | .end() 28 | .oneOf("external") 29 | .use("file-loader") 30 | .loader("file-loader") 31 | .options({ 32 | name: "assets/[name].[hash:8].[ext]", 33 | svgo: { 34 | plugins: [{ prefixIds: true }], 35 | }, 36 | }); 37 | }, 38 | 39 | // https://cli.vuejs.org/guide/webpack.html 40 | configureWebpack: () => { 41 | if (process.env.NODE_ENV !== "production") { 42 | return {}; 43 | } 44 | 45 | return { 46 | performance: { 47 | hints: false, 48 | }, 49 | plugins: [], 50 | }; 51 | }, 52 | 53 | // https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-pwa 54 | pwa: { 55 | name: "aspnet-core-vue-vuex-playground-template", 56 | msTileColor: "#fff", 57 | themeColor: "#209cee", 58 | workboxPluginMode: "GenerateSW", 59 | workboxOptions: { 60 | skipWaiting: true, 61 | cacheId: "VueNetCoreSpa", 62 | importWorkboxFrom: "local", 63 | navigateFallback: "/index.html", 64 | }, 65 | }, 66 | }; -------------------------------------------------------------------------------- /GhostUI/HealthChecks/GCInfo/GCInfoHealthCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using System.Collections.Generic; 5 | using Microsoft.Extensions.Options; 6 | using Microsoft.Extensions.Diagnostics.HealthChecks; 7 | 8 | namespace GhostUI.HealthChecks 9 | { 10 | public class GCInfoHealthCheck : IHealthCheck 11 | { 12 | private readonly IOptionsMonitor _options; 13 | 14 | public GCInfoHealthCheck(IOptionsMonitor options) 15 | { 16 | _options = options; 17 | } 18 | 19 | public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) 20 | { 21 | // This example will report degraded status if the application is using more than the configured amount of memory (1gb by default). 22 | // Additionally we include some GC info in the reported diagnostics. 23 | var options = _options.Get(context.Registration.Name); 24 | var allocated = GC.GetTotalMemory(forceFullCollection: false); 25 | 26 | var data = new Dictionary 27 | { 28 | { "Allocated", allocated }, 29 | { "Gen0Collections", GC.CollectionCount(0) }, 30 | { "Gen1Collections", GC.CollectionCount(1) }, 31 | { "Gen2Collections", GC.CollectionCount(2) } 32 | }; 33 | 34 | // Report failure if the allocated memory is >= the threshold. 35 | // Using context.Registration.FailureStatus means that the application developer can configure how they want failures to appear. 36 | var result = allocated >= options.Threshold 37 | ? context.Registration.FailureStatus 38 | : HealthStatus.Healthy; 39 | 40 | return Task.FromResult(new HealthCheckResult( 41 | result, 42 | description: "reports degraded status if allocated bytes >= 1gb", 43 | data: data)); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aspnet-core-vue-vuex-playground-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build --modern", 9 | "lint": "vue-cli-service lint", 10 | "test:e2e": "vue-cli-service test:e2e", 11 | "test:unit": "vue-cli-service test:unit" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome-svg-core": "^1.2.35", 15 | "@fortawesome/free-brands-svg-icons": "^5.15.3", 16 | "@fortawesome/free-solid-svg-icons": "^5.15.3", 17 | "@fortawesome/vue-fontawesome": "^2.0.2", 18 | "@microsoft/signalr": "^5.0.8", 19 | "axios": "^0.21.1", 20 | "bulma": "^0.9.3", 21 | "core-js": "^3.15.2", 22 | "register-service-worker": "^1.7.2", 23 | "vue": "^2.6.14", 24 | "vue-class-component": "^7.2.6", 25 | "vue-property-decorator": "^9.1.2", 26 | "vue-router": "^3.5.2", 27 | "vue-snotify": "^3.2.1", 28 | "vue-styled-components": "^1.6.0", 29 | "vuex": "^3.6.2" 30 | }, 31 | "devDependencies": { 32 | "@testing-library/jest-dom": "^5.14.1", 33 | "@types/jest": "^26.0.24", 34 | "@typescript-eslint/eslint-plugin": "^4.28.3", 35 | "@typescript-eslint/parser": "^4.28.3", 36 | "@vue/cli-plugin-babel": "^4.5.13", 37 | "@vue/cli-plugin-e2e-nightwatch": "^4.5.13", 38 | "@vue/cli-plugin-eslint": "^4.5.13", 39 | "@vue/cli-plugin-pwa": "^4.5.13", 40 | "@vue/cli-plugin-typescript": "^4.5.13", 41 | "@vue/cli-plugin-unit-jest": "^4.5.13", 42 | "@vue/cli-service": "^4.5.13", 43 | "@vue/eslint-config-prettier": "^6.0.0", 44 | "@vue/eslint-config-typescript": "^7.0.0", 45 | "@vue/test-utils": "1.2.1", 46 | "eslint": "^7.30.0", 47 | "eslint-plugin-prettier": "^3.4.0", 48 | "eslint-plugin-vue": "^7.13.0", 49 | "prettier": "^2.3.2", 50 | "sass": "^1.35.2", 51 | "sass-loader": "^10.1.1", 52 | "ts-jest": "^27.0.3", 53 | "typescript": "^4.3.5", 54 | "vue-svg-loader": "^0.16.0", 55 | "vue-template-compiler": "^2.6.14", 56 | "vuex-module-decorators": "^1.0.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /GhostUI/nswag.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtime": "Net50", 3 | "defaultVariables": null, 4 | "documentGenerator": { 5 | "aspNetCoreToOpenApi": { 6 | "project": "GhostUI.csproj", 7 | "msBuildProjectExtensionsPath": null, 8 | "configuration": null, 9 | "runtime": null, 10 | "targetFramework": null, 11 | "noBuild": true, 12 | "verbose": true, 13 | "workingDirectory": null, 14 | "requireParametersWithoutDefault": false, 15 | "apiGroupNames": null, 16 | "defaultPropertyNameHandling": "Default", 17 | "defaultReferenceTypeNullHandling": "Null", 18 | "defaultDictionaryValueReferenceTypeNullHandling": "NotNull", 19 | "defaultResponseReferenceTypeNullHandling": "NotNull", 20 | "defaultEnumHandling": "Integer", 21 | "flattenInheritanceHierarchy": false, 22 | "generateKnownTypes": true, 23 | "generateEnumMappingDescription": false, 24 | "generateXmlObjects": false, 25 | "generateAbstractProperties": false, 26 | "generateAbstractSchemas": true, 27 | "ignoreObsoleteProperties": false, 28 | "allowReferencesWithProperties": false, 29 | "excludedTypeNames": [], 30 | "serviceHost": null, 31 | "serviceBasePath": null, 32 | "serviceSchemes": [], 33 | "infoTitle": "My Title", 34 | "infoDescription": null, 35 | "infoVersion": "1.0.0", 36 | "documentTemplate": null, 37 | "documentProcessorTypes": [], 38 | "operationProcessorTypes": [], 39 | "typeNameGeneratorType": null, 40 | "schemaNameGeneratorType": null, 41 | "contractResolverType": null, 42 | "serializerSettingsType": null, 43 | "useDocumentProvider": true, 44 | "documentName": "v1", 45 | "aspNetCoreEnvironment": null, 46 | "createWebHostBuilderMethod": null, 47 | "startupType": null, 48 | "allowNullableBodyParameters": true, 49 | "output": "wwwroot/docs/api-specification.json", 50 | "outputType": "OpenApi3", 51 | "assemblyPaths": [], 52 | "assemblyConfig": null, 53 | "referencePaths": [], 54 | "useNuGetCache": false 55 | } 56 | }, 57 | "codeGenerators": {} 58 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/store/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import store from '@/store'; 2 | import { AuthApi } from '@/api'; 3 | import { IAuthState, IAuthUser, AuthStatusEnum } from './types'; 4 | import { Module, VuexModule, Mutation, MutationAction, getModule } from 'vuex-module-decorators'; 5 | 6 | const initialState: IAuthState = { 7 | token: '', 8 | userName: '', 9 | password: '', 10 | rememberMe: false, 11 | status: AuthStatusEnum.NONE 12 | }; 13 | 14 | @Module({ 15 | store, 16 | name: 'auth', 17 | dynamic: true 18 | }) 19 | class Auth extends VuexModule implements IAuthState { 20 | public token: string = initialState.token; 21 | public userName: string = initialState.userName; 22 | public password: string = initialState.password; 23 | public status: AuthStatusEnum = initialState.status; 24 | public rememberMe: boolean = initialState.rememberMe; 25 | 26 | public get isLoginInputValid(): boolean { 27 | return !this.userName || !this.password; 28 | } 29 | 30 | public get isAuthenticated(): boolean { 31 | return !!this.token && this.status === AuthStatusEnum.SUCCESS; 32 | } 33 | 34 | @MutationAction({ mutate: ['token', 'status'] }) 35 | public async LoginUser(): Promise { 36 | try { 37 | const authUser = await AuthApi.login(this.userName, this.password, this.rememberMe); 38 | return authUser; 39 | } catch (e) { 40 | return { 41 | token: initialState.token, 42 | status: AuthStatusEnum.FAIL 43 | }; 44 | } 45 | } 46 | 47 | @MutationAction({ mutate: ['token', 'status', 'userName', 'password', 'rememberMe'] }) 48 | public async LogoutUser(): Promise { 49 | await AuthApi.logout(); 50 | return { ...initialState }; 51 | } 52 | 53 | @Mutation 54 | public UPDATE_USER_NAME(userName: string): void { 55 | this.userName = userName; 56 | } 57 | 58 | @Mutation 59 | public UPDATE_PASSWORD(password: string): void { 60 | this.password = password; 61 | } 62 | 63 | @Mutation 64 | public UPDATE_REMEMBER_ME(rememberMe: boolean): void { 65 | this.rememberMe = rememberMe; 66 | } 67 | } 68 | 69 | export const AuthModule = getModule(Auth); -------------------------------------------------------------------------------- /GhostUI/ClientApp/tests/unit/VCheckbox.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount, ThisTypedShallowMountOptions } from '@vue/test-utils'; 2 | import VCheckbox from '@/components/VCheckbox.render'; 3 | 4 | /** 5 | * Unit Tests For Component: VCheckbox.render.tsx 6 | */ 7 | describe("VCheckbox.render.tsx", () => { 8 | const inputElQuery = 'input[type="checkbox"]'; 9 | 10 | const mountVCheckbox = ( 11 | options?: ThisTypedShallowMountOptions 12 | ) => { 13 | return mount(VCheckbox, { 14 | ...options, 15 | }); 16 | }; 17 | 18 | it("should mount and render properly", async () => { 19 | const wrapper = mountVCheckbox(); 20 | expect(wrapper).toBeTruthy(); 21 | expect(wrapper.find(inputElQuery).exists()).toBe(true); 22 | }); 23 | 24 | it("'id', 'disabled', 'readonly', and 'name' attributes are rendered on input element when those props are explicitly defined", async () => { 25 | const propsData = { 26 | id: 'checkbox', 27 | disabled: true, 28 | readOnly: true, 29 | name: 'checkbox' 30 | }; 31 | 32 | const wrapper = mountVCheckbox({ propsData }); 33 | const inputEl = wrapper.find(inputElQuery).element; 34 | 35 | Object.keys(propsData).forEach((key) => { 36 | const attr = key.toLowerCase(); 37 | expect(inputEl.hasAttribute(attr)).toBe(true); 38 | }); 39 | }); 40 | 41 | it("emits the custom @checked event with new target value when the @change event is triggered", async () => { 42 | const wrapper = mountVCheckbox({ 43 | propsData: { 44 | checked: false 45 | } 46 | }); 47 | 48 | const inputNode = wrapper.find(inputElQuery); 49 | const inputEl = inputNode.element as HTMLInputElement; 50 | 51 | inputEl.checked = true; 52 | inputEl.value = inputEl.checked.toString(); 53 | 54 | await inputNode.trigger("change"); 55 | expect(wrapper.emitted().checked).toBeTruthy(); 56 | }); 57 | 58 | it("A label element is rendered with the specified value when the 'label' prop is defined", async () => { 59 | const label = "Test Label"; 60 | const wrapper = mountVCheckbox({ 61 | propsData: { 62 | label 63 | } 64 | }); 65 | 66 | expect(wrapper.find("label").text()).toMatch(label); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/config/axios.config.ts: -------------------------------------------------------------------------------- 1 | import { EventBus } from '@/event-bus'; 2 | import axios, { AxiosError, AxiosResponse } from 'axios'; 3 | 4 | const handleAxiosError = (error: AxiosError): void => { 5 | // Error Message Object 6 | const message = { 7 | body: 'Internal Server Error', 8 | request: '', 9 | status: 500 10 | }; 11 | 12 | // Setup Error Message 13 | if ( 14 | typeof error !== 'undefined' && 15 | Object.prototype.hasOwnProperty.call(error, 'message') 16 | ) { 17 | message.body = error.message; 18 | } 19 | 20 | if (typeof error.response !== 'undefined') { 21 | // Setup Generic Response Messages 22 | switch (error.response.status) { 23 | case 401: 24 | message.body = 'UnAuthorized'; 25 | break; 26 | case 404: 27 | message.body = 'API Route is Missing or Undefined'; 28 | break; 29 | case 405: 30 | message.body = 'API Route Method Not Allowed'; 31 | break; 32 | case 422: 33 | break; 34 | case 500: 35 | default: 36 | message.body = 'Internal Server Error'; 37 | break; 38 | } 39 | 40 | // Assign error status code 41 | if (error.response.status > 0) { 42 | message.status = error.response.status; 43 | } 44 | 45 | // Try to Use the Response Message 46 | if ( 47 | Object.prototype.hasOwnProperty.call(error, 'response') && 48 | Object.prototype.hasOwnProperty.call(error.response, 'data') && 49 | Object.prototype.hasOwnProperty.call(error.response.data, 'message') && 50 | !!error.response.data.message.length 51 | ) { 52 | message.body = error.response.data.message; 53 | } 54 | } 55 | 56 | 57 | // Log in console or use Snotify notification (via Global EventBus) 58 | EventBus.$snotify.error( 59 | `${message.status} (${message.body})`, 60 | 'XHR Error' 61 | ); 62 | }; 63 | 64 | export class AxiosGlobalConfig { 65 | public static setup(): void { 66 | axios.interceptors.response.use( 67 | (response: AxiosResponse) => { 68 | return response; 69 | }, 70 | (error: AxiosError) => { 71 | handleAxiosError(error); 72 | return Promise.reject(error); 73 | } 74 | ); 75 | } 76 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/scoped/navbar.scss: -------------------------------------------------------------------------------- 1 | @import "../base/variables.scss"; 2 | 3 | .navbar { 4 | width: 100%; 5 | height: 66px; 6 | background-color: $color-nav-bar; 7 | padding-left: 7rem; 8 | padding-right: 7rem; 9 | 10 | @include reduceNavBarPadding { 11 | padding-left: 4rem; 12 | padding-right: 4rem; 13 | } 14 | 15 | @include removeNavBarPadding { 16 | padding-left: 1rem; 17 | padding-right: 1rem; 18 | } 19 | 20 | .navbar-wrapper { 21 | width: 100%; 22 | height: 100%; 23 | margin: auto; 24 | display: flex; 25 | 26 | .brand-wrapper { 27 | display: flex; 28 | align-items: center; 29 | width: 46%; 30 | margin: auto; 31 | 32 | @include renderMobileNavView { 33 | margin-right: 0.25rem; 34 | } 35 | } 36 | 37 | .navbar-routes { 38 | width: 54%; 39 | 40 | .navbar-items-group { 41 | height: 100%; 42 | display: flex; 43 | font-size: 1.25rem; 44 | align-items: center; 45 | justify-content: flex-end; 46 | animation-delay: 0.25s; 47 | animation: opacityFadeIn 0.25s both ease; 48 | 49 | .navbar-item { 50 | color: white; 51 | font-weight: 600; 52 | background-color: transparent; 53 | transition: color 0.2s ease-out, border-bottom-color 0.2s ease-out; 54 | border-bottom: 2.5px solid transparent; 55 | border-top: 2.5px solid transparent; 56 | display: flex; 57 | overflow-x: auto; 58 | overflow-y: hidden; 59 | height: 100%; 60 | 61 | @include renderMobileNavView { 62 | font-size: 0.95rem; 63 | padding: 0.75rem 0.2rem 0.75rem 0.2rem; 64 | } 65 | 66 | &:not(:first-child) { 67 | margin-left: 1.25rem; 68 | 69 | @include renderMobileNavView { 70 | margin-left: 0; 71 | } 72 | } 73 | 74 | &:hover { 75 | color: $color-blue-highlight; 76 | background-color: transparent; 77 | } 78 | 79 | &.is-active { 80 | color: $color-blue-highlight !important; 81 | border-bottom-color: $color-blue-highlight !important; 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/api/signalr.service.ts: -------------------------------------------------------------------------------- 1 | import { EventBus } from '@/event-bus'; 2 | import { SIGNALR_CONFIG } from '../config'; 3 | import { HubConnection, HubConnectionBuilder, HubConnectionState, LogLevel } from '@microsoft/signalr'; 4 | 5 | /** 6 | * SignalR API abstraction layer communication. 7 | * Configures/manages hub connections (typescript singleton pattern). 8 | */ 9 | class SignalRService { 10 | private _hubConnection: HubConnection; 11 | private static _signalRService: SignalRService; 12 | 13 | private constructor() { 14 | this.createConnection(); 15 | this.registerOnServerEvents(); 16 | } 17 | 18 | public static get Instance(): SignalRService { 19 | return this._signalRService || (this._signalRService = new this()); 20 | } 21 | 22 | get connectionState(): HubConnectionState { 23 | return this._hubConnection?.state ?? HubConnectionState.Disconnected; 24 | } 25 | 26 | public async startConnection(): Promise { 27 | try { 28 | await this._hubConnection?.start(); 29 | console.assert(this.connectionState === HubConnectionState.Connected); 30 | } catch (e) { 31 | console.assert(this.connectionState === HubConnectionState.Disconnected); 32 | console.error(e); 33 | setTimeout(() => this.startConnection(), 5000); 34 | } 35 | } 36 | 37 | private createConnection(): void { 38 | this._hubConnection = new HubConnectionBuilder() 39 | .withUrl(SIGNALR_CONFIG.baseUrl) 40 | .withAutomaticReconnect() 41 | .configureLogging(LogLevel.Information) 42 | .build(); 43 | } 44 | 45 | private hubToastMessage( 46 | message: string, 47 | title: string = SIGNALR_CONFIG.messageTitle, 48 | delay: number = SIGNALR_CONFIG.messageDelay 49 | ): void { 50 | setTimeout(() => { 51 | EventBus.$snotify.info(message, title); 52 | }, delay); 53 | } 54 | 55 | private registerOnServerEvents(): void { 56 | this._hubConnection.on(SIGNALR_CONFIG.events.login, () => { 57 | this.hubToastMessage('A user has logged in'); 58 | }); 59 | 60 | this._hubConnection.on(SIGNALR_CONFIG.events.logout, () => { 61 | this.hubToastMessage('A user has logged out'); 62 | }); 63 | 64 | this._hubConnection.on(SIGNALR_CONFIG.events.closeConnections, async (reason: string) => { 65 | try { 66 | await this._hubConnection.stop(); 67 | this.hubToastMessage(`Hub closed (${reason})`); 68 | } catch (e) { 69 | console.error(e); 70 | } 71 | }); 72 | } 73 | } 74 | 75 | export const SignalRApi = SignalRService.Instance; 76 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/components/Settings.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 87 | 88 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/FetchData/FetchData.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/scoped/authenticator.scss: -------------------------------------------------------------------------------- 1 | @import "../base/variables.scss"; 2 | 3 | .fingerprint-spinner { 4 | height: 100px; 5 | width: 100px; 6 | padding: 2px; 7 | margin: 1.25em auto auto auto; 8 | overflow: hidden; 9 | position: relative; 10 | box-sizing: border-box; 11 | 12 | &.success { 13 | > div { 14 | border-top-color: $color-login-success; 15 | } 16 | } 17 | 18 | &.fail { 19 | > div { 20 | border-top-color: $color-login-fail; 21 | } 22 | } 23 | 24 | > div { 25 | position: absolute; 26 | border-radius: 50%; 27 | box-sizing: border-box; 28 | border: 2px solid transparent; 29 | margin: auto; 30 | bottom: 0; 31 | left: 0; 32 | right: 0; 33 | top: 0; 34 | border-top-color: $color-login-default; 35 | animation: fingerprint-spinner-animation 1500ms cubic-bezier(0.68, -0.75, 0.265, 1.75) infinite forwards; 36 | 37 | &:nth-child(1) { 38 | height: calc(96px / 9 + 0 * 96px / 9); 39 | width: calc(96px / 9 + 0 * 96px / 9); 40 | animation-delay: calc(50ms * 1); 41 | } 42 | 43 | &:nth-child(2) { 44 | height: calc(96px / 9 + 1 * 96px / 9); 45 | width: calc(96px / 9 + 1 * 96px / 9); 46 | animation-delay: calc(50ms * 2); 47 | } 48 | 49 | &:nth-child(3) { 50 | height: calc(96px / 9 + 2 * 96px / 9); 51 | width: calc(96px / 9 + 2 * 96px / 9); 52 | animation-delay: calc(50ms * 3); 53 | } 54 | 55 | &:nth-child(4) { 56 | height: calc(96px / 9 + 3 * 96px / 9); 57 | width: calc(96px / 9 + 3 * 96px / 9); 58 | animation-delay: calc(50ms * 4); 59 | } 60 | 61 | &:nth-child(5) { 62 | height: calc(96px / 9 + 4 * 96px / 9); 63 | width: calc(96px / 9 + 4 * 96px / 9); 64 | animation-delay: calc(50ms * 5); 65 | } 66 | 67 | &:nth-child(6) { 68 | height: calc(96px / 9 + 5 * 96px / 9); 69 | width: calc(96px / 9 + 5 * 96px / 9); 70 | animation-delay: calc(50ms * 6); 71 | } 72 | 73 | &:nth-child(7) { 74 | height: calc(96px / 9 + 6 * 96px / 9); 75 | width: calc(96px / 9 + 6 * 96px / 9); 76 | animation-delay: calc(50ms * 7); 77 | } 78 | 79 | &:nth-child(8) { 80 | height: calc(96px / 9 + 7 * 96px / 9); 81 | width: calc(96px / 9 + 7 * 96px / 9); 82 | animation-delay: calc(50ms * 8); 83 | } 84 | 85 | &:nth-child(9) { 86 | height: calc(96px / 9 + 8 * 96px / 9); 87 | width: calc(96px / 9 + 8 * 96px / 9); 88 | animation-delay: calc(50ms * 9); 89 | } 90 | } 91 | } 92 | 93 | @keyframes fingerprint-spinner-animation { 94 | 100% { 95 | transform: rotate(360deg); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/scoped/settings.scss: -------------------------------------------------------------------------------- 1 | @import "../base/variables.scss"; 2 | 3 | .fixed-plugin { 4 | position: fixed; 5 | top: 120px; 6 | right: 0; 7 | width: 65px; 8 | background: rgba(0, 0, 0, 0.45); 9 | z-index: 1031; 10 | border-radius: 8px 0 0 8px; 11 | text-align: center; 12 | transition: background 0.15s ease-in; 13 | animation: opacityFadeIn 0.25s both ease; 14 | animation-delay: 0.25s; 15 | 16 | &:hover, 17 | &.fixed-plugin-active { 18 | background: rgba(0, 0, 0, 0.6); 19 | } 20 | 21 | .fa-cog { 22 | color: white; 23 | padding: 10px; 24 | border-radius: 0 0 6px 6px; 25 | width: auto; 26 | } 27 | 28 | .dropdown-menu { 29 | top: 0; 30 | opacity: 1; 31 | right: 62px; 32 | left: auto; 33 | width: 11rem; 34 | min-width: 11rem; 35 | display: block; 36 | padding: 5px 0; 37 | background-color: white; 38 | position: absolute; 39 | z-index: 1000; 40 | user-select: none; 41 | border-radius: 0.25rem; 42 | box-shadow: 0 2px 7px 0 rgba(0,0,0,.08), 0 5px 20px 0 rgba(0,0,0,.06); 43 | 44 | &:before, 45 | &:after { 46 | top: 22px; 47 | content: ""; 48 | width: 17px; 49 | position: absolute; 50 | display: inline-block; 51 | transform: translateY(-50%); 52 | border-top: 16px solid transparent; 53 | border-bottom: 16px solid transparent; 54 | } 55 | 56 | &:before { 57 | left: auto; 58 | right: -16px; 59 | margin-left: auto; 60 | border-left: 16px solid #dbdbdb; 61 | } 62 | 63 | &:after { 64 | right: -15px; 65 | border-left: 16px solid #fff; 66 | } 67 | 68 | .header-title { 69 | line-height: 35px; 70 | font-size: 18px; 71 | font-weight: 600; 72 | text-align: center; 73 | color: #7f888f; 74 | text-transform: uppercase; 75 | padding-bottom: 3px; 76 | margin-bottom: 0.5rem; 77 | margin-left: auto; 78 | margin-right: auto; 79 | border-bottom: 1px solid rgba(0,0,0,.1); 80 | } 81 | 82 | .dropdown-item { 83 | width: 100%; 84 | font-size: 1rem; 85 | text-align: left; 86 | display: inline-block; 87 | color: #555; 88 | z-index: initial; 89 | pointer-events: visible; 90 | 91 | > svg { 92 | opacity: 0.8; 93 | margin-left: 0.3rem; 94 | margin-right: 0.5rem; 95 | } 96 | } 97 | 98 | > li:not(:first-of-type) { 99 | transition: background-color .2s ease-out; 100 | 101 | :hover { 102 | background-color: #f5f5f5; 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Login/Login.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | -------------------------------------------------------------------------------- /GhostUI/GhostUI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | true 6 | Latest 7 | false 8 | ClientApp\ 9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 10 | GhostUI 11 | GhostUI 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | %(DistFiles.Identity) 58 | PreserveNewest 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/img/VueCore.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/components/VDropdown.render.tsx: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | import { Component, Prop } from 'vue-property-decorator'; 3 | 4 | /** 5 | * React render function in Vue - using TypeScript (.tsx) - single file 6 | */ 7 | @Component 8 | export default class VDropdown extends Vue { 9 | public $refs: { 10 | dropdownMenu: HTMLElement; 11 | dropdownButton: HTMLElement; 12 | }; 13 | 14 | @Prop({ default: () => [] }) public readonly options: any[]; 15 | @Prop({ default: false }) public readonly disabled: boolean; 16 | @Prop({ default: 'label' }) public readonly labelKey: string; 17 | @Prop({ default: '' }) public readonly placeholder: string; 18 | @Prop({ default: '' }) public readonly wrapperClass: string; 19 | @Prop({ default: '' }) public readonly buttonClass: string; 20 | @Prop({ default: '' }) public readonly selectedOptionLabel: string; 21 | 22 | public open: boolean = false; 23 | 24 | get isArrayOfObjects(): boolean { 25 | return this.options && this.options[0] === Object(this.options[0]); 26 | } 27 | 28 | public render(): VNode { 29 | return ( 30 |
31 | 45 | 50 |
51 | ); 52 | } 53 | 54 | public renderListOption(option: any, index: number): VNode { 55 | const optionLabel = this.getOptionLabelName(option); 56 | 57 | return ( 58 |
  • 59 | this.updateSelectedOption(option)} 63 | > 64 | {optionLabel} 65 | 66 |
  • 67 | ); 68 | } 69 | 70 | public hideDropdownMenu(): void { 71 | this.open = false; 72 | } 73 | 74 | public toggleDropdownMenu(): void { 75 | this.open = !this.open; 76 | } 77 | 78 | public updateSelectedOption(option: any): void { 79 | this.$emit('select', option); 80 | } 81 | 82 | public getOptionLabelName(option: any): string { 83 | return this.isArrayOfObjects ? option[this.labelKey] || option[0] : option; 84 | } 85 | 86 | public keyDownHandler(e: KeyboardEvent): void { 87 | switch (e.key) { 88 | case 'ArrowUp': 89 | case 'ArrowDown': 90 | this.toggleDropdownMenu(); 91 | e.preventDefault(); 92 | break; 93 | case 'Escape': 94 | this.$refs.dropdownButton.focus(); 95 | this.hideDropdownMenu(); 96 | break; 97 | case 'Tab': 98 | this.hideDropdownMenu(); 99 | break; 100 | default: 101 | break; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/img/BulmaLogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/components/VCheckbox.render.tsx: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | import styled from 'vue-styled-components'; 3 | import { Component, Prop } from 'vue-property-decorator'; 4 | 5 | // ============================================================================ 6 | // React CSS-in-JS using its popular styled-components library 7 | // ...(creators have made one for Vue.js) 8 | // ============================================================================ 9 | 10 | const BORDER_COLOR = '#dbdbdb'; 11 | const CHECK_MARK_COLOR = '#09d3ac'; 12 | const BORDER_CHECKED_COLOR = 'rgba(9, 211, 172, 0.6)'; 13 | 14 | const StyledSpan = styled.span` 15 | padding-left: 1.5rem; 16 | `; 17 | 18 | const StyledLabelWrapper = styled.label` 19 | display: flex; 20 | user-select: none; 21 | position: relative; 22 | `; 23 | 24 | const StyledInput = styled.input` 25 | top: 0.2em; 26 | z-index: 3; 27 | opacity: 0; 28 | width: 1rem; 29 | height: 1rem; 30 | cursor: pointer; 31 | position: absolute; 32 | 33 | :checked ~ i { 34 | border-color: ${BORDER_CHECKED_COLOR}; 35 | 36 | :after, 37 | :before { 38 | opacity: 1; 39 | transition: height 0.38s ease; 40 | } 41 | 42 | :after { 43 | height: 0.5rem; 44 | } 45 | 46 | :before { 47 | height: 1.2rem; 48 | transition-delay: 0.15s; 49 | } 50 | } 51 | `; 52 | 53 | const StyledCheckIcon = styled.i` 54 | z-index: 0; 55 | width: 1rem; 56 | height: 1rem; 57 | position: absolute; 58 | color: ${BORDER_COLOR}; 59 | box-sizing: border-box; 60 | border-radius: 0.0625rem; 61 | background-color: transparent; 62 | border: 0.125rem solid currentColor; 63 | transition: border-color 0.38s ease; 64 | top: 0.2em; 65 | 66 | :after, 67 | :before { 68 | height: 0; 69 | opacity: 0; 70 | content: ""; 71 | width: 0.2rem; 72 | display: block; 73 | position: absolute; 74 | border-radius: 0.25rem; 75 | transform-origin: left top; 76 | background-color: ${CHECK_MARK_COLOR}; 77 | transition: opacity 0.38s ease, height 0s linear 0.38s; 78 | } 79 | 80 | :after { 81 | left: 0; 82 | top: 0.3rem; 83 | transform: rotate(-45deg); 84 | } 85 | 86 | :before { 87 | top: 0.65rem; 88 | left: 0.38rem; 89 | transform: rotate(-135deg); 90 | } 91 | `; 92 | 93 | // ============================================================================ 94 | // React render function in Vue - using TypeScript (.tsx) 95 | // ...single file component 96 | // ============================================================================ 97 | 98 | @Component 99 | export default class VCheckBox extends Vue { 100 | @Prop({ default: undefined }) public readonly id: string; 101 | @Prop({ default: undefined }) public readonly name: string; 102 | @Prop({ default: undefined }) public readonly label: string; 103 | @Prop({ default: false }) public readonly checked: boolean; 104 | @Prop({ default: false }) public readonly disabled: boolean; 105 | @Prop({ default: false }) public readonly readOnly: boolean; 106 | 107 | public render(): VNode { 108 | return ( 109 | 110 | 120 | 121 | {this.label && {this.label}} 122 | 123 | ); 124 | } 125 | 126 | public handleOnChange(event: Event): void { 127 | this.$emit('checked', (event.target as HTMLInputElement).checked); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Form/Form.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | -------------------------------------------------------------------------------- /GhostUI/wwwroot/docs/api-specification.json: -------------------------------------------------------------------------------- 1 | { 2 | "x-generator": "NSwag v13.11.3.0 (NJsonSchema v10.4.4.0 (Newtonsoft.Json v12.0.0.0))", 3 | "openapi": "3.0.0", 4 | "info": { 5 | "title": "GhostUI API", 6 | "version": "1.0.0" 7 | }, 8 | "paths": { 9 | "/api/Auth/Login": { 10 | "post": { 11 | "tags": [ 12 | "Auth" 13 | ], 14 | "operationId": "Auth_Login", 15 | "requestBody": { 16 | "x-name": "request", 17 | "content": { 18 | "application/json": { 19 | "schema": { 20 | "$ref": "#/components/schemas/Credentials" 21 | } 22 | } 23 | }, 24 | "required": true, 25 | "x-position": 1 26 | }, 27 | "responses": { 28 | "200": { 29 | "description": "", 30 | "content": { 31 | "application/json": { 32 | "schema": { 33 | "$ref": "#/components/schemas/AuthUser" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | }, 41 | "/api/Auth/Logout": { 42 | "post": { 43 | "tags": [ 44 | "Auth" 45 | ], 46 | "operationId": "Auth_Logout", 47 | "responses": { 48 | "200": { 49 | "description": "" 50 | } 51 | } 52 | } 53 | }, 54 | "/api/SampleData/GetWeatherForecasts": { 55 | "get": { 56 | "tags": [ 57 | "SampleData" 58 | ], 59 | "operationId": "SampleData_GetWeatherForecasts", 60 | "parameters": [ 61 | { 62 | "name": "startDateIndex", 63 | "in": "query", 64 | "schema": { 65 | "type": "integer", 66 | "format": "int32" 67 | }, 68 | "x-position": 1 69 | } 70 | ], 71 | "responses": { 72 | "200": { 73 | "description": "", 74 | "content": { 75 | "application/json": { 76 | "schema": { 77 | "type": "array", 78 | "items": { 79 | "$ref": "#/components/schemas/WeatherForecast" 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }, 89 | "components": { 90 | "schemas": { 91 | "AuthUser": { 92 | "type": "object", 93 | "additionalProperties": false, 94 | "properties": { 95 | "status": { 96 | "type": "string", 97 | "nullable": true 98 | }, 99 | "token": { 100 | "type": "string", 101 | "nullable": true 102 | }, 103 | "userName": { 104 | "type": "string", 105 | "nullable": true 106 | } 107 | } 108 | }, 109 | "Credentials": { 110 | "type": "object", 111 | "additionalProperties": false, 112 | "properties": { 113 | "userName": { 114 | "type": "string", 115 | "nullable": true 116 | }, 117 | "password": { 118 | "type": "string", 119 | "nullable": true 120 | }, 121 | "rememberMe": { 122 | "type": "boolean" 123 | } 124 | } 125 | }, 126 | "WeatherForecast": { 127 | "type": "object", 128 | "additionalProperties": false, 129 | "properties": { 130 | "temperatureC": { 131 | "type": "integer", 132 | "format": "int32" 133 | }, 134 | "dateFormatted": { 135 | "type": "string", 136 | "nullable": true 137 | }, 138 | "summary": { 139 | "type": "string", 140 | "nullable": true 141 | }, 142 | "temperatureF": { 143 | "type": "integer", 144 | "format": "int32" 145 | }, 146 | "id": { 147 | "type": "integer", 148 | "format": "int32" 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/views/Dashboard/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor directories and files 2 | .idea 3 | *.suo 4 | *.user 5 | *.userosscache 6 | *.sln 7 | *.sln.docstates 8 | *.ntvs* 9 | *.njsproj 10 | .vscode 11 | 12 | # File generated by NuGet package AspNetCore.HealthChecks.UI 13 | healthchecksdb 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015 cache/options directory 31 | .vs/ 32 | /wwwroot/dist/ 33 | /ClientApp/dist/ 34 | /GhostUI/wwwroot/dist 35 | /GhostUI/ClientApp/dist 36 | 37 | # MSTest test Results 38 | [Tt]est[Rr]esult*/ 39 | [Bb]uild[Ll]og.* 40 | 41 | # NUNIT 42 | *.VisualState.xml 43 | TestResult.xml 44 | 45 | # Build Results of an ATL Project 46 | [Dd]ebugPS/ 47 | [Rr]eleasePS/ 48 | dlldata.c 49 | 50 | # DNX 51 | project.lock.json 52 | project.fragment.lock.json 53 | artifacts/ 54 | 55 | *_i.c 56 | *_p.c 57 | *_i.h 58 | *.ilk 59 | *.meta 60 | *.obj 61 | *.pch 62 | *.pdb 63 | *.pgc 64 | *.pgd 65 | *.rsp 66 | *.sbr 67 | *.tlb 68 | *.tli 69 | *.tlh 70 | *.tmp 71 | *.tmp_proj 72 | *.log 73 | *.vspscc 74 | *.vssscc 75 | .builds 76 | *.pidb 77 | *.svclog 78 | *.scc 79 | 80 | # Chutzpah Test files 81 | _Chutzpah* 82 | 83 | # Visual C++ cache files 84 | ipch/ 85 | *.aps 86 | *.ncb 87 | *.opendb 88 | *.opensdf 89 | *.sdf 90 | *.cachefile 91 | *.VC.db 92 | *.VC.VC.opendb 93 | 94 | # Visual Studio profiler 95 | *.psess 96 | *.vsp 97 | *.vspx 98 | *.sap 99 | 100 | # TFS 2012 Local Workspace 101 | $tf/ 102 | 103 | # Guidance Automation Toolkit 104 | *.gpState 105 | 106 | # ReSharper is a .NET coding add-in 107 | _ReSharper*/ 108 | *.[Rr]e[Ss]harper 109 | *.DotSettings.user 110 | 111 | # JustCode is a .NET coding add-in 112 | .JustCode 113 | 114 | # TeamCity is a build add-in 115 | _TeamCity* 116 | 117 | # DotCover is a Code Coverage Tool 118 | *.dotCover 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | #*.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignoreable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | node_modules/ 203 | orleans.codegen.cs 204 | .DS_Store 205 | package-lock.json 206 | 207 | # Since there are multiple workflows, uncomment next line to ignore bower_components 208 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 209 | #bower_components/ 210 | 211 | # RIA/Silverlight projects 212 | Generated_Code/ 213 | 214 | # Backup & report files from converting an old project file 215 | # to a newer Visual Studio version. Backup files are not needed, 216 | # because we have git ;-) 217 | _UpgradeReport_Files/ 218 | Backup*/ 219 | UpgradeLog*.XML 220 | UpgradeLog*.htm 221 | 222 | # SQL Server files 223 | *.mdf 224 | *.ldf 225 | 226 | # Business Intelligence projects 227 | *.rdl.data 228 | *.bim.layout 229 | *.bim_*.settings 230 | 231 | # Microsoft Fakes 232 | FakesAssemblies/ 233 | 234 | # GhostDoc plugin setting file 235 | *.GhostDoc.xml 236 | 237 | # Node.js Tools for Visual Studio 238 | .ntvs_analysis.dat 239 | 240 | # Visual Studio 6 build log 241 | *.plg 242 | 243 | # Visual Studio 6 workspace options file 244 | *.opt 245 | 246 | # Visual Studio LightSwitch build output 247 | **/*.HTMLClient/GeneratedArtifacts 248 | **/*.DesktopClient/GeneratedArtifacts 249 | **/*.DesktopClient/ModelManifest.xml 250 | **/*.Server/GeneratedArtifacts 251 | **/*.Server/ModelManifest.xml 252 | _Pvt_Extensions 253 | 254 | # Paket dependency manager 255 | .paket/paket.exe 256 | paket-files/ 257 | 258 | # FAKE - F# Make 259 | .fake/ 260 | 261 | # JetBrains Rider 262 | .idea/ 263 | *.sln.iml 264 | 265 | # CodeRush 266 | .cr/ 267 | 268 | # Python Tools for Visual Studio (PTVS) 269 | __pycache__/ 270 | *.pyc -------------------------------------------------------------------------------- /GhostUI/ClientApp/src/assets/style/scss/base/generic.scss: -------------------------------------------------------------------------------- 1 | /* OVERIDE & GENERAL STYLES */ 2 | html, 3 | body { 4 | background-color: $color-body-bg; 5 | } 6 | 7 | .box { 8 | padding: 1.75rem; 9 | border-radius: 0.25rem; 10 | border-color: rgba(0,0,0,.05); 11 | box-shadow: 0 2px 7px 0 rgba(0,0,0,.08), 0 5px 20px 0 rgba(0,0,0,.06); 12 | 13 | &.login-box { 14 | padding: 1.5rem; 15 | } 16 | 17 | &.container-box { 18 | min-height: 440px; 19 | } 20 | } 21 | 22 | #login-img { 23 | opacity: 0.9; 24 | padding-bottom: 0.75rem; 25 | } 26 | 27 | .section { 28 | min-height: 60rem; 29 | } 30 | 31 | .section-login { 32 | padding: 1rem 1.5rem; 33 | } 34 | 35 | code { 36 | padding: 3px 4px; 37 | font-size: 0.94em; 38 | word-break: break-word; 39 | border-radius: 0.25rem; 40 | color: $color-hero-is-dark; 41 | background-color: rgba(9, 211, 172, 0.13); 42 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 43 | } 44 | 45 | .hero.is-dark { 46 | background-color: $color-hero-is-dark; 47 | 48 | .hero-body { 49 | padding: 1.5rem; 50 | } 51 | 52 | .title { 53 | color: $color-blue-highlight; 54 | } 55 | } 56 | 57 | .form-columns { 58 | h5 { 59 | margin-bottom: 3rem !important; 60 | } 61 | 62 | .title:not(.is-spaced) + .subtitle { 63 | margin-top: -1rem; 64 | } 65 | 66 | .form-control-group { 67 | min-height: 65px; 68 | } 69 | } 70 | 71 | .is-horizontal-center { 72 | align-items: center; 73 | justify-content: center; 74 | } 75 | 76 | .button { 77 | font-weight: 600; 78 | border-radius: 3px; 79 | 80 | &.is-light { 81 | background-color: rgba(9,30,66,.0725); 82 | 83 | &:hover { 84 | background-color: rgba(9, 30, 66, 0.11); 85 | } 86 | } 87 | } 88 | 89 | .remember-me-field { 90 | display: flex; 91 | 92 | label { 93 | margin-left: auto; 94 | margin-right: auto; 95 | } 96 | 97 | span { 98 | font-size: 1.02em; 99 | } 100 | } 101 | 102 | .incrementer-buttons { 103 | align-items: initial; 104 | margin-bottom: .75rem !important; 105 | 106 | > .button { 107 | margin-bottom: 0; 108 | min-width: 6.5rem; 109 | 110 | svg { 111 | margin: auto; 112 | font-size: 1.4em; 113 | } 114 | 115 | &.plus { 116 | svg { 117 | color: $cyan; 118 | } 119 | } 120 | 121 | &.minus { 122 | svg { 123 | color: $red; 124 | } 125 | } 126 | 127 | &:not(:last-child) { 128 | margin-right: 0.75rem !important; 129 | } 130 | } 131 | } 132 | 133 | .dashboard-wrapper { 134 | min-height: 60rem; 135 | padding-bottom: 3rem; 136 | 137 | .card-content { 138 | padding: 1rem 1.5rem; 139 | 140 | .content li + li { 141 | margin-top: 1em; 142 | } 143 | 144 | .dashboard-info { 145 | padding: 1.5em !important; 146 | } 147 | } 148 | 149 | hr { 150 | width: 50%; 151 | margin: 0.5rem auto; 152 | background-color: rgba(0, 0, 0, 0.06); 153 | } 154 | } 155 | 156 | .dashboard-link { 157 | color: #363636; 158 | font-weight: 700; 159 | padding: 0.25em 0.5em 0.25em; 160 | margin-right: 0.25em; 161 | transition: background-color 0.2s ease-out, border-bottom-color 0.2s ease-out; 162 | 163 | &.vue { 164 | background-color: rgba(66, 185, 131, 0.18); 165 | border-bottom: 1px solid rgba(66, 185, 131, 0.725); 166 | 167 | &:hover { 168 | background-color: rgba(66, 185, 131, 0.3); 169 | border-bottom-color:rgba(66, 185, 131, 1); 170 | } 171 | } 172 | 173 | &.vuex { 174 | background-color: rgba(62, 83, 104, 0.18); 175 | border-bottom: 1px solid rgba(62, 83, 104, 0.725); 176 | 177 | &:hover { 178 | background-color: rgba(62, 83, 104, 0.3); 179 | border-bottom-color: rgba(62, 83, 104, 1); 180 | } 181 | } 182 | 183 | &.bulma { 184 | background-color: rgba(0, 196, 167, 0.18); 185 | border-bottom: 1px solid rgba(0, 196, 167, 0.725); 186 | 187 | &:hover { 188 | background-color: rgba(0, 196, 167, 0.3); 189 | border-bottom-color: rgba(0, 196, 167, 1); 190 | } 191 | } 192 | 193 | &.aspcore { 194 | background-color: rgba(118, 74, 188, 0.18); 195 | border-bottom: 1px solid rgba(118, 74, 188, 0.725); 196 | 197 | &:hover { 198 | background-color: rgba(118, 74, 188, 0.3); 199 | border-bottom-color: rgba(118, 74, 188, 1); 200 | } 201 | } 202 | 203 | &.sass { 204 | background-color: rgba(198, 83, 140, 0.18); 205 | border-bottom: 1px solid rgba(198, 83, 140, 0.725); 206 | 207 | &:hover { 208 | background-color: rgba(198, 83, 140, 0.3); 209 | border-bottom-color: rgba(198, 83, 140, 1); 210 | } 211 | } 212 | 213 | &.typescript { 214 | background-color: rgba(41, 78, 128, 0.18); 215 | border-bottom: 1px solid rgba(41, 78, 128, 0.725); 216 | 217 | &:hover { 218 | background-color: rgba(41, 78, 128, 0.3); 219 | border-bottom-color: rgba(41, 78, 128, 1); 220 | } 221 | } 222 | } 223 | 224 | .content.dashboard-content li + li { 225 | margin-top: 1em; 226 | } 227 | 228 | .dashboard-info { 229 | padding: 1.5em !important; 230 | } 231 | 232 | .is-pagination-group { 233 | .button:not(:last-child) { 234 | margin-right: 0.75rem !important; 235 | } 236 | 237 | > a { 238 | width: 8em; 239 | 240 | &:nth-child(1) { 241 | margin-left: auto; 242 | } 243 | 244 | &:nth-child(2) { 245 | margin-right: auto; 246 | } 247 | } 248 | } 249 | 250 | .icon-clickable { 251 | pointer-events: visible !important; 252 | cursor: pointer; 253 | 254 | &:hover { 255 | color: #363636 !important; 256 | opacity: 0.7; 257 | } 258 | } 259 | 260 | .table.is-fullwidth { 261 | @media (max-width: 449px) { 262 | font-size: 0.8rem; 263 | } 264 | } -------------------------------------------------------------------------------- /GhostUI/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using GhostUI.Hubs; 3 | using GhostUI.Models; 4 | using VueCliMiddleware; 5 | using GhostUI.Extensions; 6 | using HealthChecks.UI.Client; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.AspNetCore.Builder; 10 | using Microsoft.AspNetCore.Hosting; 11 | using Microsoft.AspNetCore.Diagnostics; 12 | using Microsoft.AspNetCore.SpaServices; 13 | using Microsoft.Extensions.Configuration; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 16 | 17 | namespace GhostUI 18 | { 19 | public class Startup 20 | { 21 | private readonly string _spaSourcePath; 22 | private readonly string _corsPolicyName; 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | public Startup(IConfiguration configuration) 27 | { 28 | Configuration = configuration; 29 | _spaSourcePath = Configuration.GetValue("SPA:SourcePath"); 30 | _corsPolicyName = Configuration.GetValue("CORS:PolicyName"); 31 | } 32 | 33 | public void ConfigureServices(IServiceCollection services) 34 | { 35 | // Custom healthcheck example 36 | services.AddHealthChecks() 37 | .AddGCInfoCheck("GCInfo"); 38 | 39 | // Write healthcheck custom results to healthchecks-ui (use InMemory for the DB - AspNetCore.HealthChecks.UI.InMemory.Storage nuget package) 40 | services.AddHealthChecksUI() 41 | .AddInMemoryStorage(); 42 | 43 | // Add CORS 44 | services.AddCorsConfig(_corsPolicyName); 45 | 46 | // Add Brotli/Gzip response compression (prod only) 47 | services.AddResponseCompressionConfig(Configuration); 48 | 49 | // Add SignalR 50 | services.AddSignalR(); 51 | 52 | // IMPORTANT CONFIG CHANGE IN 3.0 - 'Async' suffix in action names get stripped by default - so, to access them by full name with 'Async' part - opt out of this feature'. 53 | services.AddMvc(opt => opt.SuppressAsyncSuffixInActionNames = false); 54 | 55 | // In production, the Vue files will be served from this directory 56 | services.AddSpaStaticFiles(opt => opt.RootPath = $"{_spaSourcePath}/dist"); 57 | 58 | // Register RazorPages/Controllers 59 | services.AddControllers(); 60 | 61 | // Register the Swagger services (using OpenApi 3.0) 62 | services.AddOpenApiDocument(configure => configure.Title = $"{this.GetType().Namespace} API"); 63 | } 64 | 65 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 66 | { 67 | // If development, enable Hot Module Replacement 68 | // If production, enable Brotli/Gzip response compression & strict transport security headers 69 | if (env.IsDevelopment()) 70 | { 71 | app.UseDeveloperExceptionPage(); 72 | } 73 | else 74 | { 75 | app.UseResponseCompression(); 76 | app.UseExceptionHandler("/Error"); 77 | app.UseHsts(); 78 | } 79 | 80 | // Global exception handling 81 | app.UseExceptionHandler(builder => 82 | { 83 | builder.Run(async context => 84 | { 85 | var error = context.Features.Get(); 86 | var exDetails = new ExceptionDetails((int)HttpStatusCode.InternalServerError, error?.Error.Message); 87 | 88 | context.Response.ContentType = "application/json"; 89 | context.Response.StatusCode = exDetails.StatusCode; 90 | context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); 91 | context.Response.Headers.Add("Application-Error", exDetails.Message); 92 | context.Response.Headers.Add("Access-Control-Expose-Headers", "Application-Error"); 93 | 94 | await context.Response.WriteAsync(exDetails.ToString()); 95 | }); 96 | }); 97 | 98 | app.UseCors(_corsPolicyName); 99 | 100 | // Show/write HealthReport data from healthchecks (AspNetCore.HealthChecks.UI.Client nuget package) 101 | app.UseHealthChecksUI(); 102 | app.UseHealthChecks("/healthchecks-json", new HealthCheckOptions() 103 | { 104 | Predicate = _ => true, 105 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse 106 | }); 107 | 108 | // Register the Swagger generator and the Swagger UI middlewares 109 | // NSwage.MsBuild + adding automation config in GhostUI.csproj makes this part of the build step (updates to API will be handled automatically) 110 | app.UseOpenApi(); 111 | app.UseSwaggerUi3(settings => 112 | { 113 | settings.Path = "/docs"; 114 | settings.DocumentPath = "/docs/api-specification.json"; 115 | }); 116 | 117 | // app.UseHttpsRedirection(); 118 | app.UseStaticFiles(); 119 | app.UseSpaStaticFiles(); 120 | app.UseRouting(); 121 | 122 | // Map controllers / SignalR hubs / HealthChecks 123 | // Configure VueCliMiddleware package to handle startup of Vue.js ClientApp front-end 124 | app.UseEndpoints(endpoints => 125 | { 126 | endpoints.MapControllers(); 127 | endpoints.MapHub("/hubs/users"); 128 | 129 | endpoints.MapToVueCliProxy( 130 | "{*path}", 131 | new SpaOptions { SourcePath = _spaSourcePath }, 132 | npmScript: System.Diagnostics.Debugger.IsAttached ? "serve" : null, 133 | regex: "Compiled successfully", 134 | port: 3001, 135 | forceKill: true 136 | ); 137 | }); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core 5.0 + Vue + Vuex + TypeScript + Hot Module Replacement (HMR) 2 | This template is a SPA application built using ASP.NET Core 5.0 as the REST API server and Vue/Vuex/TypeScript as the web client (Bulma + SASS + vue-styled-components for UI styling). You can find a similar version using React + Redux (and associated libraries) here: [aspnet-core-react-redux-playground-template](https://github.com/based-ghost/aspnet-core-react-redux-playground-template) 3 | 4 | 5 | ![](https://j.gifs.com/oVKkEY.gif) 6 | 7 | 8 | ## General Overview 9 | This template is vaguely based on the original Vue + TypeScript .NET Core SPA template that was offered in the past with earlier versions of the framework (Vue has been removed as an option from their starter templates for some reason). Using that as a base, this template greatly extends the functionality provided and also uses the latest versions of all referenced libraries/packages. Keep in mind that I use this project (or others like it) as a testing ground for varying libraries/packages and it is not meant to act as a stand-alone final solution - it is more of POC. For example, the login & logout processes are stubbed to simulate the actual process (no real authentication is happening, however, it is something I plan to add to this project in the near future). I plan on keeping this up to date, and the listed technology stack may be subject to change. 10 | 11 | * Front-end bootstrapped using the [`Vue CLI App`](https://cli.vuejs.org) 12 | * Server has the [`aspnetcore-vueclimiddleware`](https://github.com/EEParker/aspnetcore-vueclimiddleware) nuget package installed in order to execute the `npm run serve` ClientApp script automatically when you run the .NET project (browser is also configured to automatically launch with IIS, so there is no need to manually run your Vue front-end every time). 13 | 14 | ## Technology Stack Overview 15 | - **Server** 16 | - ASP.NET Core 5.0 17 | - SignalR 18 | - [`aspnetcore-vueclimiddleware`](https://github.com/EEParker/aspnetcore-vueclimiddleware) 19 | - HealthChecks + [AspNetCore.HealthChecks.UI package](https://github.com/xabaril/AspNetCore.Diagnostics.HealthChecks) - this provides a nicely formatted UI for viewing the results of the HealthCheck modules in use and is accessed on ```/health-ui``` (also, provide an option for viewing the raw JSON data that the UI package prettifies for you at ```/healthchecks-json```). Access this view in the application via the floating settings cog on right screen by clicking the "Health Checks" link. 20 | - API Documentation using Swagger UI - using package [NSwag.AspNetCore](http://NSwag.org) to prettify the specification output and display at ```/docs``` & [NSwag.MSBuild](http://NSwag.org) to handle automatic updates - so that when the project builds, the NSwag CLI will run and generate an updated API specification. Access this view in the application via the floating settings cog on right screen by clicking the "Swagger API" link. 21 | - Brotli/Gzip response compression (production build) 22 | - **Client** 23 | - [`Vue`](https://vuejs.org/) 24 | - [`Vuex`](https://vuex.vuejs.org/) 25 | - [`Vue-router`](https://router.vuejs.org/) 26 | - [`TypeScript`](https://www.typescriptlang.org/) 27 | - [`Webpack`](https://github.com/webpack/webpack) for bundling of application assets and HMR (Hot Module Replacement) 28 | - [`Bulma CSS`](https://bulma.io/) + [`SASS`](https://github.com/sass/sass) + Font Awesome 5 (using fontawesome-svg-core) 29 | - [`Axios`](https://github.com/axios/axios) for REST endpoint requests 30 | - [`vue-svg-loader`](https://github.com/visualfanatic/vue-svg-loader) for fetching and displaying SVG images inline 31 | - [`vue-styled-components`](https://github.com/styled-components/vue-styled-components) - this is the Vue.js implementation of the popular React.js [styled-components](https://www.styled-components.com/). Write component-scoped CSS code in JavaScript via template literals - see example further down with the VCheckbox.render.tsx component. 32 | - [`vuex-module-decorators`](https://github.com/championswimmer/vuex-module-decorators) - a helpful package of decorators which allows you to write your vuex store modules in class-based syntax (inspired by vue-class-component). Also allows for easier namespacing and registration of modules into store at runtime after store is constructed - dynamic modules (I have all the modules configured this way in my project). 33 | - [`vue-snotify`](https://github.com/artemsky/vue-snotify) - a highly configurable toast notification library - comes hooked up to display login error & SignalR hub push notifications examples. 34 | - Two different loader components (spinner & authentication animation w/ callback for success/fail) 35 | - Babel integration to handle transformation of React-like JSX/TSX render function syntax - configured in package.json, but can be moved to a babelrc file. The app's VCheckbox.render.tsx & VDropdown.render.tsx components are live examples. This is a nice option to have for components that have very little HTML or for those that come from a React background and are comfortable with JSX syntax. Here is what the VCheckbox.render.tsx component looks like: 36 | 37 | Note: I wired up ```vue-styled-components``` for this component as well to fully demonstrate the scope of React's influence over Vue's ecosystem (check out the source code for VCheckbox.render.tsx to see how styled-components are implemented in Vue.js). 38 | 39 | ```TSX 40 | import Vue, { VNode } from 'vue'; 41 | import styled from 'vue-styled-components'; 42 | import { Component, Prop } from 'vue-property-decorator'; 43 | 44 | const StyledSpan = styled.span` 45 | padding-left: 1.5rem; 46 | `; 47 | 48 | const StyledLabelWrapper = styled.label` 49 | display: flex; 50 | user-select: none; 51 | position: relative; 52 | `; 53 | 54 | const StyledInput = styled.input` 55 | /* ...CSS CODE */ 56 | ... 57 | `; 58 | 59 | const StyledCheckIcon = styled.i` 60 | /* ...CSS CODE */ 61 | ... 62 | `; 63 | 64 | @Component 65 | export default class VCheckBox extends Vue { 66 | @Prop({ default: null }) public readonly id: string; 67 | @Prop({ default: null }) public readonly name: string; 68 | @Prop({ default: null }) public readonly label: string; 69 | @Prop({ default: false }) public readonly checked: boolean; 70 | @Prop({ default: false }) public readonly disabled: boolean; 71 | @Prop({ default: false }) public readonly readOnly: boolean; 72 | 73 | public render(): VNode { 74 | return ( 75 | 76 | 86 | 87 | {this.label && {this.label}} 88 | 89 | ); 90 | } 91 | 92 | public handleOnChange(event: Event): void { 93 | this.$emit('checked', (event.target as HTMLInputElement).checked); 94 | } 95 | } 96 | ``` 97 | 98 | - **Unit Testing** 99 | - Jest - configured in package.json and pointed to run all tests in any files under /ClientApp/tests. Run ```npm run test:unit``` to execute. Unit tests for components `VCheckBox.render.tsx` and `Spinner.vue` are included as examples. 100 | 101 | ## Setup 102 | - [Node.js version >= 10](https://nodejs.org/en/download/) 103 | - [`.NET 5.0 SDK`](https://dotnet.microsoft.com/download/dotnet/5.0) 104 | - Clone the repository and running ```npm install``` should properly restore all packages and dependencies - if the vendor.js & vendor-manifest.json did not get installed, run ```npm run webpack``` to execute the script added to accomplish this task. 105 | - A solution.sln file is added to act as an entry point to open the application in Visual Studio. Visual Studio 2019 and up and the [Vue.js Pack 2017](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.VuejsPack-18329) extension may need to be installed as well. 106 | - GhostUI/GhostUI.csproj acts as the entry point to open the application in Visual Studio Code. 107 | -------------------------------------------------------------------------------- /GhostUI/ClientApp/public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 148 | 149 | 150 | --------------------------------------------------------------------------------