Merge main

Picked up on most recent change with DeleteSource func
This commit is contained in:
Nicholas Murray 2023-10-16 07:49:45 -07:00
commit d154d9e811
168 changed files with 9217 additions and 918 deletions

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="fasten" uuid="d164a8cf-0ebf-4a73-b7a0-59097243fbce">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/fasten.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.40.1/org/xerial/sqlite-jdbc/3.40.1.0/sqlite-jdbc-3.40.1.0.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

View File

@ -21,7 +21,8 @@ var goos string
var goarch string var goarch string
func main() { func main() {
log.Print("Starting fasten-onprem")
defer log.Print("Finished fasten-onprem")
appconfig, err := config.Create() appconfig, err := config.Create()
if err != nil { if err != nil {
fmt.Printf("FATAL: %+v\n", err) fmt.Printf("FATAL: %+v\n", err)

View File

@ -884,6 +884,62 @@ func (gr *GormRepository) GetSources(ctx context.Context) ([]models.SourceCreden
return sourceCreds, results.Error return sourceCreds, results.Error
} }
func (gr *GormRepository) DeleteSource(ctx context.Context, sourceId string) (int64, error) {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil {
return 0, currentUserErr
}
if strings.TrimSpace(sourceId) == "" {
return 0, fmt.Errorf("sourceId cannot be blank")
}
//delete all resources for this source
sourceUUID, err := uuid.Parse(sourceId)
if err != nil {
return 0, err
}
rowsEffected := int64(0)
resourceTypes := databaseModel.GetAllowedResourceTypes()
for _, resourceType := range resourceTypes {
tableName, err := databaseModel.GetTableNameByResourceType(resourceType)
if err != nil {
return 0, err
}
results := gr.GormClient.WithContext(ctx).
Where(models.OriginBase{
UserID: currentUser.ID,
SourceID: sourceUUID,
}).
Table(tableName).
Delete(&models.ResourceBase{})
rowsEffected += results.RowsAffected
if results.Error != nil {
return rowsEffected, results.Error
}
}
//delete relatedResources entries
results := gr.GormClient.WithContext(ctx).
Where(models.RelatedResource{ResourceBaseUserID: currentUser.ID, ResourceBaseSourceID: sourceUUID}).
Delete(&models.RelatedResource{})
if results.Error != nil {
return rowsEffected, results.Error
}
//soft delete the source credential
results = gr.GormClient.WithContext(ctx).
Where(models.SourceCredential{
ModelBase: models.ModelBase{
ID: sourceUUID,
},
UserID: currentUser.ID,
}).
Delete(&models.SourceCredential{})
rowsEffected += results.RowsAffected
return rowsEffected, results.Error
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Background Job // Background Job
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -38,6 +38,7 @@ type DatabaseRepository interface {
GetSourceSummary(context.Context, string) (*models.SourceSummary, error) GetSourceSummary(context.Context, string) (*models.SourceSummary, error)
GetSources(context.Context) ([]models.SourceCredential, error) GetSources(context.Context) ([]models.SourceCredential, error)
UpdateSource(ctx context.Context, sourceCreds *models.SourceCredential) error UpdateSource(ctx context.Context, sourceCreds *models.SourceCredential) error
DeleteSource(ctx context.Context, sourceId string) (int64, error)
CreateGlossaryEntry(ctx context.Context, glossaryEntry *models.Glossary) error CreateGlossaryEntry(ctx context.Context, glossaryEntry *models.Glossary) error
GetGlossaryEntry(ctx context.Context, code string, codeSystem string) (*models.Glossary, error) GetGlossaryEntry(ctx context.Context, code string, codeSystem string) (*models.Glossary, error)

View File

@ -1,6 +1,7 @@
package models package models
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk" "github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
@ -134,7 +135,90 @@ func (s *SourceCredential) IsDynamicClient() bool {
return len(s.DynamicClientRegistrationMode) > 0 return len(s.DynamicClientRegistrationMode) > 0
} }
// This method will generate a new keypair, register a new dynamic client with the provider
// it will set the following fields:
// - DynamicClientJWKS
// - DynamicClientId
func (s *SourceCredential) RegisterDynamicClient() error {
//this source requires dynamic client registration
// see https://fhir.epic.com/Documentation?docId=Oauth2&section=Standalone-Oauth2-OfflineAccess-0
// Generate a public-private key pair
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
if err != nil {
return fmt.Errorf("an error occurred while generating device-specific keypair for dynamic client: %w", err)
}
//store in sourceCredential
serializedKeypair, err := jwk.JWKSerialize(sourceSpecificClientKeyPair)
if err != nil {
return fmt.Errorf("an error occurred while serializing keypair for dynamic client: %w", err)
}
s.DynamicClientJWKS = []map[string]string{
serializedKeypair,
}
//generate dynamic client registration request
payload := ClientRegistrationRequest{
SoftwareId: s.ClientId,
Jwks: ClientRegistrationRequestJwks{
Keys: []ClientRegistrationRequestJwksKey{
{
KeyType: "RSA",
KeyId: serializedKeypair["kid"],
Modulus: serializedKeypair["n"],
PublicExponent: serializedKeypair["e"],
},
},
},
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("an error occurred while marshalling dynamic client registration request: %w", err)
}
//http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair))))
req, err := http.NewRequest(http.MethodPost, s.RegistrationEndpoint, bytes.NewBuffer(payloadBytes))
if err != nil {
return fmt.Errorf("an error occurred while generating dynamic client registration request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.AccessToken))
registrationResponse, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("an error occurred while sending dynamic client registration request: %w", err)
}
defer registrationResponse.Body.Close()
if registrationResponse.StatusCode >= 300 || registrationResponse.StatusCode < 200 {
b, err := io.ReadAll(registrationResponse.Body)
if err == nil {
log.Printf("Error Response body: %s", string(b))
}
return fmt.Errorf("an error occurred while reading dynamic client registration response, status code was not 200: %d", registrationResponse.StatusCode)
}
//read response
var registrationResponseBytes ClientRegistrationResponse
err = json.NewDecoder(registrationResponse.Body).Decode(&registrationResponseBytes)
if err != nil {
return fmt.Errorf("an error occurred while parsing dynamic client registration response: %w", err)
}
//store the dynamic client id
s.DynamicClientId = registrationResponseBytes.ClientId
return nil
}
// this will set/update the AccessToken and Expiry using the dynamic client credentials // this will set/update the AccessToken and Expiry using the dynamic client credentials
// it will set the following fields:
// - AccessToken
// - ExpiresAt
func (s *SourceCredential) RefreshDynamicClientAccessToken() error { func (s *SourceCredential) RefreshDynamicClientAccessToken() error {
if len(s.DynamicClientRegistrationMode) == 0 { if len(s.DynamicClientRegistrationMode) == 0 {
return fmt.Errorf("dynamic client registration mode not set") return fmt.Errorf("dynamic client registration mode not set")

View File

@ -5,124 +5,29 @@
"description": "An example dashboard to show-off the power of Fasten widgets", "description": "An example dashboard to show-off the power of Fasten widgets",
"widgets": [ "widgets": [
{ {
"title_text": "Diabetes Tracking", "title_text": "Records Summary",
"description_text": "Track key metrics for your chronic disease (eg. Diabetes). The data within this widget is not reflective of your health record, and is only present for demonstrational purposes.", "description_text": "Track key metrics for your chronic disease (eg. Diabetes). The data within this widget is not reflective of your health record, and is only present for demonstrational purposes.",
"x": 0, "x": 0,
"y": 0, "y": 0,
"width": 8, "width": 8,
"height": 5, "height": 6,
"item_type": "complex-line-widget" "item_type": "records-summary-widget"
},
{
"title_text": "Weight",
"description_text": "",
"x": 8,
"y": 0,
"width": 2,
"height": 2,
"item_type": "simple-line-chart-widget",
"queries": [{
"q": {
"select": [
"valueQuantity.value as data",
"valueQuantity.unit as unit",
"(effectiveDateTime | issued).first() as label"
],
"from": "Observation",
"where": {
"code": "http://loinc.org|29463-7,http://loinc.org|3141-9,http://snomed.info/sct|27113001"
}
}
}],
"parsing": {
"xAxisKey": "label",
"yAxisKey": "data"
}
},
{
"title_text": "Height",
"description_text": "",
"x": 10,
"y": 0,
"width": 2,
"height": 2,
"item_type": "simple-line-chart-widget",
"queries": [{
"q": {
"select": [
"valueQuantity.value as data",
"valueQuantity.unit as unit",
"(effectiveDateTime | issued).first() as label"
],
"from": "Observation",
"where": {
"code": "http://loinc.org|8302-2"
}
}
}],
"parsing": {
"xAxisKey": "label",
"yAxisKey": "data"
}
},
{
"title_text": "Blood Pressure",
"description_text": "How much pressure your blood is exerting against your artery walls when the heart beats",
"x": 8,
"y": 2,
"width": 4,
"height": 3,
"item_type": "grouped-bar-chart-widget",
"queries": [
{
"q": {
"select": [
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.value as data",
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.unit as unit"
],
"from": "Observation",
"where": {
"componentCode": "http://loinc.org|8462-4"
}
},
"dataset_options": {
"label": "Diastolic"
}
},
{
"q": {
"select": [
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.value as data",
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.unit as unit"
],
"from": "Observation",
"where": {
"componentCode": "http://loinc.org|8480-6"
}
},
"dataset_options": {
"label": "Systolic"
}
}],
"parsing": {
"xAxisKey": "id",
"yAxisKey": "data"
}
}, },
{ {
"title_text": "Patient Vitals", "title_text": "Patient Vitals",
"description_text": "", "description_text": "",
"x": 0, "x": 8,
"y": 5, "y": 0,
"width": 4, "width": 4,
"height": 5, "height": 6,
"item_type": "patient-vitals-widget" "item_type": "patient-vitals-widget"
}, },
{ {
"title_text": "Observations by Type", "title_text": "Observations by Type",
"description_text": "", "description_text": "",
"x": 4, "x": 0,
"y": 5, "y": 6,
"width": 8, "width": 8,
"height": 5, "height": 5,
"item_type": "donut-chart-widget", "item_type": "donut-chart-widget",
@ -141,11 +46,113 @@
"key": "value" "key": "value"
} }
}, },
{
"title_text": "Weight",
"description_text": "",
"x": 8,
"y": 6,
"width": 2,
"height": 2,
"item_type": "simple-line-chart-widget",
"queries": [{
"q": {
"select": [
"valueQuantity.value as data",
"valueQuantity.unit as unit",
"(effectiveDateTime | issued).first() as label"
],
"from": "Observation",
"where": {
"code": "http://loinc.org|29463-7,http://loinc.org|3141-9,http://snomed.info/sct|27113001"
},
"limit": 50
}
}],
"parsing": {
"xAxisKey": "label",
"yAxisKey": "data"
}
},
{
"title_text": "Height",
"description_text": "",
"x": 10,
"y": 6,
"width": 2,
"height": 2,
"item_type": "simple-line-chart-widget",
"queries": [{
"q": {
"select": [
"valueQuantity.value as data",
"valueQuantity.unit as unit",
"(effectiveDateTime | issued).first() as label"
],
"from": "Observation",
"where": {
"code": "http://loinc.org|8302-2"
},
"limit": 50
}
}],
"parsing": {
"xAxisKey": "label",
"yAxisKey": "data"
}
},
{
"title_text": "Blood Pressure",
"description_text": "How much pressure your blood is exerting against your artery walls when the heart beats",
"x": 8,
"y": 8,
"width": 4,
"height": 3,
"item_type": "grouped-bar-chart-widget",
"queries": [
{
"q": {
"select": [
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.value as data",
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8462-4').valueQuantity.unit as unit"
],
"from": "Observation",
"where": {
"componentCode": "http://loinc.org|8462-4"
},
"limit": 50
},
"dataset_options": {
"label": "Diastolic"
}
},
{
"q": {
"select": [
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.value as data",
"component.where(code.coding.system = 'http://loinc.org' and code.coding.code = '8480-6').valueQuantity.unit as unit"
],
"from": "Observation",
"where": {
"componentCode": "http://loinc.org|8480-6"
},
"limit": 50
},
"dataset_options": {
"label": "Systolic"
}
}],
"parsing": {
"xAxisKey": "id",
"yAxisKey": "data"
}
},
{ {
"title_text": "Compliance", "title_text": "Compliance",
"description_text": "Use to track important healthcare and medical tasks.", "description_text": "Use to track important healthcare and medical tasks.",
"x": 0, "x": 0,
"y": 10, "y": 11,
"width": 4, "width": 4,
"height": 2, "height": 2,
"item_type": "dual-gauges-widget", "item_type": "dual-gauges-widget",
@ -184,7 +191,7 @@
"title_text": "Recent Encounters", "title_text": "Recent Encounters",
"description_text": "Recent interactions with healthcare providers", "description_text": "Recent interactions with healthcare providers",
"x": 4, "x": 4,
"y": 10, "y": 11,
"width": 8, "width": 8,
"height": 4, "height": 4,
"item_type": "table-widget", "item_type": "table-widget",
@ -197,7 +204,8 @@
"participant.individual.display as provider" "participant.individual.display as provider"
], ],
"from": "Encounter", "from": "Encounter",
"where": {} "where": {},
"limit": 50
} }
}], }],
"parsing": { "parsing": {

View File

@ -0,0 +1,26 @@
{
"id": "secondary",
"schema_version": "1.0",
"title": "Secondary Dashboard",
"description": "An second dashboard to show-off the flexibility of the dashboard system.",
"widgets": [
{
"title_text": "Records Summary",
"description_text": "Track key metrics for your chronic disease (eg. Diabetes). The data within this widget is not reflective of your health record, and is only present for demonstrational purposes.",
"x": 0,
"y": 0,
"width": 8,
"height": 6,
"item_type": "records-summary-widget"
},
{
"title_text": "Care Team",
"description_text": "Track key metrics for your chronic disease (eg. Diabetes). The data within this widget is not reflective of your health record, and is only present for demonstrational purposes.",
"x": 8,
"y": 0,
"width": 4,
"height": 6,
"item_type": "image-list-group-widget"
}
]
}

View File

@ -1,24 +1,21 @@
package handler package handler
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg" "github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database" "github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus" "github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/jwk"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models" "github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/fastenhealth/fasten-sources/clients/factory" "github.com/fastenhealth/fasten-sources/clients/factory"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg" sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
) )
func CreateSource(c *gin.Context) { func CreateReconnectSource(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry) logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository) databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
@ -39,91 +36,13 @@ func CreateSource(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"success": false}) c.JSON(http.StatusBadRequest, gin.H{"success": false})
return return
} }
//this source requires dynamic client registration
// see https://fhir.epic.com/Documentation?docId=Oauth2&section=Standalone-Oauth2-OfflineAccess-0
// Generate a public-private key pair err := sourceCred.RegisterDynamicClient()
// Must be 2048 bits (larger keys will silently fail when used with Epic, untested on other providers)
sourceSpecificClientKeyPair, err := jwk.JWKGenerate()
if err != nil { if err != nil {
logger.Errorln("An error occurred while generating device-specific keypair for dynamic client", err) logger.Errorln("An error occurred while registering dynamic client", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false}) c.JSON(http.StatusBadRequest, gin.H{"success": false})
return return
} }
//store in sourceCredential
serializedKeypair, err := jwk.JWKSerialize(sourceSpecificClientKeyPair)
if err != nil {
logger.Errorln("An error occurred while serializing keypair for dynamic client", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
sourceCred.DynamicClientJWKS = []map[string]string{
serializedKeypair,
}
//generate dynamic client registration request
payload := models.ClientRegistrationRequest{
SoftwareId: sourceCred.ClientId,
Jwks: models.ClientRegistrationRequestJwks{
Keys: []models.ClientRegistrationRequestJwksKey{
{
KeyType: "RSA",
KeyId: serializedKeypair["kid"],
Modulus: serializedKeypair["n"],
PublicExponent: serializedKeypair["e"],
},
},
},
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
logger.Errorln("An error occurred while marshalling dynamic client registration request", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
//http.Post("https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token", "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=%s&scope=system/Patient.read", sourceSpecificClientKeyPair))))
req, err := http.NewRequest(http.MethodPost, sourceCred.RegistrationEndpoint, bytes.NewBuffer(payloadBytes))
if err != nil {
logger.Errorln("An error occurred while generating dynamic client registration request", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sourceCred.AccessToken))
registrationResponse, err := http.DefaultClient.Do(req)
if err != nil {
logger.Errorln("An error occurred while sending dynamic client registration request", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
defer registrationResponse.Body.Close()
if registrationResponse.StatusCode >= 300 || registrationResponse.StatusCode < 200 {
logger.Errorln("An error occurred while reading dynamic client registration response, status code was not 200", registrationResponse.StatusCode)
b, err := io.ReadAll(registrationResponse.Body)
if err == nil {
logger.Printf("Error Response body: %s", string(b))
}
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
//read response
var registrationResponseBytes models.ClientRegistrationResponse
err = json.NewDecoder(registrationResponse.Body).Decode(&registrationResponseBytes)
if err != nil {
logger.Errorln("An error occurred while parsing dynamic client registration response", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false})
return
}
//store the dynamic client id
sourceCred.DynamicClientId = registrationResponseBytes.ClientId
//generate a JWT token and then use it to get an access token for the dynamic client //generate a JWT token and then use it to get an access token for the dynamic client
err = sourceCred.RefreshDynamicClientAccessToken() err = sourceCred.RefreshDynamicClientAccessToken()
if err != nil { if err != nil {
@ -133,11 +52,22 @@ func CreateSource(c *gin.Context) {
} }
} }
err := databaseRepo.CreateSource(c, &sourceCred) if sourceCred.ID != uuid.Nil {
if err != nil { //reconnect
logger.Errorln("An error occurred while storing source credential", err) err := databaseRepo.UpdateSource(c, &sourceCred)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) if err != nil {
return logger.Errorln("An error occurred while reconnecting source credential", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
} else {
//create source for the first time
err := databaseRepo.CreateSource(c, &sourceCred)
if err != nil {
logger.Errorln("An error occurred while storing source credential", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
} }
// after creating the source, we should do a bulk import (in the background) // after creating the source, we should do a bulk import (in the background)
@ -312,3 +242,16 @@ func ListSource(c *gin.Context) {
} }
c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCreds}) c.JSON(http.StatusOK, gin.H{"success": true, "data": sourceCreds})
} }
func DeleteSource(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
rowsEffected, err := databaseRepo.DeleteSource(c, c.Param("sourceId"))
if err != nil {
logger.Errorln("An error occurred while deleting source credential", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": rowsEffected})
}

View File

@ -1,7 +1,6 @@
package handler package handler
import ( import (
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg" "github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/config" "github.com/fastenhealth/fasten-onprem/backend/pkg/config"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database" "github.com/fastenhealth/fasten-onprem/backend/pkg/database"
@ -53,16 +52,6 @@ func UnsafeRequestSource(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return return
} }
//TODO: if source has been updated, we should save the access/refresh token.
//if updatedSource != nil {
// logger.Warnf("TODO: source credential has been updated, we should store it in the database: %v", updatedSource)
// // err := databaseRepo.CreateSource(c, updatedSource)
// // if err != nil {
// // logger.Errorf("An error occurred while updating source credential %v", err)
// // c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
// // return
// // }
//}
var resp map[string]interface{} var resp map[string]interface{}
@ -75,6 +64,23 @@ func UnsafeRequestSource(c *gin.Context) {
//make sure we include all query string parameters with the raw request. //make sure we include all query string parameters with the raw request.
parsedUrl.RawQuery = c.Request.URL.Query().Encode() parsedUrl.RawQuery = c.Request.URL.Query().Encode()
//make sure we store the source credential information in the database, even if the request fails
defer func() {
//update source incase the access token/refresh token has been updated
sourceCredential := client.GetSourceCredential()
sourceCredentialConcrete, ok := sourceCredential.(*models.SourceCredential)
if !ok {
logger.Errorln("An error occurred while updating source credential, source credential is not of type *models.SourceCredential")
return
}
err = databaseRepo.UpdateSource(c, sourceCredentialConcrete)
if err != nil {
logger.Errorf("An error occurred while updating source credential: %v", err)
return
}
logger.Info("Successfully updated source credential")
}()
_, err = client.GetRequest(parsedUrl.String(), &resp) _, err = client.GetRequest(parsedUrl.String(), &resp)
if err != nil { if err != nil {
logger.Errorf("Error making raw request, %v", err) logger.Errorf("Error making raw request, %v", err)
@ -82,21 +88,6 @@ func UnsafeRequestSource(c *gin.Context) {
return return
} }
//update source incase the access token/refresh token has been updated
sourceCredential := client.GetSourceCredential()
sourceCredentialConcrete, ok := sourceCredential.(*models.SourceCredential)
if !ok {
logger.Errorln("An error occurred while updating source credential, source credential is not of type *models.SourceCredential")
c.JSON(http.StatusOK, gin.H{"success": false, "data": resp, "error": fmt.Errorf("An error occurred while updating source credential, source credential is not of type *models.SourceCredential")})
return
}
err = databaseRepo.UpdateSource(c, sourceCredentialConcrete)
if err != nil {
logger.Errorf("An error occurred while updating source credential: %v", err)
c.JSON(http.StatusOK, gin.H{"success": false, "data": resp, "error": fmt.Errorf("An error occurred while updating source credential: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": resp}) c.JSON(http.StatusOK, gin.H{"success": true, "data": resp})
} }

View File

@ -64,10 +64,11 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
{ {
secure.GET("/summary", handler.GetSummary) secure.GET("/summary", handler.GetSummary)
secure.POST("/source", handler.CreateSource) secure.POST("/source", handler.CreateReconnectSource)
secure.POST("/source/manual", handler.CreateManualSource) secure.POST("/source/manual", handler.CreateManualSource)
secure.GET("/source", handler.ListSource) secure.GET("/source", handler.ListSource)
secure.GET("/source/:sourceId", handler.GetSource) secure.GET("/source/:sourceId", handler.GetSource)
secure.DELETE("/source/:sourceId", handler.DeleteSource)
secure.POST("/source/:sourceId/sync", handler.SourceSync) secure.POST("/source/:sourceId/sync", handler.SourceSync)
secure.GET("/source/:sourceId/summary", handler.GetSourceSummary) secure.GET("/source/:sourceId/summary", handler.GetSourceSummary)
secure.GET("/resource/fhir", handler.ListResourceFhir) secure.GET("/resource/fhir", handler.ListResourceFhir)

View File

@ -11,15 +11,7 @@ import { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode, GridSta
import { GridItemCompHTMLElement, GridstackItemComponent } from './gridstack-item.component'; import { GridItemCompHTMLElement, GridstackItemComponent } from './gridstack-item.component';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {WidgetsModule} from '../../widgets/widgets.module'; import {WidgetsModule, WidgetComponents} from '../../widgets/widgets.module';
import {ComplexLineWidgetComponent} from '../../widgets/complex-line-widget/complex-line-widget.component';
import {DashboardWidgetComponent} from '../../widgets/dashboard-widget/dashboard-widget.component';
import {DonutChartWidgetComponent} from '../../widgets/donut-chart-widget/donut-chart-widget.component';
import {DualGaugesWidgetComponent} from '../../widgets/dual-gauges-widget/dual-gauges-widget.component';
import {GroupedBarChartWidgetComponent} from '../../widgets/grouped-bar-chart-widget/grouped-bar-chart-widget.component';
import {PatientVitalsWidgetComponent} from '../../widgets/patient-vitals-widget/patient-vitals-widget.component';
import {SimpleLineChartWidgetComponent} from '../../widgets/simple-line-chart-widget/simple-line-chart-widget.component';
import {TableWidgetComponent} from '../../widgets/table-widget/table-widget.component';
import {DashboardWidgetComponentInterface} from '../../widgets/dashboard-widget-component-interface'; import {DashboardWidgetComponentInterface} from '../../widgets/dashboard-widget-component-interface';
/** events handlers emitters signature for different events */ /** events handlers emitters signature for different events */
@ -137,16 +129,7 @@ export class GridstackComponent implements OnInit, AfterContentInit, OnDestroy {
) { ) {
// register all our dynamic components created in the grid // register all our dynamic components created in the grid
GridstackComponent.addComponentToSelectorType([ GridstackComponent.addComponentToSelectorType(WidgetComponents());
ComplexLineWidgetComponent,
DashboardWidgetComponent,
DonutChartWidgetComponent,
DualGaugesWidgetComponent,
GroupedBarChartWidgetComponent,
PatientVitalsWidgetComponent,
SimpleLineChartWidgetComponent,
TableWidgetComponent,
]);
// set globally our method to create the right widget type // set globally our method to create the right widget type
GridStack.addRemoveCB = gsCreateNgComponents; GridStack.addRemoveCB = gsCreateNgComponents;
GridStack.saveCB = gsSaveAdditionalNgInfo; GridStack.saveCB = gsSaveAdditionalNgInfo;

View File

@ -13,7 +13,7 @@
</div> </div>
<div class="card-footer text-center p-1" style="width:100%"> <div class="card-footer text-center p-1" style="width:100%">
<small class="tx-gray-700"> <small class="tx-gray-700">
{{sourceInfo?.metadata.display}} {{sourceInfo?.metadata?.display || getSourceDisplayName(sourceInfo)}}
</small> </small>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
.img-fluid { .img-fluid {
min-height:50px; width:100%;
} }
.border-left-danger { .border-left-danger {

View File

@ -1,5 +1,6 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {SourceListItem} from '../../pages/medical-sources/medical-sources.component'; import {SourceListItem} from '../../pages/medical-sources/medical-sources.component';
import moment from 'moment/moment';
@Component({ @Component({
selector: 'app-medical-sources-card', selector: 'app-medical-sources-card',
@ -22,4 +23,14 @@ export class MedicalSourcesCardComponent implements OnInit {
this.onClick.emit(this.sourceInfo) this.onClick.emit(this.sourceInfo)
} }
getSourceDisplayName(sourceItem: SourceListItem): string {
if(sourceItem.metadata?.display) {
return sourceItem.metadata?.display
}
if(sourceItem.source?.source_type == 'manual') {
return 'Uploaded ' + moment(sourceItem.source?.created_at).format('MMM DD, YYYY')
}
return "Unknown"
}
} }

View File

@ -42,10 +42,9 @@
Actions Actions
</button> </button>
<div ngbDropdownMenu aria-labelledby="dropdownManual"> <div ngbDropdownMenu aria-labelledby="dropdownManual">
<button ngbDropdownItem (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button> <button *ngIf="modalSelectedSourceListItem.source?.source_type != 'manual'" ngbDropdownItem (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>
<!-- <button ngbDropdownItem (click)="connectHandler($event, modalSelectedSourceListItem.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>--> <button *ngIf="modalSelectedSourceListItem.source?.source_type != 'manual'" ngbDropdownItem (click)="sourceReconnectHandler(modalSelectedSourceListItem)" type="button" class="btn btn-danger">Reconnect</button>
<button ngbDropdownItem type="button" class="btn disabled btn-outline-danger">Reconnect</button> <button ngbDropdownItem (click)="sourceDeleteHandler()" type="button" class="btn btn-danger">Delete</button>
<button ngbDropdownItem type="button" class="btn disabled btn-outline-danger">Delete</button>
</div> </div>
</div> </div>
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button> <button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>

View File

@ -11,6 +11,7 @@ import {ToastService} from '../../services/toast.service';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {Location} from '@angular/common'; import {Location} from '@angular/common';
import {EventBusService} from '../../services/event-bus.service'; import {EventBusService} from '../../services/event-bus.service';
import {SourceState} from '../../models/fasten/source-state';
@Component({ @Component({
selector: 'app-medical-sources-connected', selector: 'app-medical-sources-connected',
@ -102,7 +103,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
}); });
this.location.replaceState(urlTree.toString()); this.location.replaceState(urlTree.toString());
const expectedSourceStateInfo = JSON.parse(localStorage.getItem(callbackState)) const expectedSourceStateInfo = JSON.parse(localStorage.getItem(callbackState)) as SourceState
localStorage.removeItem(callbackState) localStorage.removeItem(callbackState)
if(callbackError && !callbackCode){ if(callbackError && !callbackCode){
@ -147,6 +148,7 @@ export class MedicalSourcesConnectedComponent implements OnInit {
//Create FHIR Client //Create FHIR Client
const dbSourceCredential = new Source({ const dbSourceCredential = new Source({
id: expectedSourceStateInfo.reconnect_source_id,
source_type: sourceType, source_type: sourceType,
authorization_endpoint: sourceMetadata.authorization_endpoint, authorization_endpoint: sourceMetadata.authorization_endpoint,
@ -323,6 +325,70 @@ export class MedicalSourcesConnectedComponent implements OnInit {
) )
} }
public sourceDeleteHandler(){
let source = this.modalSelectedSourceListItem.source
let sourceDisplayName = this.modalSelectedSourceListItem?.metadata?.display || this.modalSelectedSourceListItem?.source?.source_type || 'unknown'
this.status[source.id] = "authorize"
this.modalService.dismissAll()
this.fastenApi.deleteSource(source.id).subscribe(
(respData) => {
delete this.status[source.id]
delete this.status[source.source_type]
//delete this source from the connnected list
let foundIndex = this.connectedSourceList.findIndex((connectedSource) => {
return connectedSource?.source?.id == source.id
}, this)
if(foundIndex > -1){
this.connectedSourceList.splice(foundIndex, 1)
}
console.log("source delete response:", respData)
const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Success
toastNotification.message = `Successfully deleted source: ${sourceDisplayName}, ${respData} row(s) effected`
this.toastService.show(toastNotification)
},
(err) => {
delete this.status[source.id]
delete this.status[source.source_type]
const toastNotification = new ToastNotification()
toastNotification.type = ToastType.Error
toastNotification.message = `An error occurred while deleting source: ${sourceDisplayName}`
this.toastService.show(toastNotification)
console.log(err)
})
}
//this is similar to the connectHandler in the MedicalSourcesComponent
public sourceReconnectHandler(selectedSourceListItem: SourceListItem){
let sourceType = selectedSourceListItem.metadata.source_type
this.lighthouseApi.getLighthouseSource(sourceType)
.then(async (sourceMetadata: LighthouseSourceMetadata) => {
console.log(sourceMetadata);
let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata, selectedSourceListItem.source.id)
console.log('authorize url:', authorizationUrl.toString());
// redirect to lighthouse with uri's (or open a new window in desktop mode)
this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType, sourceMetadata.redirect_uri).subscribe((codeData) => {
//Note: this code will only run in Desktop mode (with popups)
//in non-desktop environments, the user is redirected in the same window, and this code is never executed.
//always close the modal
this.modalService.dismissAll()
//redirect the browser back to this page with the code in the query string parameters
this.lighthouseApi.redirectWithDesktopCode(sourceType, codeData)
})
});
}
private getDismissReason(reason: any): string { private getDismissReason(reason: any): string {
if (reason === ModalDismissReasons.ESC) { if (reason === ModalDismissReasons.ESC) {

View File

@ -2,6 +2,8 @@ export class SourceState {
state: string state: string
source_type: string //used to override the source_type for sources which have a single redirect url (eg. Epic) source_type: string //used to override the source_type for sources which have a single redirect url (eg. Epic)
reconnect_source_id?: string //used to reconnect a source
code_verifier?: string code_verifier?: string
code_challenge_method?: string code_challenge_method?: string
code_challenge?: string code_challenge?: string

View File

@ -3,6 +3,8 @@ import {BackgroundJob} from './background-job';
export class Source extends LighthouseSourceMetadata{ export class Source extends LighthouseSourceMetadata{
id?: string id?: string
created_at?: string
updated_at?: string
user_id?: number user_id?: number
source_type: string source_type: string
latest_background_job?: BackgroundJob latest_background_job?: BackgroundJob

View File

@ -3,7 +3,7 @@ import * as _ from 'lodash';
export class DashboardWidgetConfig { export class DashboardWidgetConfig {
id?: string id?: string
item_type: "complex-line-widget" | "donut-chart-widget" | "dual-gauges-widget" | "grouped-bar-chart-widget" | "patient-vitals-widget" | "simple-line-chart-widget" | "table-widget" item_type: "image-list-group-widget" | "complex-line-widget" | "donut-chart-widget" | "dual-gauges-widget" | "grouped-bar-chart-widget" | "patient-vitals-widget" | "simple-line-chart-widget" | "table-widget" | "records-summary-widget"
title_text: string title_text: string
description_text: string description_text: string

View File

@ -5,7 +5,7 @@ import {LighthouseSourceMetadata} from '../../models/lighthouse/lighthouse-sourc
import {Source} from '../../models/fasten/source'; import {Source} from '../../models/fasten/source';
import {MetadataSource} from '../../models/fasten/metadata-source'; import {MetadataSource} from '../../models/fasten/metadata-source';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {ActivatedRoute, Router, UrlSerializer} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {environment} from '../../../environments/environment'; import {environment} from '../../../environments/environment';
import {BehaviorSubject, forkJoin, Observable, of, Subject} from 'rxjs'; import {BehaviorSubject, forkJoin, Observable, of, Subject} from 'rxjs';
import { import {
@ -17,7 +17,6 @@ import {debounceTime, distinctUntilChanged, pairwise, startWith} from 'rxjs/oper
import {MedicalSourcesFilter, MedicalSourcesFilterService} from '../../services/medical-sources-filter.service'; import {MedicalSourcesFilter, MedicalSourcesFilterService} from '../../services/medical-sources-filter.service';
import {FormControl, FormGroup} from '@angular/forms'; import {FormControl, FormGroup} from '@angular/forms';
import * as _ from 'lodash'; import * as _ from 'lodash';
import {Location} from '@angular/common';
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120) export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
@ -79,9 +78,7 @@ export class MedicalSourcesComponent implements OnInit {
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private filterService: MedicalSourcesFilterService, private filterService: MedicalSourcesFilterService,
private modalService: NgbModal, private modalService: NgbModal,
private router: Router,
private urlSerializer: UrlSerializer,
private location: Location,
) { ) {
this.filterService.filterChanges.subscribe((filterInfo) => { this.filterService.filterChanges.subscribe((filterInfo) => {
@ -295,22 +292,8 @@ export class MedicalSourcesComponent implements OnInit {
//always close the modal //always close the modal
this.modalService.dismissAll() this.modalService.dismissAll()
if(!codeData){ //redirect the browser back to this page with the code in the query string parameters
//if we redirected completely, no callback data will be present. this.lighthouseApi.redirectWithDesktopCode(sourceType, codeData)
return
}
//User was shown a popup, which was closed, and data was returned using events
//redirect to callback page with code
let urlTree = this.router.createUrlTree(
['/sources/callback/' + sourceType],
{ queryParams: codeData, }
);
let absUrl = this.location.prepareExternalUrl(this.urlSerializer.serialize(urlTree))
console.log(absUrl);
window.location.replace(absUrl)
}) })
}); });
} }

View File

@ -28,7 +28,9 @@
</div> </div>
<div class="az-content-label mg-b-5">Condition</div> <div class="az-content-label mg-b-5">Condition</div>
<p class="mg-b-20">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna </p> <p class="mg-b-20">
A condition is a disease, illness, or injury that needs to be managed over time. A condition may be a comorbidity (a co-occurring condition), or it may be a main diagnosis.
</p>
<form (ngSubmit)="onSubmit()" [formGroup]="form"> <form (ngSubmit)="onSubmit()" [formGroup]="form">
@ -75,7 +77,9 @@
<div class="card-header" (click)="collapsePanel['medication'] = !collapsePanel['medication']"> <div class="card-header" (click)="collapsePanel['medication'] = !collapsePanel['medication']">
<div> <div>
<h6 class="card-title">Medications</h6> <h6 class="card-title">Medications</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p> <p class="card-text">
Medications are substances that are taken to treat or prevent disease or illness. Medications are generally characterized by their chemical composition and the way they are administered.
</p>
</div> </div>
</div> </div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['medication']" class="card-body"> <div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['medication']" class="card-body">
@ -173,7 +177,9 @@
<div class="card-header" (click)="collapsePanel['procedure'] = !collapsePanel['procedure']"> <div class="card-header" (click)="collapsePanel['procedure'] = !collapsePanel['procedure']">
<div> <div>
<h6 class="card-title">Major Surgeries and Implants</h6> <h6 class="card-title">Major Surgeries and Implants</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p> <p class="card-text">
Implants, devices, major surgeries and other procedures that are part of a patient's history.
</p>
</div> </div>
</div> </div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['procedure']" class="card-body"> <div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['procedure']" class="card-body">
@ -262,7 +268,7 @@
<div class="card-header" (click)="collapsePanel['practitioner'] = !collapsePanel['practitioner']"> <div class="card-header" (click)="collapsePanel['practitioner'] = !collapsePanel['practitioner']">
<div> <div>
<h6 class="card-title">Medical Practitioners</h6> <h6 class="card-title">Medical Practitioners</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p> <p class="card-text">Practitioners involved in the care of the patient.</p>
</div> </div>
</div> </div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['practitioner']" class="card-body"> <div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['practitioner']" class="card-body">
@ -332,7 +338,8 @@
<div class="card-header" (click)="collapsePanel['organization'] = !collapsePanel['organization']"> <div class="card-header" (click)="collapsePanel['organization'] = !collapsePanel['organization']">
<div> <div>
<h6 class="card-title">Medical Location/Organizations</h6> <h6 class="card-title">Medical Location/Organizations</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p> <p class="card-text">Locations and Organizations involved in the care of the patient.
</p>
</div> </div>
</div> </div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['organization']" class="card-body"> <div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['organization']" class="card-body">
@ -397,7 +404,9 @@
<div class="card-header" (click)="collapsePanel['attachments'] = !collapsePanel['attachments']"> <div class="card-header" (click)="collapsePanel['attachments'] = !collapsePanel['attachments']">
<div> <div>
<h6 class="card-title">Notes & Attachments</h6> <h6 class="card-title">Notes & Attachments</h6>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna</p> <p class="card-text">
Files and notes related to medications, procedures, or the current condition.
</p>
</div> </div>
</div> </div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['attachments']" class="card-body"> <div #collapse="ngbCollapse" [(ngbCollapse)]="collapsePanel['attachments']" class="card-body">

View File

@ -137,6 +137,15 @@ export class FastenApiService {
); );
} }
deleteSource(sourceId: string): Observable<number> {
return this._httpClient.delete<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/source/${sourceId}`)
.pipe(
map((response: ResponseWrapper) => {
return response.data as number
})
);
}
syncSource(sourceId: string): Observable<any> { syncSource(sourceId: string): Observable<any> {
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/source/${sourceId}/sync`, {}) return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/source/${sourceId}/sync`, {})
.pipe( .pipe(

View File

@ -13,6 +13,8 @@ import {LighthouseSourceSearch} from '../models/lighthouse/lighthouse-source-sea
import {HTTP_CLIENT_TOKEN} from "../dependency-injection"; import {HTTP_CLIENT_TOKEN} from "../dependency-injection";
import {MedicalSourcesFilter} from './medical-sources-filter.service'; import {MedicalSourcesFilter} from './medical-sources-filter.service';
import {OpenExternalLink} from '../../lib/utils/external_link'; import {OpenExternalLink} from '../../lib/utils/external_link';
import {Router, UrlSerializer} from '@angular/router';
import {Location} from '@angular/common';
export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120) export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
@ -21,8 +23,12 @@ export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 12
}) })
export class LighthouseService { export class LighthouseService {
constructor(@Inject(HTTP_CLIENT_TOKEN) private _httpClient: HttpClient) { constructor(
} @Inject(HTTP_CLIENT_TOKEN) private _httpClient: HttpClient,
private router: Router,
private urlSerializer: UrlSerializer,
private location: Location,
) {}
public searchLighthouseSources(filter: MedicalSourcesFilter): Observable<LighthouseSourceSearch> { public searchLighthouseSources(filter: MedicalSourcesFilter): Observable<LighthouseSourceSearch> {
if(filter.searchAfter){ if(filter.searchAfter){
@ -87,11 +93,15 @@ export class LighthouseService {
} }
async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata): Promise<URL> { async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata, reconnectSourceId?: string): Promise<URL> {
const state = uuidV4() const state = uuidV4()
let sourceStateInfo = new SourceState() let sourceStateInfo = new SourceState()
sourceStateInfo.state = state sourceStateInfo.state = state
sourceStateInfo.source_type = sourceType sourceStateInfo.source_type = sourceType
if(reconnectSourceId){
//if the source already exists, and we want to re-connect it (because of an expiration), we need to pass the existing source id
sourceStateInfo.reconnect_source_id = reconnectSourceId
}
// generate the authorization url // generate the authorization url
const authorizationUrl = new URL(lighthouseSource.authorization_endpoint); const authorizationUrl = new URL(lighthouseSource.authorization_endpoint);
@ -283,6 +293,28 @@ export class LighthouseService {
) )
} }
//after waiting for the desktop code, we need to redirect to the callback page with the code in the query params
// (which is what would have happened if we were in a browser and we were redirected as usual)
redirectWithDesktopCode(sourceType: string, codeData: any){
if(!codeData){
//if we redirected completely, no callback data will be present.
return
}
//User was shown a popup, which was closed, and data was returned using events
//redirect to callback page with code
let urlTree = this.router.createUrlTree(
['/sources/callback/' + sourceType],
{ queryParams: codeData, }
);
let absUrl = this.location.prepareExternalUrl(this.urlSerializer.serialize(urlTree))
console.log(absUrl);
window.location.replace(absUrl)
}
} }

View File

@ -0,0 +1,35 @@
<ng-container [ngTemplateOutlet]="loading ? showLoading : (!loading && isEmpty) ? showEmpty : showChart"></ng-container>
<ng-template #showLoading>
<loading-widget></loading-widget>
</ng-template>
<ng-template #showEmpty>
<empty-widget></empty-widget>
</ng-template>
<ng-template #showChart>
<ul class="list-group">
<li class="list-group-item d-flex align-items-center">
<img src="../img/faces/face6.jpg" class="wd-30 rounded-circle mg-r-15" alt="">
<div>
<h6 class="tx-13 tx-inverse tx-semibold mg-b-0">Dr. Ester Howard</h6>
<span class="d-block tx-11 text-muted">Neurologist, UCSF</span>
</div>
</li>
<li class="list-group-item d-flex align-items-center">
<img src="../img/faces/face7.jpg" class="wd-30 rounded-circle mg-r-15" alt="">
<div>
<h6 class="tx-13 tx-inverse tx-semibold mg-b-0">Dr. Joel Mendez</h6>
<span class="d-block tx-11 text-muted">General Medicine, Kaiser</span>
</div>
</li>
<li class="list-group-item d-flex align-items-center">
<img src="../img/faces/face8.jpg" class="wd-30 rounded-circle mg-r-15" alt="">
<div>
<h6 class="tx-13 tx-inverse tx-semibold mg-b-0">Dr. Marianne Audrey</h6>
<span class="d-block tx-11 text-muted">orthopedic surgeon, UCSF</span>
</div>
</li>
</ul>
</ng-template><!-- card -->

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ImageListGroupWidgetComponent } from './image-list-group-widget.component';
describe('ImageListGroupWidgetComponent', () => {
let component: ImageListGroupWidgetComponent;
let fixture: ComponentFixture<ImageListGroupWidgetComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ImageListGroupWidgetComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ImageListGroupWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,35 @@
import { Component, OnInit } from '@angular/core';
import {NgChartsModule} from 'ng2-charts';
import {CommonModule} from '@angular/common';
import {LoadingWidgetComponent} from '../loading-widget/loading-widget.component';
import {EmptyWidgetComponent} from '../empty-widget/empty-widget.component';
import {DashboardWidgetComponent} from '../dashboard-widget/dashboard-widget.component';
import {DashboardWidgetConfig} from '../../models/widget/dashboard-widget-config';
@Component({
standalone: true,
imports: [NgChartsModule, CommonModule, LoadingWidgetComponent, EmptyWidgetComponent],
selector: 'image-list-group-widget',
templateUrl: './image-list-group-widget.component.html',
styleUrls: ['./image-list-group-widget.component.scss']
})
export class ImageListGroupWidgetComponent extends DashboardWidgetComponent implements OnInit {
ngOnInit(): void {
//manually define the widget config, rather than pull from the configuration file
this.widgetConfig = {
id: 'image-list-group-widget',
item_type: 'image-list-group-widget',
description_text: 'Displays a summary of patient records',
width: 4,
height: 5,
title_text: 'Medical Records',
queries: []
} as DashboardWidgetConfig
super.ngOnInit();
this.loading = false
this.isEmpty = false
}
}

View File

@ -0,0 +1,47 @@
<ng-container [ngTemplateOutlet]="loading ? showLoading : (!loading && isEmpty) ? showEmpty : showChart"></ng-container>
<ng-template #showLoading>
<loading-widget></loading-widget>
</ng-template>
<ng-template #showEmpty>
<empty-widget></empty-widget>
</ng-template>
<ng-template #showChart>
<div class="card card-dashboard-sixteen">
<div class="card-header">
<h6 class="card-title">Medical Records</h6>
</div><!-- card-header -->
<div class="card-body">
<div class="row">
<div class="col-12">
<div class="table-responsive">
<table class="table mg-b-0">
<tbody>
<tr *ngFor="let groupInfo of groupLookup">
<td>
<div class="az-img-user"><img src="assets/icons/{{groupInfo.imageName}}.svg" alt=""></div>
</td>
<td>
<h6 class="mg-b-0 tx-inverse">{{groupInfo.displayName}}</h6>
<small class="tx-11 tx-gray-500" *ngIf="groupInfo.includedResourceTypes.length > 1">
<span *ngFor="let resourceTypes of groupInfo.includedResourceTypes">
{{resourceTypes}}<br/>
</span>
</small>
</td>
<td>
<h6 class="mg-b-0 tx-inverse">{{groupInfo.count}}</h6>
<small class="tx-11 tx-gray-500">Records</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div><!-- card-body -->
</div>
</ng-template><!-- card -->

View File

@ -0,0 +1,14 @@
tbody {
display: grid;
grid-template-columns: repeat(2, auto);
}
/* Borders */
/* The margin declarations are used to simulate border-collapse:collapse */
tr {
margin: 0 0 -1px -1px;
//border: 1px solid black;
}
tbody:not(:empty) {
margin: 0 0 1px 1px;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RecordsSummaryWidgetComponent } from './records-summary-widget.component';
describe('RecordsSummaryWidgetComponent', () => {
let component: RecordsSummaryWidgetComponent;
let fixture: ComponentFixture<RecordsSummaryWidgetComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RecordsSummaryWidgetComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(RecordsSummaryWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,214 @@
import { Component, OnInit } from '@angular/core';
import {NgChartsModule} from 'ng2-charts';
import {CommonModule} from '@angular/common';
import {MomentModule} from 'ngx-moment';
import {LoadingWidgetComponent} from '../loading-widget/loading-widget.component';
import {EmptyWidgetComponent} from '../empty-widget/empty-widget.component';
import {DashboardWidgetComponent} from '../dashboard-widget/dashboard-widget.component';
import {DashboardWidgetConfig} from '../../models/widget/dashboard-widget-config';
import {Summary} from '../../models/fasten/summary';
class GroupedSummary {
displayName: string
imageName: string
resourceTypes: string[]
includedResourceTypes: string[] = []
count: number = 0
}
@Component({
standalone: true,
imports: [CommonModule, LoadingWidgetComponent, EmptyWidgetComponent],
selector: 'records-summary-widget',
templateUrl: './records-summary-widget.component.html',
styleUrls: ['./records-summary-widget.component.scss']
})
export class RecordsSummaryWidgetComponent extends DashboardWidgetComponent implements OnInit {
// constructor() { }
summary: Summary
groupLookup: GroupedSummary[] = [
{
displayName: 'Allergies',
imageName: 'allergies',
resourceTypes: ['AllergyIntolerance', 'AdverseEvent'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Care Team',
imageName: 'care-team',
resourceTypes: ['CareTeam', 'Practitioner', 'Patient', 'RelatedPerson', 'PractitionerRole'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Clinical Notes',
imageName: 'clinical-notes',
resourceTypes: ['DocumentReference', 'DiagnosticReport'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Files',
imageName: 'files',
resourceTypes: ['Binary', 'Media'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Lab Results',
imageName: 'lab-results',
resourceTypes: ['Observation', 'Specimen'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Health Issues',
imageName: 'health-issues',
resourceTypes: ['Conditions', 'Encounters'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Facilities',
imageName: 'facilities',
resourceTypes: ['Organization', 'Location'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Health Goals',
imageName: 'health-goals',
resourceTypes: ['Goal'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Health Insurance',
imageName: 'health-insurance',
resourceTypes: ['Coverage', 'ExplanationOfBenefit', 'Claim'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Health Assessments',
imageName: 'health-assessments',
resourceTypes: ['QuestionnaireResponse','Questionnaire', 'CarePlan', 'FamilyMemberHistory'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Immunizations',
imageName: 'immunizations',
resourceTypes: ['Immunization'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Implants',
imageName: 'implants',
resourceTypes: ['Device'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Medications',
imageName: 'medications',
resourceTypes: ['Medication', 'MedicationRequest', 'MedicationStatement', 'MedicationAdministration', 'MedicationDispense'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Demographics',
imageName: 'demographics',
resourceTypes: ['Patient'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Procedures',
imageName: 'procedures',
resourceTypes: ['Procedure','ServiceRequest'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Provenance',
imageName: 'provenance',
resourceTypes: ['Provenance'],
includedResourceTypes:[],
count: 0
},
{
displayName: 'Appointments',
imageName: 'appointments',
resourceTypes: ['Appointment', 'Schedule', 'Slot'],
includedResourceTypes:[],
count: 0
}
]
ngOnInit(): void {
//manually define the widget config, rather than pull from the configuration file
this.widgetConfig = {
id: 'records-summary-widget',
item_type: 'records-summary-widget',
description_text: 'Displays a summary of patient records',
width: 4,
height: 5,
title_text: 'Medical Records',
queries: [
]
} as DashboardWidgetConfig
super.ngOnInit();
this.chartProcessQueryResults(null)
}
chartProcessQueryResults(queryResults: any[]) {
this.fastenApi.getSummary().subscribe((summary: Summary) => {
this.summary = summary
for (let resourceTypeCount of summary.resource_type_counts) {
let foundGroup = false
for (let groupKey in this.groupLookup) {
let group = this.groupLookup[groupKey]
if (group.resourceTypes.indexOf(resourceTypeCount.resource_type) > -1) {
foundGroup = true
this.groupLookup[groupKey].count += resourceTypeCount.count
this.groupLookup[groupKey].includedResourceTypes.push(resourceTypeCount.resource_type)
}
}
if (!foundGroup) {
this.groupLookup[resourceTypeCount.resource_type] = {
displayName: resourceTypeCount.resource_type,
resourceTypes: [resourceTypeCount.resource_type],
count: resourceTypeCount.count
}
}
}
//filter any groups with 0 counts
this.groupLookup = this.groupLookup.filter((group) => {
return group.count > 0
})
if(this.summary.resource_type_counts.length > 0){
this.isEmpty = false
}
this.loading = false
},
(error) => {
this.loading = false
},
() => {
console.log('completed getting summary')
})
}
}

View File

@ -0,0 +1,50 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {RecordsSummaryWidgetComponent} from './records-summary-widget.component';
import {applicationConfig, moduleMetadata} from '@storybook/angular';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {importProvidersFrom} from '@angular/core';
import {CommonModule} from '@angular/common';
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<RecordsSummaryWidgetComponent> = {
title: 'Widget/RecordsSummaryWidget',
component: RecordsSummaryWidgetComponent,
decorators: [
applicationConfig({
providers: [
{
provide: HttpClient,
useClass: HttpClient
},
{
provide: HTTP_CLIENT_TOKEN,
useClass: HttpClient,
},
importProvidersFrom(HttpClientModule)
]
}),
moduleMetadata({
imports: [CommonModule, HttpClientModule],
})
],
tags: ['autodocs'],
render: (args: RecordsSummaryWidgetComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
},
};
export default meta;
type Story = StoryObj<RecordsSummaryWidgetComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
export const Example: Story = {
args: {
}
};

View File

@ -1,4 +1,4 @@
import { NgModule } from '@angular/core'; import {Component, NgModule, Type} from '@angular/core';
import {ComplexLineWidgetComponent} from './complex-line-widget/complex-line-widget.component'; import {ComplexLineWidgetComponent} from './complex-line-widget/complex-line-widget.component';
import {DonutChartWidgetComponent} from './donut-chart-widget/donut-chart-widget.component'; import {DonutChartWidgetComponent} from './donut-chart-widget/donut-chart-widget.component';
import {DualGaugesWidgetComponent} from './dual-gauges-widget/dual-gauges-widget.component'; import {DualGaugesWidgetComponent} from './dual-gauges-widget/dual-gauges-widget.component';
@ -9,6 +9,8 @@ import {TableWidgetComponent} from './table-widget/table-widget.component';
import { LoadingWidgetComponent } from './loading-widget/loading-widget.component'; import { LoadingWidgetComponent } from './loading-widget/loading-widget.component';
import { EmptyWidgetComponent } from './empty-widget/empty-widget.component'; import { EmptyWidgetComponent } from './empty-widget/empty-widget.component';
import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.component'; import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.component';
import { RecordsSummaryWidgetComponent } from './records-summary-widget/records-summary-widget.component';
import { ImageListGroupWidgetComponent } from './image-list-group-widget/image-list-group-widget.component';
@NgModule({ @NgModule({
imports: [ imports: [
@ -18,6 +20,8 @@ import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.comp
DualGaugesWidgetComponent, DualGaugesWidgetComponent,
GroupedBarChartWidgetComponent, GroupedBarChartWidgetComponent,
PatientVitalsWidgetComponent, PatientVitalsWidgetComponent,
RecordsSummaryWidgetComponent,
ImageListGroupWidgetComponent,
SimpleLineChartWidgetComponent, SimpleLineChartWidgetComponent,
TableWidgetComponent, TableWidgetComponent,
LoadingWidgetComponent, LoadingWidgetComponent,
@ -32,6 +36,8 @@ import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.comp
DualGaugesWidgetComponent, DualGaugesWidgetComponent,
GroupedBarChartWidgetComponent, GroupedBarChartWidgetComponent,
PatientVitalsWidgetComponent, PatientVitalsWidgetComponent,
RecordsSummaryWidgetComponent,
ImageListGroupWidgetComponent,
SimpleLineChartWidgetComponent, SimpleLineChartWidgetComponent,
TableWidgetComponent, TableWidgetComponent,
LoadingWidgetComponent, LoadingWidgetComponent,
@ -41,3 +47,21 @@ import {DashboardWidgetComponent} from './dashboard-widget/dashboard-widget.comp
}) })
export class WidgetsModule { } export class WidgetsModule { }
//when adding widgets to this list, you must also register the widget id in
// frontend/src/app/models/widget/dashboard-widget-config.ts
export function WidgetComponents(): Type<Object>[] {
return [
ComplexLineWidgetComponent,
DonutChartWidgetComponent,
DualGaugesWidgetComponent,
GroupedBarChartWidgetComponent,
PatientVitalsWidgetComponent,
RecordsSummaryWidgetComponent,
ImageListGroupWidgetComponent,
SimpleLineChartWidgetComponent,
TableWidgetComponent,
LoadingWidgetComponent,
EmptyWidgetComponent
]
}

View File

@ -0,0 +1,41 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #ffd67b;
}
.cls-2 {
fill: #fff;
}
.cls-3 {
fill: #bde9ff;
}
.cls-4 {
fill: #0098e3;
}
.cls-5 {
fill: #e89b00;
}
</style>
</defs>
<g>
<path class="cls-1" d="M29.29,29.05s-14-22-2.81-24.74S34.91,17.24,29.29,29.05Z"/>
<path class="cls-1" d="M29.24,28.83S3.73,23.18,9.76,13.32,24.87,16.51,29.24,28.83Z"/>
<path class="cls-1" d="M29.05,28.71s-22,14-24.74,2.81S17.24,23.09,29.05,28.71Z"/>
<path class="cls-1" d="M28.83,28.76S23.18,54.27,13.32,48.24,16.51,33.13,28.83,28.76Z"/>
<path class="cls-1" d="M28.71,29s14,22,2.81,24.74S23.09,40.76,28.71,29Z"/>
<path class="cls-1" d="M28.76,29.17s25.51,5.65,19.48,15.51S33.13,41.49,28.76,29.17Z"/>
<path class="cls-1" d="M29,29.29s22-14,24.74-2.81S40.76,34.91,29,29.29Z"/>
<path class="cls-1" d="M29.17,29.24S34.82,3.73,44.68,9.76,41.49,24.87,29.17,29.24Z"/>
</g>
<path class="cls-2" d="M58,32.66a6.68,6.68,0,0,0-9.44.06L39.81,41.6h0l-6.88,7A6.68,6.68,0,1,0,42.43,58L50.25,50h0l7.82-7.92A6.67,6.67,0,0,0,58,32.66Z"/>
<g>
<path class="cls-3" d="M49.63,51.24l8.5-8.61a6.48,6.48,0,0,0-.06-9.17h0a6.49,6.49,0,0,0-9.17.06l-8.5,8.62Z"/>
<path class="cls-4" d="M41.31,41.21,33.72,48.9a6.49,6.49,0,0,0,.06,9.17h0A6.48,6.48,0,0,0,43,58l7.59-7.69Z"/>
</g>
<circle class="cls-5" cx="29" cy="29" r="8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,26 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #1eb37a;
}
.cls-2 {
fill: #a5f3ca;
}
.cls-3 {
fill: #0e6b5a;
}
</style>
</defs>
<circle class="cls-1" cx="32" cy="32" r="28"/>
<circle class="cls-2" cx="32" cy="32" r="22"/>
<circle class="cls-1" cx="32" cy="32" r="17"/>
<circle class="cls-2" cx="32" cy="32" r="12"/>
<circle class="cls-1" cx="32" cy="32" r="7"/>
<path class="cls-3" d="M32.67,28.82h0a3.83,3.83,0,0,1,.62-1.4L43,14l4.17,4L36.63,29.32a5,5,0,0,1-1.46,1.09l-.26.14A1.57,1.57,0,0,1,32.67,28.82Z"/>
<polygon class="cls-3" points="43 14 43 11 48.17 4 50 11 45 17 43 14"/>
<polygon class="cls-3" points="47 18 50 18 57 11.83 50 11 44 17 47 18"/>
<polygon class="cls-3" points="33.5 29 32 32 35 30 33.5 29"/>
</svg>

After

Width:  |  Height:  |  Size: 891 B

View File

@ -0,0 +1,40 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #34b4ff;
}
.cls-2 {
fill: #0098e3;
}
.cls-3 {
fill: #ffd67b;
}
.cls-4 {
fill: #818cb0;
}
.cls-5 {
fill: #eff1f4;
}
.cls-6 {
fill: #dfe3e8;
}
</style>
</defs>
<g>
<path class="cls-1" d="M48.36,54.78,34.19,59.64a6.84,6.84,0,0,1-4.38,0L15.64,54.78a7.52,7.52,0,0,1-5.21-7.05h0A10.82,10.82,0,0,1,21.49,37.18h21A10.82,10.82,0,0,1,53.57,47.73h0A7.52,7.52,0,0,1,48.36,54.78Z"/>
<path class="cls-2" d="M35.06,37.18H28.94A1.36,1.36,0,0,0,27.65,39l2.19,6.32a2.29,2.29,0,0,0,4.32,0L36.35,39A1.36,1.36,0,0,0,35.06,37.18Z"/>
<path class="cls-3" d="M32,4a14.93,14.93,0,0,0-13.63,8.85H45.63A14.93,14.93,0,0,0,32,4Z"/>
<path class="cls-3" d="M17.07,18.93v6.64a14.93,14.93,0,0,0,29.86,0V18.93c0-.19,0-.37,0-.55H17.1C17.09,18.56,17.07,18.74,17.07,18.93Z"/>
<path class="cls-4" d="M18.37,12.85a14.78,14.78,0,0,0-1.27,5.53H46.9a14.78,14.78,0,0,0-1.27-5.53Z"/>
<g>
<circle class="cls-5" cx="32" cy="15.61" r="6.69"/>
<path class="cls-6" d="M32,9.42a6.19,6.19,0,1,1-6.19,6.19A6.2,6.2,0,0,1,32,9.42m0-1a7.19,7.19,0,1,0,7.19,7.19A7.19,7.19,0,0,0,32,8.42Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,48 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #ffd67b;
}
.cls-2 {
fill: #fdba19;
}
.cls-3 {
fill: #fffbd5;
}
.cls-4 {
fill: #243147;
}
.cls-5 {
fill: #1eb37a;
}
.cls-6 {
fill: #14866d;
}
.cls-7 {
fill: #c0c7d2;
}
</style>
</defs>
<g>
<g>
<path class="cls-1" d="M11,10H41a7,7,0,0,1,7,7V49a7,7,0,0,1-7,7H16L4,44V17A7,7,0,0,1,11,10Z"/>
<path class="cls-2" d="M10.82,44H4L16,56V49.18A5.18,5.18,0,0,0,10.82,44Z"/>
<rect class="cls-3" x="9" y="17" width="27" height="3" transform="translate(45 37) rotate(-180)"/>
<rect class="cls-3" x="9" y="22" width="17" height="3" transform="translate(35 47) rotate(-180)"/>
<rect class="cls-3" x="9" y="28" width="11" height="3" transform="translate(29 59) rotate(-180)"/>
</g>
<g>
<path class="cls-4" d="M24.35,40.66h2.24a0,0,0,0,1,0,0v5.6a1.12,1.12,0,0,1-1.12,1.12h0a1.12,1.12,0,0,1-1.12-1.12v-5.6a0,0,0,0,1,0,0Z" transform="translate(38.59 -5.12) rotate(45)"/>
<path class="cls-5" d="M57.17,18.67,32.6,43.23a4.47,4.47,0,0,1-6.33,0h0a4.48,4.48,0,0,1,0-6.34L50.83,12.33Z"/>
<path class="cls-6" d="M58,11.54a4.48,4.48,0,0,0-6.34,0l-2.18,2.18,6.34,6.34L58,17.88A4.48,4.48,0,0,0,58,11.54Z"/>
<path class="cls-7" d="M59.27,21.42l-8.75-8.76-1.75,1.76,8.75,8.75L47.89,32.8a1.24,1.24,0,0,0,1.75,1.75l9.63-9.63h0a2.47,2.47,0,0,0,0-3.5Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,30 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #34b4ff;
}
.cls-2 {
fill: #0098e3;
}
.cls-3 {
fill: #bde9ff;
}
</style>
</defs>
<g>
<rect class="cls-1" x="4" y="10" width="56" height="44" rx="6.94"/>
<path class="cls-2" d="M60,16.87A6.94,6.94,0,0,0,53,10H11a6.94,6.94,0,0,0-7,6.87V20H60Z"/>
<g>
<rect class="cls-3" x="33" y="34" width="21" height="3"/>
<rect class="cls-3" x="33" y="41" width="21" height="3"/>
<rect class="cls-3" x="33" y="27" width="12" height="3"/>
</g>
<g>
<circle class="cls-3" cx="18" cy="30" r="6"/>
<path class="cls-3" d="M16.27,38h3.47A6.27,6.27,0,0,1,26,44.27V46a0,0,0,0,1,0,0H10a0,0,0,0,1,0,0V44.27A6.27,6.27,0,0,1,16.27,38Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 881 B

View File

@ -0,0 +1,38 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #706e85;
}
.cls-2 {
fill: #c0c7d2;
}
</style>
</defs>
<rect class="cls-1" x="4" y="57" width="56" height="3" rx="1.38"/>
<path class="cls-2" d="M16.65,8.73H47.35A4.65,4.65,0,0,1,52,13.38V57a0,0,0,0,1,0,0H12a0,0,0,0,1,0,0V13.38A4.65,4.65,0,0,1,16.65,8.73Z"/>
<g>
<rect class="cls-1" x="16.55" y="22.66" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="16.55" y="31.31" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="16.55" y="39.97" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="25.57" y="22.66" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="25.57" y="31.31" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="25.57" y="39.97" width="4.88" height="5.77" rx="0.65"/>
</g>
<g>
<rect class="cls-1" x="34.55" y="22.66" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="34.55" y="31.31" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="34.55" y="39.97" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="43.57" y="22.66" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="43.57" y="31.31" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="43.57" y="39.97" width="4.88" height="5.77" rx="0.65"/>
</g>
<rect class="cls-1" x="34.55" y="14.14" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="26" y="49" width="13" height="7" rx="1.17"/>
<rect class="cls-1" x="43.57" y="14.14" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="16.55" y="14.14" width="4.88" height="5.77" rx="0.65"/>
<rect class="cls-1" x="25.57" y="14.14" width="4.88" height="5.77" rx="0.65"/>
<path class="cls-2" d="M21.87,5.89H44.13a.87.87,0,0,1,.87.87v2a0,0,0,0,1,0,0H21a0,0,0,0,1,0,0v-2A.87.87,0,0,1,21.87,5.89Z"/>
<path class="cls-2" d="M25.58,4H40.42a.58.58,0,0,1,.58.58V5.89a0,0,0,0,1,0,0H25a0,0,0,0,1,0,0V4.58A.58.58,0,0,1,25.58,4Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,48 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #ffd67b;
}
.cls-2 {
fill: #fdba19;
}
.cls-3 {
fill: #fffbd5;
}
.cls-4 {
fill: #243147;
}
.cls-5 {
fill: #1eb37a;
}
.cls-6 {
fill: #14866d;
}
.cls-7 {
fill: #c0c7d2;
}
</style>
</defs>
<g>
<g>
<path class="cls-1" d="M11,10H41a7,7,0,0,1,7,7V49a7,7,0,0,1-7,7H16L4,44V17A7,7,0,0,1,11,10Z"/>
<path class="cls-2" d="M10.82,44H4L16,56V49.18A5.18,5.18,0,0,0,10.82,44Z"/>
<rect class="cls-3" x="9" y="17" width="27" height="3" transform="translate(45 37) rotate(-180)"/>
<rect class="cls-3" x="9" y="22" width="17" height="3" transform="translate(35 47) rotate(-180)"/>
<rect class="cls-3" x="9" y="28" width="11" height="3" transform="translate(29 59) rotate(-180)"/>
</g>
<g>
<path class="cls-4" d="M24.35,40.66h2.24a0,0,0,0,1,0,0v5.6a1.12,1.12,0,0,1-1.12,1.12h0a1.12,1.12,0,0,1-1.12-1.12v-5.6a0,0,0,0,1,0,0Z" transform="translate(38.59 -5.12) rotate(45)"/>
<path class="cls-5" d="M57.17,18.67,32.6,43.23a4.47,4.47,0,0,1-6.33,0h0a4.48,4.48,0,0,1,0-6.34L50.83,12.33Z"/>
<path class="cls-6" d="M58,11.54a4.48,4.48,0,0,0-6.34,0l-2.18,2.18,6.34,6.34L58,17.88A4.48,4.48,0,0,0,58,11.54Z"/>
<path class="cls-7" d="M59.27,21.42l-8.75-8.76-1.75,1.76,8.75,8.75L47.89,32.8a1.24,1.24,0,0,0,1.75,1.75l9.63-9.63h0a2.47,2.47,0,0,0,0-3.5Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,31 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #de9c76;
}
.cls-2 {
fill: #f8f9fb;
}
.cls-3 {
fill: #9eacc3;
}
.cls-4 {
fill: #c0c7d2;
}
</style>
</defs>
<rect class="cls-1" x="10" y="6" width="44" height="52" rx="3.44"/>
<g>
<rect class="cls-1" x="10" y="6" width="44" height="52" rx="7"/>
<rect class="cls-2" x="15" y="9" width="34" height="45" rx="2.07"/>
<path class="cls-3" d="M21.75,6h20.5a0,0,0,0,1,0,0V9.2a1.92,1.92,0,0,1-1.92,1.92H23.67A1.92,1.92,0,0,1,21.75,9.2V6A0,0,0,0,1,21.75,6Z"/>
</g>
<rect class="cls-4" x="20" y="18" width="25" height="3"/>
<rect class="cls-4" x="20" y="24" width="25" height="3"/>
<rect class="cls-4" x="20" y="30" width="25" height="3"/>
<rect class="cls-4" x="20" y="36" width="25" height="3"/>
</svg>

After

Width:  |  Height:  |  Size: 937 B

View File

@ -0,0 +1,26 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #1eb37a;
}
.cls-2 {
fill: #a5f3ca;
}
.cls-3 {
fill: #0e6b5a;
}
</style>
</defs>
<circle class="cls-1" cx="32" cy="32" r="28"/>
<circle class="cls-2" cx="32" cy="32" r="22"/>
<circle class="cls-1" cx="32" cy="32" r="17"/>
<circle class="cls-2" cx="32" cy="32" r="12"/>
<circle class="cls-1" cx="32" cy="32" r="7"/>
<path class="cls-3" d="M32.67,28.82h0a3.83,3.83,0,0,1,.62-1.4L43,14l4.17,4L36.63,29.32a5,5,0,0,1-1.46,1.09l-.26.14A1.57,1.57,0,0,1,32.67,28.82Z"/>
<polygon class="cls-3" points="43 14 43 11 48.17 4 50 11 45 17 43 14"/>
<polygon class="cls-3" points="47 18 50 18 57 11.83 50 11 44 17 47 18"/>
<polygon class="cls-3" points="33.5 29 32 32 35 30 33.5 29"/>
</svg>

After

Width:  |  Height:  |  Size: 891 B

View File

@ -0,0 +1,45 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #c0c7d2;
}
.cls-2 {
fill: #dfe3e8;
}
.cls-3 {
fill: #f8f9fb;
}
.cls-4 {
fill: #fb7373;
}
.cls-5 {
fill: #bde9ff;
}
.cls-6 {
fill: #d1d6dd;
}
</style>
</defs>
<g>
<path class="cls-1" d="M51,58H13a7,7,0,0,1-7-7V21H58V51A7,7,0,0,1,51,58Z"/>
<path class="cls-2" d="M51,57H13a7,7,0,0,1-7-7V20H58V50A7,7,0,0,1,51,57Z"/>
<path class="cls-3" d="M51,55H13a7,7,0,0,1-7-7V19H58V48A7,7,0,0,1,51,55Z"/>
<path class="cls-4" d="M13,8H51a7,7,0,0,1,7,7v7a0,0,0,0,1,0,0H6a0,0,0,0,1,0,0V15A7,7,0,0,1,13,8Z"/>
<g>
<rect class="cls-4" x="40" y="39" width="10" height="10" rx="2.14"/>
<rect class="cls-5" x="26.5" y="39" width="10" height="10" rx="2.14"/>
<rect class="cls-5" x="13.5" y="39" width="10" height="10" rx="2.14"/>
<rect class="cls-5" x="40" y="26" width="10" height="10" rx="2.14"/>
<rect class="cls-5" x="26.5" y="26" width="10" height="10" rx="2.14"/>
<rect class="cls-5" x="13.5" y="26" width="10" height="10" rx="2.14"/>
</g>
<rect class="cls-6" x="16.82" y="4" width="5.36" height="8.93" rx="2.61"/>
<rect class="cls-6" x="41.82" y="4" width="5.36" height="8.93" rx="2.61"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,29 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #976cea;
}
.cls-2 {
fill: #d6e3ff;
}
.cls-3 {
fill: #b387ff;
}
</style>
</defs>
<rect class="cls-1" x="38.35" y="5.06" width="4.53" height="36.66" rx="1.91" transform="translate(28.43 -21.87) rotate(45)"/>
<rect class="cls-1" x="45.19" y="8.92" width="15.84" height="3.93" rx="1.17" transform="translate(23.26 -34.37) rotate(45)"/>
<rect class="cls-2" x="20.91" y="9.81" width="18.1" height="48.45" rx="4.39" transform="translate(32.84 -11.22) rotate(45)"/>
<rect class="cls-1" x="34.39" y="16.33" width="22.63" height="3.93" rx="1.4" transform="translate(26.32 -26.96) rotate(45)"/>
<polygon class="cls-1" points="10.3 48.63 8.19 54.23 9.69 55.74 15.23 53.57 10.3 48.63"/>
<path class="cls-1" d="M12.12,50.46,4.59,57.83a1.15,1.15,0,0,0-.13,1.63h0a1.16,1.16,0,0,0,1.62-.12l7.46-7.46Z"/>
<path class="cls-3" d="M12.78,33.48H28.62a0,0,0,0,1,0,0V49.21a3.9,3.9,0,0,1-3.9,3.9h-8a3.9,3.9,0,0,1-3.9-3.9V33.48a0,0,0,0,1,0,0Z" transform="translate(36.68 -1.96) rotate(45)"/>
<rect class="cls-1" x="36.32" y="16.61" width="2.07" height="5.66" transform="translate(-2.81 32.11) rotate(-45)"/>
<rect class="cls-1" x="30.77" y="22.17" width="2.07" height="5.66" transform="translate(-8.36 29.81) rotate(-45)"/>
<rect class="cls-1" x="25.21" y="27.72" width="2.07" height="5.66" transform="translate(-13.92 27.51) rotate(-45)"/>
<rect class="cls-1" x="19.66" y="33.28" width="2.07" height="5.66" transform="translate(-19.47 25.21) rotate(-45)"/>
<rect class="cls-1" x="14.1" y="38.84" width="2.07" height="5.66" transform="translate(-25.03 22.91) rotate(-45)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,25 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #c0c7d2;
}
.cls-2 {
fill: #9eacc3;
}
.cls-3 {
fill: #d1d6dd;
}
</style>
</defs>
<g>
<path class="cls-1" d="M20.23,32.69l-3.55,3.79-3.9,4.15-3.72,4-3.77,4a2.2,2.2,0,0,0,.1,3.12l7.66,7.18a2.21,2.21,0,0,0,3.11-.1l7.92-8.38,3.76-4L31.92,42l3.72-4,4-4.28L24.06,28.6Z"/>
<polygon class="cls-1" points="33.35 18.7 27.51 24.93 43.05 30.15 44.22 28.9 33.35 18.7"/>
<polygon class="cls-2" points="24.06 28.6 39.65 33.77 43.05 30.15 27.51 24.93 24.06 28.6"/>
</g>
<path class="cls-3" d="M49.51,17.55,46,14l2.85-4.75a18,18,0,0,0-25.29.94L54.16,38.93a18,18,0,0,0,.22-24.36Z"/>
<polygon class="cls-2" points="27.84 46.36 24.08 50.38 9.06 44.6 12.78 40.63 27.84 46.36"/>
<polygon class="cls-2" points="35.64 38.05 31.92 42.02 16.68 36.48 20.23 32.69 35.64 38.05"/>
</svg>

After

Width:  |  Height:  |  Size: 977 B

View File

@ -0,0 +1,29 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #caeaf1;
}
.cls-2 {
fill: #d6e3ff;
}
.cls-3 {
fill: #b387ff;
}
.cls-4 {
fill: #ebdcff;
}
</style>
</defs>
<g>
<path class="cls-1" d="M23,26S10,44,10,51a7,7,0,0,0,7,7H47a7,7,0,0,0,7-7c0-7-13-25-13-25"/>
<path class="cls-2" d="M41,26V11h.5a2.5,2.5,0,0,0,0-5h-19a2.5,2.5,0,0,0,0,5H23V26S10,44,10,51a7,7,0,0,0,7,7H47a7,7,0,0,0,7-7C54,44,41,26,41,26Z"/>
<path class="cls-3" d="M43.81,34c3.4,5.08,8.07,12.7,8.07,16.31A5.69,5.69,0,0,1,46.19,56H17.81a5.68,5.68,0,0,1-5.68-5.69C12.13,46.7,20,34,20.19,34,29,34,29,37,36,37,40,37,43.81,34,43.81,34Z"/>
<circle class="cls-4" cx="24" cy="50" r="2"/>
<circle class="cls-4" cx="28.5" cy="45.5" r="1.5"/>
<circle class="cls-4" cx="23.5" cy="38.5" r="1.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 950 B

View File

@ -0,0 +1,24 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #fdc7c7;
}
.cls-2 {
fill: #fb7373;
}
</style>
</defs>
<g>
<g>
<path class="cls-1" d="M34.82,38.32,49.57,23.37a11.26,11.26,0,0,0-.1-15.92h0a11.26,11.26,0,0,0-15.92.1l-14.76,15Z"/>
<path class="cls-2" d="M20.37,20.91,7.2,34.26a11.26,11.26,0,0,0,.1,15.92h0a11.26,11.26,0,0,0,15.92-.11L36.4,36.72Z"/>
</g>
<g>
<path class="cls-2" d="M59.88,48.86a11,11,0,0,1-22,.14Z"/>
<path class="cls-2" d="M37.88,48.14a11,11,0,0,1,22-.14Z"/>
<polygon class="cls-1" points="60 48.82 38.01 49 38 47.18 59.99 47 60 48.82"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 747 B

View File

@ -0,0 +1,17 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #fb7373;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<path class="cls-1" d="M52.23,60H11.77a6.9,6.9,0,0,1-6.59-8.07l4-27A6.78,6.78,0,0,1,15.72,19H48.28a6.78,6.78,0,0,1,6.59,5.94l4,27A6.9,6.9,0,0,1,52.23,60Z"/>
<rect class="cls-2" x="28" y="28" width="9" height="24"/>
<rect class="cls-2" x="28" y="27.5" width="9" height="25" transform="translate(72.5 7.5) rotate(90)"/>
<path class="cls-1" d="M46.22,22.22H18.78V13a8.68,8.68,0,0,1,8.67-8.67h10.1A8.68,8.68,0,0,1,46.22,13Zm-24-3.44H42.78V13a5.24,5.24,0,0,0-5.23-5.23H27.45A5.24,5.24,0,0,0,22.22,13Z"/>
</svg>

After

Width:  |  Height:  |  Size: 742 B

View File

@ -0,0 +1,27 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #1eb37a;
}
.cls-2 {
fill: #fff;
}
.cls-3 {
fill: #34b4ff;
}
</style>
</defs>
<circle class="cls-1" cx="18.5" cy="19.5" r="14.5"/>
<g>
<path class="cls-2" d="M28.4,27a3,3,0,0,1-.62,1.8A3.33,3.33,0,0,1,26.17,30l-6.08,2a3,3,0,0,1-1.88,0l-6.08-2A3.16,3.16,0,0,1,9.9,27a4.24,4.24,0,0,1,1.39-3.12,4.9,4.9,0,0,1,3.35-1.29h.49a6.34,6.34,0,0,0,2.29,1.15,6.57,6.57,0,0,0,1.73.23,6.54,6.54,0,0,0,4-1.38h.48A4.59,4.59,0,0,1,28.4,27Z"/>
<path class="cls-2" d="M25.55,14.24V17a6.17,6.17,0,0,1-2.38,4.86A6.38,6.38,0,0,1,20.87,23a6.06,6.06,0,0,1-1.72.23A6.18,6.18,0,0,1,17.42,23a6.34,6.34,0,0,1-2.29-1.15A6.18,6.18,0,0,1,12.74,17V14.24a1.64,1.64,0,0,1,0-.22,6.16,6.16,0,0,1,.54-2.32A6.47,6.47,0,0,1,25,11.7,6,6,0,0,1,25.54,14C25.54,14.09,25.55,14.17,25.55,14.24Z"/>
</g>
<circle class="cls-3" cx="39.25" cy="39.25" r="20.25"/>
<g>
<path class="cls-2" d="M52.2,50.77a4.22,4.22,0,0,1-.87,2.54,4.73,4.73,0,0,1-2.27,1.62l-8.57,2.86a4.25,4.25,0,0,1-2.64,0l-8.57-2.86a4.45,4.45,0,0,1-3.14-4.16,6,6,0,0,1,1.95-4.39,6.9,6.9,0,0,1,4.73-1.82h.69a9.2,9.2,0,0,0,11.32,0h.69A6.46,6.46,0,0,1,52.2,50.77Z"/>
<path class="cls-2" d="M48.19,32.8v3.91a8.69,8.69,0,0,1-3.36,6.85,9,9,0,0,1-3.23,1.62,9.2,9.2,0,0,1-8.09-1.62,8.7,8.7,0,0,1-3.37-6.85V32.8c0-.11,0-.22,0-.32a8.57,8.57,0,0,1,.77-3.26,9.11,9.11,0,0,1,16.47,0,8.39,8.39,0,0,1,.77,3.26C48.18,32.58,48.19,32.69,48.19,32.8Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,24 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #d1d6dd;
}
.cls-2 {
fill: #00a6f7;
}
.cls-3 {
fill: #8e9fba;
}
</style>
</defs>
<g>
<path class="cls-1" d="M51.55,6.21a8.61,8.61,0,0,0-1.93-.52c-2.73-.35-1.59.2-2.24-.06A.32.32,0,0,0,47,5.8a.24.24,0,0,0,0,.15c.07,1.15.17,2.07.38,2,.48-.26,1.41,0,1.91-.06,2.42-.1,3.2,1.15,2.83,3.54a37.75,37.75,0,0,1-4.8,13.35,32.45,32.45,0,0,1-3.89,5.56c-1.85,2-3.17,1.93-5.1.11l-.15-.15c-4.88-5-7.61-11.86-8.79-18.64-.43-2.45.39-3.4,2.88-3.62a2.34,2.34,0,0,1,.95.14c.06-.78.08-1.57.07-2.36-.68.28-1.38,0-2.13.2-3.42.76-4.54,2.61-4,6.06.91,5.85,3.77,11,6.13,16.24a10.92,10.92,0,0,0,5,5.15,2.12,2.12,0,0,1,.43.28,5.33,5.33,0,0,1,4.12.45,2.83,2.83,0,0,1,1.09-1,12.79,12.79,0,0,0,4.88-5.52,71.52,71.52,0,0,0,2.93-6.95A54,54,0,0,0,53.51,15a24.19,24.19,0,0,0,.76-4.54A4.27,4.27,0,0,0,51.55,6.21Z"/>
<path class="cls-2" d="M53,18.25a1.66,1.66,0,0,0-3.21-.69c-2.48,6.76-6.74,14.06-9.3,14.06-2.23,0-6.61-7.29-9.3-14.1A1.71,1.71,0,0,0,28,18.78c1.35,3.41,5.78,13.74,10.73,15.86-1.05,4.55.64,7.32,2.14,9.77C42,46.27,43,47.87,43,50.28c-.13,5.23-5.43,6.3-13.35,6.3-4.14,0-9.26-1.36-11.34-4.4C17.11,50.51,17,48.52,18,46.1A12.59,12.59,0,0,1,21.06,42C23.5,39.48,26.53,36.41,23,30.5A1.71,1.71,0,0,0,20,32.27c2.21,3.64,1,4.85-1.39,7.27a15.62,15.62,0,0,0-3.87,5.32c-1.68,4.37-.56,7.44.68,9.25,3,4.35,9.49,5.89,14.16,5.89,4.95,0,16.53,0,16.77-9.63C46.45,47,45,44.65,43.8,42.63c-1.43-2.33-2.56-4.16-1.65-7.59a2,2,0,0,0,.06-.34c5.46-2.1,9.89-13.49,10.79-16Z"/>
<path class="cls-3" d="M26.49,25.64a6.42,6.42,0,1,1-6.09-4.35h0A6.34,6.34,0,0,1,26.49,25.64Z"/>
<path class="cls-3" d="M36.21,7a2.6,2.6,0,0,1-2.6,2.6h0c-1,0-.91-1.16-.91-2.6s-.24-2.6.91-2.6A2.61,2.61,0,0,1,36.21,7Z"/>
<path class="cls-3" d="M45,6.56A2.56,2.56,0,0,1,47.6,4h0c1,0,.89,1.18.87,2.61,0,1.67.29,3-1.62,2.48A2.54,2.54,0,0,1,45,6.57Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,33 @@
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<style>
.cls-1 {
fill: #fb7373;
}
.cls-2 {
fill: #ffb1c0;
}
.cls-3 {
fill: #fff;
stroke: #ffb1c0;
stroke-miterlimit: 10;
stroke-width: 2px;
}
.cls-4 {
fill: #fedada;
opacity: 0.25;
}
</style>
</defs>
<g>
<rect class="cls-1" x="6" y="6" width="52" height="52" rx="7.58"/>
<path class="cls-2" d="M53.41,19.59a2,2,0,0,0-2.82,0L32,38.17,19.41,25.59a2,2,0,0,0-2.82,0L6,36.17v5.66l12-12L30.59,42.41a2,2,0,0,0,2.82,0l20-20A2,2,0,0,0,53.41,19.59Z"/>
<circle class="cls-3" cx="18" cy="28" r="4"/>
<circle class="cls-3" cx="32" cy="41" r="4"/>
<circle class="cls-3" cx="52.5" cy="20.5" r="4"/>
<rect class="cls-4" x="6" y="24" width="52" height="18"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="172.66693mm"
height="19.869011mm"
viewBox="0 0 172.66693 19.869011"
version="1.1"
id="svg5"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e4, 2022-07-14)"
sodipodi:docname="PRN_Banner_Logo.svg"
inkscape:dataloss="true"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.14"
inkscape:cx="407.01754"
inkscape:cy="173.24561"
inkscape:window-width="1390"
inkscape:window-height="1205"
inkscape:window-x="584"
inkscape:window-y="108"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs2" /><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-17.665347,-25.597495)"><g
id="g6958"><rect
style="fill:#1953a4;fill-opacity:1;stroke:#1953a4;stroke-width:0.001;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
id="rect1907"
width="19.385"
height="19.379999"
x="17.68"
y="25.613001"
inkscape:export-filename="rect1907.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" /><g
aria-label="P"
transform="matrix(0.36808457,0,0,0.41364273,43.541475,28.391929)"
id="text2073"
style="font-size:42.8327px;font-family:'Trajan Pro';-inkscape-font-specification:'Trajan Pro, Normal';fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.862431"><path
d="m -68.456551,13.220795 c 0.01834,2.268624 -0.01764,6.967438 -0.07824,9.083709 -0.03582,1.251065 -0.91553,1.59871 -1.764011,2.054275 -1.05e-4,0.529631 -4.82e-4,0.728918 -6.09e-4,1.17631 1.28498,0 4.25801,-0.0049 4.386508,-0.0049 0.214163,0 2.948819,-3.4e-4 5.304618,-3.4e-4 -0.09285,-0.521346 -0.890534,-0.765322 -2.090824,-1.413227 -0.562953,-0.303877 -1.281534,-1.991633 -1.309246,-3.070346 -0.07583,-2.95172 -0.06938,-5.470042 -0.06938,-7.937909 V -4.4958538 c 0,-0.428327 0.0079,-0.762466 0.0079,-0.762466 0,0 2.174423,-0.279373 3.040358,-0.308351 1.031625,-0.03452 3.913481,-0.151492 6.472604,2.559415 2.559122,2.71090699 2.27823,6.4146497 1.884078,7.725814 -0.394152,1.3111643 -1.881538,3.2625519 -2.844972,3.9171479 -0.963435,0.654596 -1.862411,1.0359956 -3.359177,1.24289 -1.496767,0.2068949 -3.412001,-0.023882 -3.403676,0.2261479 0.02155,0.647266 2.356849,0.531491 3.027364,0.507821 0.670516,-0.02367 1.976921,0.0075 3.144765,-0.333939 1.167844,-0.3413988 3.332494,-1.4925958 4.397591,-2.6268058 1.895335,-2.018322 3.112969,-3.864086 3.148861,-6.520499 0.04107,-3.039569 -0.444482,-4.44153 -1.702238,-5.822209 -1.093796,-1.0281485 -2.048493,-1.7565988 -4.744156,-2.0531377 -0.867648,-0.00591 -1.955473,-0.00815 -3.274611,-0.00815 -1.970304,0 -6.201979,0.00343 -7.546706,-2.512e-4 -1.344726,-0.00368 -0.01779,-0.00575 -4.467438,0.00233 0.0012,0.3537653 -6.49e-4,0.2735701 -4.73e-4,0.6246381 0.427158,0.2619053 0.989472,0.586534 1.287609,0.9117073 0.46051,0.502271 0.541875,1.648131 0.539481,2.570302 -0.0057,2.20793799 -0.09501,2.37774099 -0.04952,8.004236 z"
id="path386"
sodipodi:nodetypes="ssccscsssscszzzzszzssccszccssss"
style="fill:#ffffff;fill-opacity:1;stroke:none" /></g><g
aria-label="R"
transform="matrix(0.39563776,0,0,0.39064801,44.41624,28.391929)"
id="text1004"
style="font-size:45.3539px;font-family:'Trajan Pro';-inkscape-font-specification:'Trajan Pro, Normal';fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.913197"><path
d="m -46.381766,13.353976 c -0.01223,2.243212 -0.08492,8.782844 -0.177789,9.742586 -0.06447,0.666209 -1.38779,2.412261 -2.147927,2.345646 -1.929922,1.199555 -0.250199,1.056622 1.010404,1.114005 1.56572,0.102316 3.092726,0.0318 3.822444,0.01896 0.602543,-0.0106 5.812522,0.493212 5.872411,0.229065 0.112418,-0.495834 -1.00092,-0.366093 -2.034728,-0.813263 -0.519795,-0.367442 -1.521008,-1.47994 -1.85299,-2.447962 -0.22677,-1.904864 -0.3585,-3.752424 -0.27575,-8.105592 l 0.04898,-2.576669 c 0.0043,-0.226728 1.119295,-0.114803 1.346062,-0.114803 1.615474,0.03695 3.414671,-0.0098 4.669131,0.784495 1.726031,1.092925 3.362185,3.349799 5.267049,5.844264 2.340272,3.599965 4.079105,5.448553 5.926856,6.667023 1.117869,0.737161 2.975123,1.601086 4.974806,1.602729 0.538764,4.43e-4 1.015339,-0.02394 1.448669,-0.06337 0.0068,-1.750116 0.0061,-0.338605 0.006,-1.745326 -0.536763,-0.0819 -2.186195,-0.433235 -4.635305,-3.018408 -2.630527,-2.811942 -5.714592,-6.848439 -9.433612,-11.610598 4.081851,-3.7643743 5.487822,-7.1205633 5.487822,-10.47675125 0,-3.12942005 -1.995571,-5.35176105 -3.129419,-6.16813105 -1.359478,-0.9747199 -2.088979,-1.4686689 -2.983134,-1.7083006 -1.042167,-0.00414 -15.502616,0.00407 -17.289252,0.00165 0.421026,0.179659 1.753492,0.3306459 2.544266,1.0370949 0.547976,0.489542 1.23546,1.215048 1.391933,2.04428 0.06419,1.523168 0.184564,9.800946 0.183043,10.079972 z m 4.265055,-18.5199823 c 0,-0.272123 0.01401,-0.468116 0.01401,-0.468116 0,0 2.689404,-0.615043 4.024277,-0.348254 3.971223,0.793694 6.376818,4.542477 6.621719,8.22748 0.22856,3.439116 -1.346638,6.936502 -2.829341,7.530166 -1.540856,0.6169493 -5.307973,0.7932893 -7.513187,0.4336973 -0.255776,-0.06767 -0.317477,-0.3174773 -0.317477,-0.5442473 z"
id="path182"
sodipodi:nodetypes="ssccssccsscscssccccscccscssscssscss"
style="fill:#ffffff;fill-opacity:1;stroke:none" /></g><g
aria-label="N"
transform="matrix(0.38001185,0,0,0.4006599,13.218138,21.430173)"
id="text1110"
style="font-size:38.0513px;font-family:'Trajan Pro';-inkscape-font-specification:'Trajan Pro, Normal';fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.940287"><path
d="m 22.414401,37.274291 c 1.243139,1.514945 5.81269,6.05318 10.379623,10.833775 4.364789,4.318207 6.961266,6.91021 8.199807,7.991161 1.238541,1.080951 2.397343,1.975644 3.24444,2.57881 0.07572,0.05392 0.15735,0.114427 0.242633,0.1764 0.56385,-0.0034 0.950635,-0.0019 1.504297,-0.0026 0.261974,-1.121488 0.281445,-3.783724 0.358758,-5.638495 0.07731,-1.854772 0.01971,-15.78434 0.01971,-17.515146 0,-1.730806 -0.02084,-2.516081 0.363787,-2.784029 0.384623,-0.267948 1.336578,-0.485128 0.505465,-1.36189 -1.647956,0.263864 -2.010206,0.262537 -2.844798,0.275326 -0.48706,0.0075 -1.48186,-0.155409 -2.774993,-0.25383 -0.755765,-0.05752 -1.62804,-0.10802 -1.623384,0.09869 0.0024,0.1058 0.285853,0.330921 1.047234,0.45307 0.838447,0.134514 1.707331,0.500405 1.949097,0.662222 0.16803,0.112465 0.437192,0.240074 0.5582,1.233763 0.151509,1.244165 0.137256,3.585901 0.162047,5.979779 0.04459,4.305819 0.06233,13.759575 0.06233,13.759575 -0.4581,-0.603937 -5.718922,-6.1178 -9.196404,-9.396197 C 31.094764,41.086289 24.086012,34.19445 23.515242,33.547578 c -0.380513,-0.380513 -2.632825,-2.64806 -2.937235,-2.64806 -0.304411,0 -0.368406,1.366359 -0.438525,3.182891 l -0.02043,18.736025 c -0.0015,1.355312 0.0063,3.496141 -0.2831,4.102219 -0.24455,0.512228 -0.663553,0.840682 -1.010746,1.09283 -0.600027,0.435769 -0.84232,0.511275 -1.605676,0.849836 3.039255,-0.0011 7.276282,0.0021 8.555603,0.002 0.100344,-0.378259 -0.624145,-0.30993 -1.057851,-0.414193 -0.576724,-0.138643 -1.089876,-0.175886 -1.403964,-0.600481 -0.393603,-0.532085 -0.518224,-1.562119 -0.561263,-2.903563 0,0 -0.104046,-4.500998 -0.132072,-7.159344 -0.02803,-2.658346 -0.214145,-6.315221 -0.249008,-7.786997 -0.03486,-1.471775 0.04343,-2.726411 0.04343,-2.726411 z"
id="path383"
sodipodi:nodetypes="cczscczzzcsssssssczcscsssccssszzcc"
style="fill:#ffffff;fill-opacity:1;stroke:none" /></g><g
aria-label="&amp;"
id="text442"
style="font-weight:bold;font-size:5.64444px;font-family:'Trajan Pro';-inkscape-font-specification:'Trajan Pro, Bold';fill:#ffffff;stroke-width:0.79375"
transform="matrix(0.39019932,0,0,0.39019932,13.911529,21.165112)"><path
d="m 25.991961,31.656869 c 0,0.491066 0.361244,1.247421 1.569154,1.247421 0.773288,0 1.151466,-0.344311 1.337732,-0.513644 0.485422,0.372533 0.688622,0.434622 1.332088,0.434622 h 0.434622 c 0.124178,0 0.169333,-0.01693 0.169333,-0.0508 0,-0.04516 -0.02822,-0.0508 -0.07902,-0.0508 -0.112889,0 -0.423333,-0.09031 -0.603955,-0.197556 -0.1524,-0.09031 -0.4064,-0.242711 -0.784577,-0.553155 0.321733,-0.378177 0.553155,-0.90311 0.553155,-1.337732 0,-0.06773 -0.08467,-0.1016 -0.191911,-0.112889 -0.316089,-0.03387 -0.541866,-0.03951 -0.677333,-0.03951 -0.04516,0 -0.09031,0.01129 -0.09031,0.05644 0,0.03387 0.03387,0.04516 0.06773,0.04516 0.08467,0 0.203199,0.01693 0.276577,0.08467 0.08467,0.07338 0.107244,0.174977 0.107244,0.259644 0,0.344311 -0.158044,0.711199 -0.270933,0.869244 -0.107244,-0.09596 -0.80151,-0.722489 -0.993421,-0.942622 -0.451555,-0.502355 -0.936977,-0.95391 -0.936977,-1.484488 0,-0.479777 0.366888,-0.643466 0.603955,-0.643466 0.316089,0 0.513644,0.118533 0.626533,0.254 0.124177,0.146755 0.163689,0.327377 0.163689,0.474133 0,0.08467 0.0056,0.107244 0.03951,0.107244 0.03387,0 0.05644,-0.01129 0.06773,-0.09596 0.01693,-0.08467 0.06209,-0.536222 0.06209,-0.722489 0,-0.06773 0,-0.112888 -0.09596,-0.146755 -0.1524,-0.06209 -0.406399,-0.118533 -0.682977,-0.118533 -0.982133,0 -1.377243,0.592666 -1.371599,1.038577 0.0056,0.254 0.0508,0.485422 0.287866,0.80151 -0.338666,0.163689 -0.920043,0.677333 -0.920043,1.337733 z m 1.761065,0.891821 c -0.587022,0 -1.083732,-0.451555 -1.083732,-1.15711 0,-0.451555 0.197555,-0.716844 0.389466,-0.857955 0.09596,0.141111 0.4572,0.547511 0.603955,0.705555 0.146755,0.158044 0.688622,0.688622 1.015999,0.970844 -0.118533,0.112888 -0.491066,0.338666 -0.925688,0.338666 z"
id="path444" /></g><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:8.80984px;font-family:'Minion Pro';-inkscape-font-specification:'Minion Pro, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-1.0324px;fill:#1953a4;fill-opacity:1;stroke:#1953a4;stroke-width:0.02;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
x="38.754604"
y="37.825531"
id="text1960"><tspan
sodipodi:role="line"
id="tspan1958"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:14.1783px;font-family:'Minion Pro';-inkscape-font-specification:'Minion Pro, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1953a4;fill-opacity:1;stroke:#1953a4;stroke-width:0.02;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
x="38.754604"
y="37.825531"
dx="0">Princeton &amp; Rutgers Neurology</tspan></text><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:14.1783px;font-family:'Minion Pro';-inkscape-font-specification:'Minion Pro, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:-0.407797px;fill:#1953a4;fill-opacity:1;stroke:#1953a4;stroke-width:0.02;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
x="121.98303"
y="44.08754"
id="text5617"><tspan
sodipodi:role="line"
id="tspan5615"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:5.36848px;font-family:'Minion Pro';-inkscape-font-specification:'Minion Pro, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke:#1953a4;stroke-width:0.02;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
x="121.98303"
y="44.08754">A New Jersey Center Of Excellence</tspan></text></g></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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