create initial react project for flexitime v2 application.

includes .net webapi backend and ui test stubs
This commit is contained in:
Chris Watts 2021-03-22 14:54:42 +00:00
parent 090152ff11
commit 005da7ce2b
102 changed files with 20271 additions and 0 deletions

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
using System;
namespace Flexitime.Interfaces
{
public interface ITokenFactory
{
string Generate(int size = 32);
}
}

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace Flexitime.Objects
{
public class Application
{
public Application()
{
Permissions = new List<Permission>();
}
public int Id { get; set; } //TODO make this GUID
public string Name { get; set; }
public List<Permission> Permissions { get; set; }
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="cd &quot;$(ProjectDir)&quot;&#xD;&#xA;csharptojs $(ProjectDir)" />
</Target>
</Project>

View File

@ -0,0 +1,10 @@
namespace Flexitime.Objects
{
public class Group
{
public int Id { get; set; }
public string Name { get; set; }
public int UserCount { get; set; }
public bool IsAssociatedToUser { get; set; }
}
}

View File

@ -0,0 +1,27 @@
using System;
namespace Flexitime.Objects
{
public class Identifier
{
public Identifier() { }
public int Id { get; set; }
public string UniqueId { get; set; }
public bool IsAssociatedToUser { get; set; }
public DateTime LastUsed { get; set; }
public override bool Equals(object obj)
{
var identObj = obj as Identifier;
if (identObj == null) return false;
return identObj.Id == Id
&& identObj.IsAssociatedToUser == IsAssociatedToUser
&& identObj.UniqueId == UniqueId;
}
public override int GetHashCode()
{
return Id.GetHashCode() ^ UniqueId.GetHashCode() ^ IsAssociatedToUser.GetHashCode() ^ LastUsed.GetHashCode();
}
}
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Flexitime.Objects
{
public class LoginRequest
{
public LoginRequest() { }
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using Flexitime.Objects;
namespace Flexitime.Objects
{
public class LoginResponse
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Token { get; set; }
public string RefreshToken { get; set; }
public LoginResponse() { }
public LoginResponse(User user, string token)
{
Id = user.UserId;
FirstName = user.FirstName;
LastName = user.LastName;
Username = user.LoginId;
Token = token;
}
}
}

View File

@ -0,0 +1,15 @@
namespace Flexitime.Objects
{
public class Permission
{
public Permission()
{
Application = new Application();
}
public Application Application { get; set; }
public string Name { get; set; }
public string Tag { get; set; }
public int Id { get; set; } //TODO switch to guid
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Security;
namespace Flexitime.Objects
{
public class User
{
public User()
{
AssociatedIdentifiers = new List<Identifier>();
Groups = new List<Group>();
Permissions = new List<Permission>();
FirstName = string.Empty;
LastName = string.Empty;
}
public string LoginId;
public int UserId { get; set; } //TODO make this guid
public string FirstName { get; set; }
public string LastName { get; set; }
public float HoursPerWeek { get; set; }
public bool IsContractor { get; set; }
public int AssociatedIdentifierCount
{
get { return AssociatedIdentifiers.Count; }
}
public DateTime LastEventDateTime { get; set; }
public List<Identifier> AssociatedIdentifiers { get; set; }
public List<Group> Groups { get; set; }
public UserState State { get; set; }
/// <summary>
/// Id of the Users Line Manager
/// </summary>
public int LineManagerId { get; set; }
/// <summary>
/// Ids of the users direct reports
/// </summary>
public int[] DirectReportIds { get; set; }
public string Password { get; set; }
public List<Permission> Permissions { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace Flexitime.Objects
{
public enum UserState
{
Unknown,
In,
Out,
OutOfOffice,
Remote
}
}

View File

@ -0,0 +1,12 @@
{
"assemblies": [
{
"name": "Flexitime.Objects",
"include": [ "Flexitime.Objects" ]
}
],
"assembliesPath": "./bin/debug/netcoreapp2.2",
"outputPath": "../flexitimeui/src/models",
"noClean": false,
"useNugetCacheResolver": true
}

View File

@ -0,0 +1,36 @@
using Flexitime.Objects;
using FlexitimeAPI.Services;
using Microsoft.AspNetCore.Mvc;
namespace FlexitimeAPI.Controllers
{
[ApiController]
[Route("[controller]")]
public class AuthenticationController:ControllerBase
{
private readonly IUserService _userService;
public AuthenticationController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public IActionResult Authenticate(LoginRequest model)
{
var response = _userService.Authenticate(model);
if (response == null)
return BadRequest(new { message = "Username or password is incorrect" });
return Ok(response);
}
[HttpGet]
[Route("logout")]
public IActionResult Logout()
{
//should be able to get the id here and log a message of that user logging out..
return Ok();
}
}
}

View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
using Flexitime.Objects;
using FlexitimeAPI.Helpers;
using FlexitimeAPI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace FlexitimeAPI.Controllers
{
[Authorize]//(Permissions=new []{"u.v"})]
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly IUserService _userService;
public UsersController(ILogger<WeatherForecastController> logger, IUserService userService)
{
_logger = logger;
_userService = userService;
}
[HttpGet]
public IEnumerable<User> Get()
{
return _userService.GetAll();
}
[HttpGet]
[Route("{id}")]
public User Get(int id)
{
return _userService.GetById(id);
}
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace FlexitimeAPI.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RandomNameGeneratorLibrary" Version="1.2.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.8.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Flexitime.Interfaces\Flexitime.Interfaces.csproj" />
<ProjectReference Include="..\Flexitime.Objects\Flexitime.Objects.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,95 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Flexitime.Objects;
using FlexitimeAPI.Models;
using FlexitimeAPI.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace FlexitimeAPI.Helpers
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
public string[] Permissions { get; set; }
public void OnAuthorization(AuthorizationFilterContext context)
{
var user = (User)context.HttpContext.Items["User"];
if (user == null)
{
// not logged in
context.Result = new JsonResult(
new {message = "Unauthorized"})
{StatusCode = StatusCodes.Status401Unauthorized};
}
else if(Permissions.Any()
&& !user.Permissions.Select(y=>y.Tag)
.Intersect(Permissions)
.Any()) //check we have permissions if they have been specified
{
context.Result =
context.Result = new JsonResult(
new { message = "Unauthorized" })
{ StatusCode = StatusCodes.Status401Unauthorized };
}
}
}
public class JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly AppSettings _appSettings;
public JwtMiddleware(RequestDelegate next, IOptions<AppSettings> appSettings)
{
_next = next;
_appSettings = appSettings.Value;
}
public async Task Invoke(HttpContext context, IUserService userService)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (token != null)
AttachUserToContext(context, userService, token);
await _next(context);
}
private void AttachUserToContext(HttpContext context, IUserService userService, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
// attach user to context on successful jwt validation
context.Items["User"] = userService.GetById(userId);
}
catch
{
// do nothing if jwt validation fails
// user is not attached to context so request won't have access to secure routes
}
}
}
}

View File

@ -0,0 +1,19 @@
using Flexitime.Interfaces;
using System;
using System.Security.Cryptography;
namespace FlexitimeAPI.Helpers
{
public class TokenGenerator:ITokenFactory
{
public string Generate(int size = 32)
{
var randomNumber = new byte[size];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
}
}

View File

@ -0,0 +1,11 @@
namespace FlexitimeAPI.Models
{
public class AppSettings
{
public AppSettings()
{
Secret = "JiminiyCricket|{08EBD219-AABA-4C33-8312-AE93EECE77D2}";
}
public string Secret { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace FlexitimeAPI
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

View File

@ -0,0 +1,31 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:46914",
"sslPort": 44370
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"FlexitimeAPI": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using Flexitime.Objects;
using FlexitimeAPI.Models;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using RandomNameGeneratorLibrary;
namespace FlexitimeAPI.Services
{
public interface IUserService
{
LoginResponse Authenticate(LoginRequest model);
IEnumerable<User> GetAll();
User GetById(int id);
}
public class UserService : IUserService
{
private PersonNameGenerator _personGenerator;
// users hardcoded for simplicity, store in a db with hashed passwords in production applications
private List<User> _users;
private readonly AppSettings _appSettings;
public UserService(IOptions<AppSettings> appSettings)
{
_appSettings = appSettings.Value;
_personGenerator = new PersonNameGenerator();
var random = new Random();
var vals = Enum.GetValues(typeof(UserState));
_users = Enumerable.Range(2, 6).Select(index =>
{
var first = _personGenerator.GenerateRandomFirstName();
var last = _personGenerator.GenerateRandomLastName();
return new User
{
UserId = index,
LoginId = $"{first}{last}",
Password = "12345",
FirstName = first,
LastName = last,
HoursPerWeek = 37,
IsContractor = false,
State = (UserState)vals.GetValue(random.Next(2, vals.Length))
};
}).ToList();
//create default known admin user..
_users.Add(new User
{
UserId = 1,
LoginId = "admin",
Password = "P@ssw0rd!",
FirstName = "Admin",
LastName = "User",
HoursPerWeek = 37,
IsContractor = false,
State = UserState.In
});
}
public LoginResponse Authenticate(LoginRequest model)
{
var user = _users.SingleOrDefault(x => x.LoginId == model.Username && x.Password == model.Password);
// return null if user not found
if (user == null) return null;
// authentication successful so generate jwt token
var token = GenerateJwtToken(user);
return new LoginResponse(user, token);
}
public IEnumerable<User> GetAll()
{
return _users;
}
public User GetById(int id)
{
return _users.FirstOrDefault(x => x.UserId == id);
}
// helper methods
private string GenerateJwtToken(User user)
{
// generate token that is valid for 7 days
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
List<Claim> claims = new List<Claim>()
{
};
//var jwt = new JwtSecurityToken(issuer:"FlexitimeUI",claims:claims);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[] { new Claim("id", user.UserId.ToString()) }),
Expires = DateTime.UtcNow.AddHours(2),
Issuer = "FlexitimeUI",
NotBefore = DateTime.UtcNow.AddSeconds(-5),
IssuedAt = DateTime.UtcNow,
//Claims = claims,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
//tokenHandler.
return tokenHandler.WriteToken(token);
}
}
}

View File

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FlexitimeAPI.Helpers;
namespace FlexitimeAPI
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "FlexitimeAPI", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "FlexitimeAPI v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseMiddleware<JwtMiddleware>();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace FlexitimeAPI
{
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string Summary { get; set; }
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,49 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31005.135
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITests", "UITests\UITests.csproj", "{1EFA5843-5BBA-469F-96A9-2CC65067151D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flexitime.Objects", "Flexitime.Objects\Flexitime.Objects.csproj", "{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flexitime.Interfaces", "Flexitime.Interfaces\Flexitime.Interfaces.csproj", "{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlexitimeAPI", "FlexitimeAPI\FlexitimeAPI.csproj", "{A9478B18-94DC-4E90-98BB-67A5343477D2}"
EndProject
Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "Flexitime.UI", "flexitimeui\Flexitime.UI.njsproj", "{9E4FD3D5-310E-4CE5-9049-F05241E4D464}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1EFA5843-5BBA-469F-96A9-2CC65067151D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1EFA5843-5BBA-469F-96A9-2CC65067151D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1EFA5843-5BBA-469F-96A9-2CC65067151D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1EFA5843-5BBA-469F-96A9-2CC65067151D}.Release|Any CPU.Build.0 = Release|Any CPU
{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9382E74E-E5FE-45A3-8C36-0E22EF2E8C21}.Release|Any CPU.Build.0 = Release|Any CPU
{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90E58E0B-836A-4F98-BD8B-4740B4D8AE0F}.Release|Any CPU.Build.0 = Release|Any CPU
{A9478B18-94DC-4E90-98BB-67A5343477D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9478B18-94DC-4E90-98BB-67A5343477D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9478B18-94DC-4E90-98BB-67A5343477D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9478B18-94DC-4E90-98BB-67A5343477D2}.Release|Any CPU.Build.0 = Release|Any CPU
{9E4FD3D5-310E-4CE5-9049-F05241E4D464}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E4FD3D5-310E-4CE5-9049-F05241E4D464}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E4FD3D5-310E-4CE5-9049-F05241E4D464}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E4FD3D5-310E-4CE5-9049-F05241E4D464}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {23786E06-E83F-43AB-93EE-E0168FCF8E4A}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,5 @@
<SolutionConfiguration>
<Settings>
<CurrentEngineMode>Run all tests automatically [Global]</CurrentEngineMode>
</Settings>
</SolutionConfiguration>

View File

@ -0,0 +1,13 @@
Feature: Calculator
![Calculator](https://specflow.org/wp-content/uploads/2020/09/calculator.png)
Simple calculator for adding **two** numbers
Link to a feature: [Calculator](UITests/Features/Calculator.feature)
***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)**
@mytag
Scenario: Add two numbers
Given the first number is 50
And the second number is 70
When the two numbers are added
Then the result should be 120

View File

@ -0,0 +1,151 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by SpecFlow (https://www.specflow.org/).
// SpecFlow Version:3.7.0.0
// SpecFlow Generator Version:3.7.0.0
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------
#region Designer generated code
#pragma warning disable
namespace UITests.Features
{
using TechTalk.SpecFlow;
using System;
using System.Linq;
[System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.7.0.0")]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public partial class CalculatorFeature : object, Xunit.IClassFixture<CalculatorFeature.FixtureData>, System.IDisposable
{
private static TechTalk.SpecFlow.ITestRunner testRunner;
private string[] _featureTags = ((string[])(null));
private Xunit.Abstractions.ITestOutputHelper _testOutputHelper;
#line 1 "Calculator.feature"
#line hidden
public CalculatorFeature(CalculatorFeature.FixtureData fixtureData, UITests_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper)
{
this._testOutputHelper = testOutputHelper;
this.TestInitialize();
}
public static void FeatureSetup()
{
testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner();
TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Calculator", @"![Calculator](https://specflow.org/wp-content/uploads/2020/09/calculator.png)
Simple calculator for adding **two** numbers
Link to a feature: [Calculator](UITests/Features/Calculator.feature)
***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)**", ProgrammingLanguage.CSharp, ((string[])(null)));
testRunner.OnFeatureStart(featureInfo);
}
public static void FeatureTearDown()
{
testRunner.OnFeatureEnd();
testRunner = null;
}
public virtual void TestInitialize()
{
}
public virtual void TestTearDown()
{
testRunner.OnScenarioEnd();
}
public virtual void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo)
{
testRunner.OnScenarioInitialize(scenarioInfo);
testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs<Xunit.Abstractions.ITestOutputHelper>(_testOutputHelper);
}
public virtual void ScenarioStart()
{
testRunner.OnScenarioStart();
}
public virtual void ScenarioCleanup()
{
testRunner.CollectScenarioErrors();
}
void System.IDisposable.Dispose()
{
this.TestTearDown();
}
[Xunit.SkippableFactAttribute(DisplayName="Add two numbers")]
[Xunit.TraitAttribute("FeatureTitle", "Calculator")]
[Xunit.TraitAttribute("Description", "Add two numbers")]
[Xunit.TraitAttribute("Category", "mytag")]
public virtual void AddTwoNumbers()
{
string[] tagsOfScenario = new string[] {
"mytag"};
System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Add two numbers", null, tagsOfScenario, argumentsOfScenario, this._featureTags);
#line 9
this.ScenarioInitialize(scenarioInfo);
#line hidden
bool isScenarioIgnored = default(bool);
bool isFeatureIgnored = default(bool);
if ((tagsOfScenario != null))
{
isScenarioIgnored = tagsOfScenario.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
}
if ((this._featureTags != null))
{
isFeatureIgnored = this._featureTags.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
}
if ((isScenarioIgnored || isFeatureIgnored))
{
testRunner.SkipScenario();
}
else
{
this.ScenarioStart();
#line 10
testRunner.Given("the first number is 50", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given ");
#line hidden
#line 11
testRunner.And("the second number is 70", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line hidden
#line 12
testRunner.When("the two numbers are added", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
#line hidden
#line 13
testRunner.Then("the result should be 120", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
#line hidden
}
this.ScenarioCleanup();
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.7.0.0")]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class FixtureData : System.IDisposable
{
public FixtureData()
{
CalculatorFeature.FeatureSetup();
}
void System.IDisposable.Dispose()
{
CalculatorFeature.FeatureTearDown();
}
}
}
}
#pragma warning restore
#endregion

View File

@ -0,0 +1,58 @@
using TechTalk.SpecFlow;
namespace UITests.Steps
{
[Binding]
public sealed class CalculatorStepDefinitions
{
// For additional details on SpecFlow step definitions see https://go.specflow.org/doc-stepdef
private readonly ScenarioContext _scenarioContext;
public CalculatorStepDefinitions(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
}
[Given("the first number is (.*)")]
public void GivenTheFirstNumberIs(int number)
{
//TODO: implement arrange (precondition) logic
// For storing and retrieving scenario-specific data see https://go.specflow.org/doc-sharingdata
// To use the multiline text or the table argument of the scenario,
// additional string/Table parameters can be defined on the step definition
// method.
_scenarioContext.Pending();
}
[Given("the second number is (.*)")]
public void GivenTheSecondNumberIs(int number)
{
//TODO: implement arrange (precondition) logic
// For storing and retrieving scenario-specific data see https://go.specflow.org/doc-sharingdata
// To use the multiline text or the table argument of the scenario,
// additional string/Table parameters can be defined on the step definition
// method.
_scenarioContext.Pending();
}
[When("the two numbers are added")]
public void WhenTheTwoNumbersAreAdded()
{
//TODO: implement act (action) logic
_scenarioContext.Pending();
}
[Then("the result should be (.*)")]
public void ThenTheResultShouldBe(int result)
{
//TODO: implement assert (verification) logic
_scenarioContext.Pending();
}
}
}

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="SpecFlow.Plus.LivingDocPlugin" Version="3.7.10" />
<PackageReference Include="SpecFlow.xUnit" Version="3.7.13" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
</ItemGroup>
<ItemGroup>
<Folder Include="Drivers\" />
<Folder Include="Hooks\" />
</ItemGroup>
</Project>

23
FlexitimeUI/flexitimeui/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{9e4fd3d5-310e-4ce5-9049-f05241e4d464}</ProjectGuid>
<ProjectHome>.</ProjectHome>
<ProjectView>ProjectFiles</ProjectView>
<StartupFile>src\index.js</StartupFile>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">16.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<LaunchUrl>http://localhost:3000</LaunchUrl>
<NodejsPort>3000</NodejsPort>
<StartWebBrowser>True</StartWebBrowser>
<SaveNodeJsSettingsInProjectFile>True</SaveNodeJsSettingsInProjectFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'" />
<PropertyGroup Condition="'$(Configuration)' == 'Release'" />
<ItemGroup>
<Content Include="craco.config.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\api\apiUtils.js" />
<Content Include="src\api\authorApi.js" />
<Content Include="src\api\groupsApi.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\api\userApi.js" />
<Content Include="src\components\common\ImagePicker.jsx">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\common\NumberInput.jsx" />
<Content Include="src\components\common\SelectInput.jsx" />
<Content Include="src\components\common\Spinner.jsx">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\common\TextInput.jsx" />
<Content Include="src\components\common\UserStateIndicator.jsx">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\groups\GroupList.jsx">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\groups\GroupsPage.jsx">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\users\ManageUserPage.jsx" />
<Content Include="src\components\users\UserForm.jsx" />
<Content Include="src\components\users\UserGrid.jsx">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\users\UserList.jsx">
<SubType>Code</SubType>
</Content>
<Content Include="src\models\Application.js" />
<Content Include="src\models\Group.js" />
<Content Include="src\models\Identifier.js" />
<Content Include="src\models\LoginRequest.js" />
<Content Include="src\models\LoginResponse.js" />
<Content Include="src\models\Permission.js" />
<Content Include="src\models\User.js" />
<Content Include="package-lock.json" />
<Content Include="package.json" />
<Content Include="README.md" />
<Content Include="public\robots.txt" />
<Content Include="public\index.html" />
<Content Include="public\logo192.png" />
<Content Include="public\logo512.png" />
<Content Include="public\favicon.ico" />
<Content Include="public\manifest.json" />
<Content Include="src\App.css" />
<Content Include="src\models\userState.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\redux\actions\actionTypes.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\redux\actions\apiStatusActions.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\redux\actions\groupActions.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\redux\actions\userActions.js" />
<Content Include="src\redux\configureStore.js" />
<Content Include="src\redux\reducers\apiStatusReducer.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\redux\reducers\index.js" />
<Content Include="src\redux\reducers\groupsReducer.js" />
<Content Include="src\redux\reducers\initialState.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\redux\reducers\userReducer.js" />
<Content Include="src\components\PageNotFound_404.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\about\AboutPage.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\common\Header.jsx">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\home\HomePage.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\users\UserPage.jsx">
<SubType>Code</SubType>
</Content>
<Content Include="src\index.css" />
<Content Include="src\logo.svg" />
<Content Include="src\components\App.jsx" />
<Content Include="src\App.test.js" />
<Content Include="src\index.js" />
<Content Include="src\reportWebVitals.js" />
<Content Include="src\services\userService.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\setupTests.js" />
<Content Include="src\styles\Spinner.css">
<SubType>Code</SubType>
</Content>
<Content Include="src\styles\StateIndicator.css">
<SubType>Code</SubType>
</Content>
<Content Include="src\styles\toggleSlider.css">
<SubType>Code</SubType>
</Content>
<Content Include="tools\apiServer.js" />
<Content Include="tools\createMockDb.js" />
<Content Include="tools\mockData.js" />
<Content Include="__webpack.config.dev.js" />
</ItemGroup>
<ItemGroup>
<Folder Include="public" />
<Folder Include="src" />
<Folder Include="src\api\" />
<Folder Include="src\components\groups\" />
<Folder Include="src\models\" />
<Folder Include="src\services\" />
<Folder Include="src\styles\" />
<Folder Include="src\redux\" />
<Folder Include="src\redux\actions\" />
<Folder Include="src\redux\reducers\" />
<Folder Include="src\components\" />
<Folder Include="src\components\home\" />
<Folder Include="src\components\about\" />
<Folder Include="src\components\common\" />
<Folder Include="src\components\users\" />
<Folder Include="tools\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsToolsV2.targets" />
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>0</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:48022/</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost:1337</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<LastActiveSolutionConfig>Debug|Any CPU</LastActiveSolutionConfig>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

View File

@ -0,0 +1,47 @@
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
process.env.NODE_ENV = "development";
module.exports = {
mode: "development",
target: "web",
devtool: "cheap-module-source-map",
entry: "./src/index",
output: {
path: path.resolve(__dirname, "build"),
publicPath: "/",
filename: "bundle.js"
},
devServer: {
stats: "minimal",
overlay: true,
historyApiFallback: true,
disableHostCheck: true,
headers: { "Access-Control-Allow-Origin": "*" },
https: false
},
plugins: [
new webpack.DefinePlugin({
"process.env.API_URL": JSON.stringify("http://localhost:3001")
}),
new HtmlWebpackPlugin({
//template: "src/index.html",
favicon: "src/favicon.ico"
})
],
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ["babel-loader", "eslint-loader"]
},
{
test: /(\.css)$/,
use: ["style-loader", "css-loader"]
}
]
}
};

View File

@ -0,0 +1,11 @@
const webpack = require("webpack");
module.exports = {
webpack: {
plugins: [
new webpack.DefinePlugin({
"process.env.API_URL": JSON.stringify("http://localhost:3001")
})
]
}
}

16951
FlexitimeUI/flexitimeui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
{
"name": "flexitimeui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.8.3",
"babel": "^6.23.0",
"bootstrap": "^4.6.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-toastify": "^7.0.3",
"redux": "^4.0.5",
"redux-immutable-state-invariant": "^2.1.0",
"redux-thunk": "^2.3.0",
"web-vitals": "^1.1.0"
},
"scripts": {
"start": "run-p start:dev start:api",
"start:dev": "craco start",
"prestart:api": "node tools/createMockDb.js",
"start:api": "node tools/apiServer.js",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"craco": "0.0.3",
"json-server": "^0.16.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -0,0 +1,17 @@
export async function handleResponse(response) {
if (response.ok) return response.json();
if (response.status === 400) {
// So, a server-side validation error occurred.
// Server side validation returns a string error message, so parse as text instead of json.
const error = await response.text();
throw new Error(error);
}
throw new Error("Network response was not ok.");
}
// In a real app, would likely call an error logging service.
export function handleError(error) {
// eslint-disable-next-line no-console
console.error("API call failed. " + error);
throw error;
}

View File

@ -0,0 +1,8 @@
import { handleResponse, handleError } from "./apiUtils";
const baseUrl = process.env.API_URL + "/authors/";
export function getAuthors() {
return fetch(baseUrl)
.then(handleResponse)
.catch(handleError);
}

View File

@ -0,0 +1,30 @@
import { handleResponse, handleError } from "./apiUtils";
const baseUrl = process.env.API_URL + "/groups/";
export function getGroups() {
return fetch(baseUrl)
.then(handleResponse)
.catch(handleError);
}
export function getGroup(id) {
return fetch(baseUrl + id)
.then(handleResponse)
.catch(handleError);
}
export function saveGroup(group) {
return fetch(baseUrl + (group.id || ""),
{ method: group.id ? "PUT" : "POST", // POST for create, PUT to update when id already exists.
headers: { "content-type": "application/json" },
body: JSON.stringify(group)
})
.then(handleResponse)
.catch(handleError);
}
export function deleteGroup(groupId) {
return fetch(baseUrl + groupId, { method: "DELETE" })
.then(handleResponse)
.catch(handleError);
}

View File

@ -0,0 +1,30 @@
import { handleResponse, handleError } from "./apiUtils";
const baseUrl = process.env.API_URL + "/users/";
export function getUsers() {
return fetch(baseUrl)
.then(handleResponse)
.catch(handleError);
}
export function getUser(id) {
return fetch(baseUrl + id)
.then(handleResponse)
.catch(handleError);
}
export function saveUser(user) {
return fetch(baseUrl + (user.id || ""), {
method: user.id ? "PUT" : "POST", // POST for create, PUT to update when id already exists.
headers: { "content-type": "application/json" },
body: JSON.stringify(user)
})
.then(handleResponse)
.catch(handleError);
}
export function deleteUser(id) {
return fetch(baseUrl + id, { method: "DELETE" })
.then(handleResponse)
.catch(handleError);
}

View File

@ -0,0 +1,34 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import HomePage from "./home/HomePage";
import AboutPage from "./about/AboutPage";
import Header from "./common/Header";
import PageNotFound_404 from "./PageNotFound_404";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import "./../App.css";
import UserPage from "./users/UserPage";
import GroupsPage from "./groups/GroupsPage";
import ManageUserPage from "./users/ManageUserPage";
function App() {
return (
<div className="container">
<Header />
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/about" component={AboutPage} />
<Route path="/users" component={UserPage} />
<Route path="/user/:id" component={ManageUserPage} />
<Route path="/user" component={ManageUserPage} />
<Route path="/groups" component={GroupsPage} />
<Route component={PageNotFound_404} />
</Switch>
<ToastContainer autoClose={3000} hideProgressBar />
</div>
);
}
export default App;

View File

@ -0,0 +1,5 @@
import React from 'react';
const PageNotFound_404 = () => <h1>Oops! Page Not Found. </h1>
export default PageNotFound_404;

View File

@ -0,0 +1,10 @@
import React from "react";
const AboutPage = () => (
<div className="jumbotron">
<h2>About</h2>
<p>This app uses React, Redux, React Router and many other helpful libraries.</p>
</div>
);
export default AboutPage;

View File

@ -0,0 +1,69 @@
import React, { useEffect, useState } from "react";
import { NavLink } from "react-router-dom";
import { withRouter } from "react-router";
function Header(props) {
function getNavLinkClass(path) {
return props.location.pathname === path
? "nav-item active"
: "nav-item";
}
return (
<div>
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<span className="navbar-brand mb-0 h1">Flexitime Tool</span>
<div className="collapse navbar-collapse">
<ul className="navbar-nav mr-auto">
<li className={getNavLinkClass("/")}>
<NavLink to="/" className="nav-link" exact>
Home
</NavLink>
</li>
<li className={getNavLinkClass("/users")}>
<NavLink to="/users" className="nav-link">
User List
</NavLink>
</li>
<li className={getNavLinkClass("/about")}>
<NavLink to="/about" className="nav-link" exact>
About
</NavLink>
</li>
{/*<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Dropdown
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Something else here</a>
</div>
</li>*/}
</ul>
<div className="my-2">
<button className="btn btn-outline-light my-2 my-sm-0" type="button">Account</button>
</div>
{/*<ul>*/}
{/* <li className={getNavLinkClass("/about") + " float-right"}>*/}
{/* <NavLink to="/about" className="nav-link" exact>*/}
{/* About*/}
{/* </NavLink>*/}
{/* </li>*/}
{/* </ul>*/}
</div>
{/*{" | "}*/}
{/*<NavLink to="/users" activeStyle={activeStyle}>User List</NavLink>*/}
{/*{" | "}*/}
{/*<NavLink to="/groups" activeStyle={activeStyle}>Group List</NavLink>*/}
{/*{" | "}*/}
{/*<NavLink to="/about" activeStyle={activeStyle} exact>About</NavLink>*/}
</nav>
</div>
);
};
export default withRouter(Header);

View File

@ -0,0 +1,37 @@
import React from "react";
import PropTypes from "prop-types";
const ImagePicker = ({ name, label, onChange, placeholder, value, error }) => {
let wrapperClass = "form-group";
if (error && error.length > 0) {
wrapperClass += " " + "has-error";
}
return (
<div className={wrapperClass}>
<label htmlFor={name}>{label}</label>
<div className="field">
<input
type="image"
name={name}
className="form-control"
placeholder={placeholder}
value={value}
onChange={onChange}
/>
{error && <div className="alert alert-danger">{error}</div>}
</div>
</div>
);
};
ImagePicker.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
value: PropTypes.string,
error: PropTypes.string
};
export default ImagePicker;

View File

@ -0,0 +1,37 @@
import React from "react";
import PropTypes from "prop-types";
const NumberInput = ({ name, label, onChange, placeholder, value, error }) => {
let wrapperClass = "form-group";
if (error && error.length > 0) {
wrapperClass += " has-error";
}
return (
<div className={wrapperClass}>
<label htmlFor={name}>{label}</label>
<div className="field">
<input
type="number"
name={name}
className="form-control"
placeholder={placeholder}
value={value}
onChange={onChange}
/>
{error && <div className="alert alert-danger">{error}</div>}
</div>
</div>
);
};
NumberInput.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.number,
value: PropTypes.number,
error: PropTypes.string
};
export default NumberInput;

View File

@ -0,0 +1,49 @@
import React from "react";
import PropTypes from "prop-types";
const SelectInput = ({
name,
label,
onChange,
defaultOption,
value,
error,
options
}) => {
return (
<div className="form-group">
<label htmlFor={name}>{label}</label>
<div className="field">
{/* Note, value is set here rather than on the option - docs: https://facebook.github.io/react/docs/forms.html */}
<select
name={name}
value={value}
onChange={onChange}
className="form-control"
>
<option value="">{defaultOption}</option>
{options.map(option => {
return (
<option key={option.value} value={option.value}>
{option.text}
</option>
);
})}
</select>
{error && <div className="alert alert-danger">{error}</div>}
</div>
</div>
);
};
SelectInput.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
defaultOption: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
error: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.object)
};
export default SelectInput;

View File

@ -0,0 +1,7 @@
import React from "react";
import "../../styles/Spinner.css";
const Spinner = () => {
return <div className="loader"> Loading...</div>;
}
export default Spinner;

View File

@ -0,0 +1,40 @@
import React from "react";
import PropTypes from "prop-types";
const TextInput = ({ name, label, onChange, placeholder, value, error }) => {
let wrapperClass = "form-group";
if (error && error.length > 0) {
wrapperClass += " has-error";
}
return (
<div className={wrapperClass}>
<label htmlFor={name}>{label}</label>
<div className="field">
<input
type="text"
name={name}
className="form-control"
placeholder={placeholder}
value={value}
onChange={onChange}
/>
{error && <div className="alert alert-danger">{error}</div>}
</div>
</div>
);
};
TextInput.defaultProps = {
value: ""
};
TextInput.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
value: PropTypes.string,
error: PropTypes.string
};
export default TextInput;

View File

@ -0,0 +1,40 @@
import React from "react";
import userState from "../../models/userState"
import PropTypes from "prop-types";
import "../../styles/StateIndicator.css";
class UserState extends React.Component {
convertStateToColor(state) {
let baseClass = "badge ";
switch (state) {
case userState.IN:
return baseClass + "badge-success";
case userState.OUT:
return baseClass + "badge-danger";
case userState.OUT_OF_OFFICE:
return baseClass + "badge-warning";
case userState.REMOTE:
return baseClass + "badge-info";
case userState.UNKNOWN:
default:
return baseClass + "badge-dark";
}
}
render() {
let stateClass = this.convertStateToColor(this.props.state);
let classes = stateClass + " stateIndicator p-2";
return (
<span className={classes}>{this.props.state}</span>
);
}
}
UserState.defaultProps = {
state: userState.UNKNOWN
};
UserState.propTypes = {
state: PropTypes.string
};
export default UserState;

View File

@ -0,0 +1,37 @@
import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
class GroupList extends React.Component {
render() {
return (
<table className="table table-striped">
<thead>
<tr>
<th className="text-center">Name</th>
<th className="text-center">User Count</th>
<th />
</tr>
</thead>
<tbody>
{this.props.groups.map(group => {
return (
<tr key={group.id}>
<td className="text-center">{group.name}</td>
<td className="text-center">{group.userCount}</td>
<td className="text-center">
<Link to={"/groups/" + group.id}>View</Link>
</td>
</tr>
);
})}
</tbody>
</table>
);
}
}
GroupList.propTypes = {
groups: PropTypes.array.isRequired
};
export default GroupList;

View File

@ -0,0 +1,42 @@
import React, { useState } from "react";
import { connect } from "react-redux";
import * as groupActions from "../../redux/actions/groupActions";
import Group from "../../models/Group";
import PropTypes from "prop-types";
import { bindActionCreators } from "redux";
import GroupList from "./GroupList";
class GroupsPage extends React.Component {
componentDidMount() {
this.props.actions.loadGroups().catch(error => {
alert("Loading groups failed " + error);
});
}
render() {
return (
<>
<h2>Groups</h2>
<GroupList groups={this.props.groups} />
</>
);
}
}
GroupsPage.propTypes = {
groups: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
groups: state.groups
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(groupActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(GroupsPage);

View File

@ -0,0 +1,14 @@
import React from "react";
import { Link } from "react-router-dom";
const HomePage = () => (
<div className="jumbotron">
<h1>PluralSight Administration</h1>
<p>React, Redux and React Router for ultra-responsive web-pages</p>
<Link to="about" className="btn btn-primary btn-lg">
Learn More
</Link>
</div>
);
export default HomePage;

View File

@ -0,0 +1,99 @@
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { loadUsers, saveUser } from "../../redux/actions/userActions";
import PropTypes from "prop-types";
import UserForm from "./UserForm";
import { User } from "../../models/User";
import Spinner from "../common/Spinner";
import { toast } from "react-toastify";
function ManagerUserPage({ users, loadUsers, saveUser, history, ...props }) {
const [user, setUser] = useState({ ...props.user });
const [errors, setErrors] = useState({});
const [saving, setSaving] = useState(false);
useEffect(
() => {
if (users.length === 0) {
loadUsers().catch(error => {
alert("Loading users failed " + error);
});
} else {
setUser({ ...props.user });
}
},
[props.user]
);
function formIsValid() {
const { firstName, lastName, hoursPerWeek } = user;
const errors = {};
if (!firstName) errors.firstName = "First Name Is Required";
if (!lastName) errors.lastName = "Last Name is Required";
if (!hoursPerWeek) errors.hoursPerWeek = "Hours Per Week is Required";
setErrors(errors);
return Object.keys(errors).length === 0;
}
function onChangeHandler(event) {
const { name, value } = event.target;
setUser(prevUser => ({
...prevUser,
[name]: name === "hoursPerWeek" ? parseInt(value, 10) : value
}));
}
function saveHandler(event) {
event.preventDefault();
if (!formIsValid()) return;
setSaving(true);
saveUser(user)
.then(() => {
toast.success("User Saved");
history.push("/users");
})
.catch(error => {
setSaving(false);
setErrors({ onSave: error.message });
});
}
return users.length === 0 ? (
<Spinner />
) : (
<UserForm
user={user}
errors={errors}
onChange={onChangeHandler}
onSave={saveHandler}
saving={saving}
/>
);
}
ManagerUserPage.propTypes = {
users: PropTypes.array.isRequired,
loadUsers: PropTypes.func.isRequired,
saveUser: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
};
export function getUserById(users, id) {
return users.find(usr => usr.id === id) || null;
}
function mapStateToProps(state, ownProps) {
const id = parseInt(ownProps.match.params.id);
const user =
id && state.users.length > 0 ? getUserById(state.users, id) : User;
return {
user: user,
users: state.users
};
}
const mapDispatchToProps = {
loadUsers,
saveUser
};
export default connect(mapStateToProps, mapDispatchToProps)(ManagerUserPage);

View File

@ -0,0 +1,62 @@
import React from "react";
import PropTypes from "prop-types";
import TextInput from "../common/TextInput";
import NumberInput from "../common/NumberInput";
const UserForm = ({
user,
onSave,
onChange,
saving = false,
errors = {}
}) => {
return (
<form onSubmit={onSave}>
<h2>{user.id ? "Edit" : "Add"} User</h2>
{errors.onSave && (
<div className="alert alert-danger" role="alert">
{errors.onSave}
</div>
)}
<TextInput
name="firstName"
label="First Name"
value={user.firstName}
onChange={onChange}
error={errors.firstName}
/>
<TextInput
name="lastName"
label="Last Name"
value={user.lastName}
onChange={onChange}
error={errors.lastName}
/>
{/* TODO: datetime input */}
{/* TODO: image upload */}
{/* TODO: wizard? */}
<NumberInput
name="hoursPerWeek"
label="Contracted Hours Per Week"
value={user.hoursPerWeek}
onChange={onChange}
error={errors.hoursPerWeek}
/>
<button type="submit" disabled={saving} className="btn btn-primary">
{saving ? "Saving..." : "Save"}
</button>
</form>
);
};
UserForm.propTypes = {
user: PropTypes.object.isRequired,
errors: PropTypes.object,
onSave: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
saving: PropTypes.bool
};
export default UserForm;

View File

@ -0,0 +1,32 @@
import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
import UserState from "../common/UserStateIndicator";
class UserGrid extends React.Component {
render() {
return (
<div className="row justify-content-start">
{this.props.users.map(user => {
return (
<div key={user.id} style={{height:120+'px'}} className="align-content-center col-2 border rounded m-1 p-2">
<h4 className="h-50 m-1">
{user.firstName} {user.lastName}
</h4>
<div className="h-25 m-2">
<UserState state={user.state} />
</div>
</div>
);
})}
</div>
//maybe justify..-around?{/*className="display-4"text-center */}
);
}
}
UserGrid.propTypes = {
users: PropTypes.array.isRequired
};
export default UserGrid;

View File

@ -0,0 +1,53 @@
import React from "react";
import PropTypes from "prop-types";
import { Link } from "react-router-dom";
import UserState from "../common/UserStateIndicator";
class UserList extends React.Component {
render() {
const { users, onDeleteClick } = this.props;
return (
<table className="table table-striped">
<thead>
<tr>
<th className="text-center">First Name</th>
<th className="text-center">Last Name</th>
<th className="text-center" width="120px;">
State
</th>
<th />
</tr>
</thead>
<tbody>
{users.map(user => {
return (
<tr key={user.id}>
<td className="text-center">{user.firstName}</td>
<td className="text-center">{user.lastName}</td>
<td>
<UserState state={user.state} />
</td>
<td className="text-center">
<Link className="btn btn-info mr-1" to={"/user/" + user.id}>Edit</Link>
<button
className="btn btn-outline-danger ml-1"
onClick={() => onDeleteClick(user)}
>
Delete
</button>
</td>
</tr>
);
})}
</tbody>
</table>
);
}
}
UserList.propTypes = {
users: PropTypes.array.isRequired,
onDeleteUser: PropTypes.func.isRequired
};
export default UserList;

View File

@ -0,0 +1,93 @@
import React, { useState } from "react";
import { connect } from "react-redux";
import * as userActions from "../../redux/actions/userActions";
import User from "../../models/User";
import PropTypes from "prop-types";
import { bindActionCreators } from "redux";
import UserList from "./UserList";
import UserGrid from "./UserGrid";
import { Link, Redirect } from "react-router-dom";
import "../../styles/toggleSlider.css";
import Spinner from "../common/Spinner";
import { toast } from "react-toastify";
class UserPage extends React.Component {
state = {
viewStyle: false,
redirectToAddUserPage: false
};
componentDidMount() {
if (this.props.users.length === 0) {
this.props.actions.loadUsers().catch(error => {
alert("Loading users failed " + error);
});
}
}
viewStyleChanged = () => {
this.setState({ viewStyle: !this.state.viewStyle });
};
handleDeleteUser = async user => {
toast.success("User " + user.firstName + " deleted");
try {
await this.props.actions.deleteUser(user);
} catch (error) {
toast.error("Delete of user: "+user.firstName+" failed. " + error.message, { autoClose: false });
}
};
render() {
let isGrid = this.state.viewStyle;
let view;
if (isGrid) {
view = <UserGrid users={this.props.users} />;
} else {
view = <UserList users={this.props.users} onDeleteClick={this.handleDeleteUser} />;
}
return (
<>
{this.props.loading ? (
<Spinner />
) : (
<>
<Link to={"/user"} className="btn btn-dark m-1">
Add User
</Link>
<label className="switch">
<input
ref="viewStyle"
type="checkbox"
defaultChecked="{isGrid}"
onChange={this.viewStyleChanged}
/>
<span className="slider round" />
</label>
<h2>Users</h2>
{view}
</>
)}
</>
);
}
}
UserPage.propTypes = {
users: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired
};
function mapStateToProps(state) {
return {
users: state.users,
loading: state.apiCallsInProgress > 0
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(userActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(UserPage);

View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -0,0 +1,26 @@
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import "bootstrap/dist/css/bootstrap.min.css";
import './index.css';
import App from './components/App';
import reportWebVitals from './reportWebVitals';
import './App.css';
import configureStore from './redux/configureStore';
import { Provider as ReduxProvider } from "react-redux";
const store = configureStore();
render(
<ReduxProvider store={store} >
<Router>
<App />
</Router>
</ReduxProvider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,9 @@
//CSharpToJs: auto
class Application {
constructor() {
this.id = 0;
this.name = null;
this.permissions = [];
}
}
export default Application;

View File

@ -0,0 +1,10 @@
//CSharpToJs: auto
class Group {
constructor() {
this.id = 0;
this.name = null;
this.userCount = 0;
this.isAssociatedToUser = false;
}
}
export default Group;

View File

@ -0,0 +1,10 @@
//CSharpToJs: auto
class Identifier {
constructor() {
this.id = 0;
this.uniqueId = null;
this.isAssociatedToUser = false;
this.lastUsed = "0001-01-01T00:00:00";
}
}
export default Identifier;

View File

@ -0,0 +1,8 @@
//CSharpToJs: auto
class LoginRequest {
constructor() {
this.username = null;
this.password = null;
}
}
export default LoginRequest;

View File

@ -0,0 +1,12 @@
//CSharpToJs: auto
class LoginResponse {
constructor() {
this.id = 0;
this.firstName = null;
this.lastName = null;
this.username = null;
this.token = null;
this.refreshToken = null;
}
}
export default LoginResponse;

View File

@ -0,0 +1,11 @@
//CSharpToJs: auto
import Application from './Application.js';
class Permission {
constructor() {
this.application = new Application();
this.name = null;
this.tag = null;
this.id = 0;
}
}
export default Permission;

View File

@ -0,0 +1,22 @@
//CSharpToJs: auto
import userState from "./userState";
export class User {
constructor() {
this.id = 0;
this.firstName = "";
this.lastName = "";
this.hoursPerWeek = 0.0;
this.isContractor = false;
this.associatedIdentifierCount = 0;
this.lastEventDateTime = "0001-01-01T00:00:00";
this.associatedIdentifiers = [];
this.groups = [];
this.state = userState.UNKNOWN;
this.lineManagerId = 0;
this.directReportIds = null;
this.password = null;
this.permissions = [];
}
}
//export default User;

View File

@ -0,0 +1,8 @@
const userState = {
UNKNOWN:"Unknown",
IN: "In",
OUT: "Out",
OUT_OF_OFFICE: "OutOfOffice",
REMOTE:"Remote"
}
export default userState;

View File

@ -0,0 +1,16 @@
export const CREATE_USER = "CREATE_USER";
export const GET_USERS = "GET_USER";
export const GET_USER_BY_ID = "GET_USER_BY_ID";
export const LOAD_USERS_SUCCESS = "LOAD_USERS_SUCCESS";
export const CREATE_USER_SUCCESS = "CREATE_USER_SUCCESS";
export const UPDATE_USER_SUCCESS = "UPDATE_USER_SUCCESS";
export const DELETE_USER_OPTIMISTIC = "DELETE_USER_OPTIMISTIC";
export const CREATE_GROUP = "CREATE_GROUP";
export const GET_GROUP = "GET_GROUP";
export const GET_GROUP_BY_ID = "GET_GROUP_BY_ID";
export const LOAD_GROUPS_SUCCESS = "LOAD_GROUPS_SUCCESS";
export const BEGIN_API_CALL = "BEGIN_API_CALL";
export const API_CALL_ERROR = "API_CALL_ERROR";

View File

@ -0,0 +1,8 @@
import * as actionTypes from "./actionTypes";
export function beginApiCall() {
return { type: actionTypes.BEGIN_API_CALL };
}
export function apiCallError() {
return { type: actionTypes.API_CALL_ERROR };
}

View File

@ -0,0 +1,33 @@
import * as types from "./actionTypes";
import * as groupsApi from "../../api/groupsApi";
import { beginApiCall, apiCallError } from "./apiStatusActions";
export function createGroup(group) {
return { type: types.CREATE_GROUP, group };
}
export function getGroups() {
return { type: types.GET_GROUP };
}
export function getGroup(id) {
return { type: types.GET_GROUP_BY_ID, id };
}
export function loadGroupsSuccess(groups) {
return { type: types.LOAD_GROUPS_SUCCESS, groups };
}
export function loadGroups() {
return function(dispatch) {
dispatch(beginApiCall());
return groupsApi
.getGroups()
.then(groups => {
dispatch(loadGroupsSuccess(groups));
})
.catch(error => {
dispatch(apiCallError(error));
throw error;
});
};
}

View File

@ -0,0 +1,60 @@
import * as types from "./actionTypes";
import * as userApi from "../../api/userApi";
import { beginApiCall, apiCallError } from "./apiStatusActions";
export function getUsers() {
return { type: types.GET_USERS };
}
export function getUserById(id) {
return { type: types.GET_USER_BY_ID, id };
}
export function loadUsersSuccess(users) {
return { type: types.LOAD_USERS_SUCCESS, users };
}
export function updateUserSuccess(user) {
return { type: types.UPDATE_USER_SUCCESS, user };
}
export function createUserSuccess(user) {
return { type: types.CREATE_USER_SUCCESS, user };
}
export function deleteUserOptimistic(user) {
return { type: types.DELETE_USER_OPTIMISTIC, user: user };
}
export function loadUsers() {
return function(dispatch) {
dispatch(beginApiCall());
return userApi
.getUsers()
.then(users => {
dispatch(loadUsersSuccess(users));
})
.catch(error => {
dispatch(apiCallError(error));
throw error;
});
};
}
export function saveUser(user) {
return function(dispatch) {
dispatch(beginApiCall());
return userApi
.saveUser(user)
.then(savedUser => {
user.id
? dispatch(updateUserSuccess(savedUser))
: dispatch(createUserSuccess(savedUser));
})
.catch(error => {
dispatch(apiCallError(error));
throw error;
});
};
}
export function deleteUser(user) {
return function(dispatch) {
dispatch(deleteUserOptimistic(user));
return userApi.deleteUser(user.id);
};
}

View File

@ -0,0 +1,14 @@
import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from './reducers';
import reduxImmutableStateInvariant from "redux-immutable-state-invariant";
import thunk from 'redux-thunk';
export default function configureStore(initialState) {
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
return createStore(
rootReducer,
initialState,
composeEnhancers(applyMiddleware(thunk, reduxImmutableStateInvariant()))
);
}

View File

@ -0,0 +1,21 @@
import * as actionTypes from "../actions/actionTypes";
import initialState from "./initialState";
function actionTypeEndsInSuccess(type) {
return type.substring(type.length - 8) === "_SUCCESS";
}
export default function apiCallStatusReducer(
state = initialState.apiCallsInProcess,
action
) {
if (action.type === actionTypes.BEGIN_API_CALL) {
return state + 1;
} else if (
action.type === actionTypes.API_CALL_ERROR ||
actionTypeEndsInSuccess(action.type)
) {
return state - 1;
}
return state;
}

View File

@ -0,0 +1,31 @@
import * as actionTypes from "../actions/actionTypes";
import initialState from "./initialState";
export default function groupsReducer(state = initialState.groups, action) {
switch (action.type) {
case actionTypes.CREATE_GROUP: {
let group = action.group;
if (state.length === 0) {
group.id = 1;
} else {
group.id =
Math.max.apply(
Math,
state.map(function(g) {
return g.id;
})
) + 1;
}
return [...state, { ...group }];
}
case actionTypes.LOAD_GROUPS_SUCCESS: {
return action.groups;
}
case actionTypes.GET_GROUP_BY_ID: {
let group = state.find(x => x.id === action.group.id);
return group;
}
default:
return state;
}
}

View File

@ -0,0 +1,12 @@
import { combineReducers } from "redux";
import users from "./userReducer";
import groups from "./groupsReducer";
import apiCallsInProgress from "./apiStatusReducer";
const rootReducer = combineReducers({
users,
groups,
apiCallsInProgress
});
export default rootReducer;

View File

@ -0,0 +1,5 @@
export default {
users: [],
groups: [],
apiCallsInProcess:0
};

View File

@ -0,0 +1,28 @@
import * as actionTypes from "../actions/actionTypes";
import initialState from "./initialState";
export default function userReducer(state = initialState.users, action) {
switch (action.type) {
case actionTypes.CREATE_USER_SUCCESS: {
return [...state, { ...action.user }];
}
case actionTypes.UPDATE_USER_SUCCESS: {
return state.map(
user => (user.id === action.user.id ? action.user : user)
);
}
case actionTypes.LOAD_USERS_SUCCESS: {
return action.users;
}
case actionTypes.GET_USER_BY_ID: {
let user = state.find(x => x.id === action.user.id);
return user;
}
case actionTypes.DELETE_USER_OPTIMISTIC:
{
return state.filter(user => user.id !== action.user.id);
}
default:
return state;
}
}

View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -0,0 +1,9 @@
function isLoggedIn() {
}
function isAuthorisedFor(featureTag) {
}

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -0,0 +1,93 @@
/*https://projects.lukehaas.me/css-loaders/*/
.loader {
color: #ff9700;
font-size: 90px;
text-indent: -9999em;
overflow: hidden;
width: 1em;
height: 1em;
border-radius: 50%;
margin: 72px auto;
position: relative;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: load6 1.7s infinite ease, round 1.7s infinite ease;
animation: load6 1.7s infinite ease, round 1.7s infinite ease;
}
@-webkit-keyframes load6 {
0% {
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
}
5%, 95% {
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
}
10%, 59% {
box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
}
20% {
box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;
}
38% {
box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;
}
100% {
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
}
}
@keyframes load6 {
0% {
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
}
5%, 95% {
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
}
10%, 59% {
box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em, -0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
}
20% {
box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em, -0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;
}
38% {
box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em, -0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;
}
100% {
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em, 0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
}
}
@-webkit-keyframes round {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes round {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,3 @@
span.stateIndicator {
display: block;
}

View File

@ -0,0 +1,62 @@
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}

View File

@ -0,0 +1,116 @@
/*
This uses json-server, but with the module approach: https://github.com/typicode/json-server#module
Downside: You can't pass the json-server command line options.
Instead, can override some defaults by passing a config object to jsonServer.defaults();
You have to check the source code to set some items.
Examples:
Validation/Customization: https://github.com/typicode/json-server/issues/266
Delay: https://github.com/typicode/json-server/issues/534
ID: https://github.com/typicode/json-server/issues/613#issuecomment-325393041
Relevant source code: https://github.com/typicode/json-server/blob/master/src/cli/run.js
*/
/* eslint-disable no-console */
const fs = require("fs");
const jsonServer = require("json-server");
const server = jsonServer.create();
const path = require("path");
const dbPath = path.join(__dirname, "db.json");
const router = jsonServer.router(dbPath);
let maxId = getMaxId(dbPath);
// Can pass a limited number of options to this to override (some) defaults. See https://github.com/typicode/json-server#api
const middlewares = jsonServer.defaults({
// Display json-server's built in homepage when json-server starts.
static: "node_modules/json-server/public"
});
// Set default middlewares (logger, static, cors and no-cache)
server.use(middlewares);
// To handle POST, PUT and PATCH you need to use a body-parser. Using JSON Server's bodyParser
server.use(jsonServer.bodyParser);
// Simulate delay on all requests
server.use(function(req, res, next) {
setTimeout(next, 2000);
});
// Declaring custom routes below. Add custom routes before JSON Server router
// Add createdAt to all POSTS
server.use((req, res, next) => {
//if (req.method === "POST") {
// req.body.createdAt = Date.now();
//}
// Continue to JSON Server router
next();
});
//create
server.post("/users/", function(req, res, next) {
const error = validateUser(req.body);
if (error) {
res.status(400).send(error);
} else {
req.body.id = maxId;
//req.body.slug = createSlug(req.body.title); // Generate a slug for new courses.
next();
}
});
//update
server.put("/users", function(req, res, next) {
const error = validateUser(req.body);
if (error) {
res.status(400).send(error);
} else {
req.body.id = req.body.id;
next();
}
});
// Use default router
server.use(router);
// Start server
const port = 3001;
server.listen(port, () => {
console.log(`JSON Server is running on port ${port}`);
});
// Centralized logic
// Returns a URL friendly slug
function createSlug(value) {
return value
.replace(/[^a-z0-9_]+/gi, "-")
.replace(/^-|-$/g, "")
.toLowerCase();
}
function validateUser(user) {
if (!user.id || user.id === 0) {
maxId = maxId + 1;
user.id = maxId;
} else if (user.id < 0 || user.id > maxId) {
return "Invalid User Id";
}
if (!user.firstName) return "First Name is required.";
if (!user.lastName) return "Last Name is required.";
if (user.hoursPerWeek <= 0) return "Hours Per Week must be a positive integer";
return "";
}
function getMaxId(dbPath) {
let rawData = fs.readFileSync(dbPath);
let data = JSON.parse(rawData);
let userArray = data.users;
return Math.max.apply(
Math,
userArray.map(function(o) {
return o.id;
})
);
}

View File

@ -0,0 +1,12 @@
/* eslint-disable no-console */
const fs = require("fs");
const path = require("path");
const mockData = require("./mockData");
const { users, authors, groups } = mockData;
const data = JSON.stringify({ users, authors, groups });
const filepath = path.join(__dirname, "db.json");
fs.writeFile(filepath, data, function(err) {
err ? console.log(err) : console.log("Mock DB created.");
});

View File

@ -0,0 +1,51 @@
{
"users": [
{
"id": 2,
"username": "lawrenb",
"firstName": "Brett",
"lastName": "Lawrence",
"hoursPerWeek": 37,
"state": "Remote"
},
{
"id": 3,
"username": "bustamae",
"firstName": "Eugene",
"lastName": "Bustamante",
"hoursPerWeek": 37,
"state": "Out"
}
],
"authors": [
{
"id": 1,
"name": "Cory House"
},
{
"id": 2,
"name": "Scott Allen"
},
{
"id": 3,
"name": "Dan Wahlin"
}
],
"groups": [
{
"id": 1,
"name": "RTPS",
"userCount": 4
},
{
"id": 2,
"name": "Embedded Rating",
"userCount": 14
},
{
"id": 3,
"name": "Rules Engine",
"userCount": 2
}
]
}

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